diff --git a/.github/workflows/nimbus-build.yml b/.github/workflows/nimbus-build.yml new file mode 100644 index 00000000000..0652f76adb8 --- /dev/null +++ b/.github/workflows/nimbus-build.yml @@ -0,0 +1,105 @@ +name: Nimbus Build + +# Priming-only, non-deploying parity build for the Starlight → Nimbus +# migration. It builds the `BUILD_TARGET=nimbus` target from the shared +# `src/content` and uploads `dist-nimbus/` as an artifact — it never deploys. +# +# Scoped to the migration author's PRs ONLY (see the `if` guard below), so +# contributors raising ordinary content/MDX PRs never see this job, never +# wait on it, and are never blocked by Nimbus-specific breakage (unported +# components, icon gaps, Sätteri quirks, etc.). Keep this OUT of the repo's +# required status checks until the cutover PR (Epic G1). +on: + pull_request: + branches: + - production + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + nimbus-build: + name: Nimbus Build (parity, non-deploying) + # Only run on the migration author's own PRs. Every other PR skips this + # job entirely (shows as "skipped", never "failed") — zero contributor + # impact. Extend this to an allowlist if more people drive the migration. + if: github.event.pull_request.user.login == 'MohamedH1998' + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Check out repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 1 + + - name: Set up pnpm + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 + with: + version: 11 + + - name: Set up node + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + id: setup-node + with: + node-version: 24.x + cache: pnpm + + - name: Restore node_modules (cache hit) + id: node-modules-cache + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 + with: + path: node_modules + key: node-modules-${{ runner.os }}-node-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('package.json') }} + + - name: Install node_modules (cache miss) + run: pnpm install --frozen-lockfile + if: steps.node-modules-cache.outputs.cache-hit != 'true' + + - name: Restore Astro (Nimbus) assets from cache + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 + with: + path: | + .astro-cache-nimbus/assets + key: astro-nimbus-assets-${{ hashFiles('src/assets/**') }} + restore-keys: | + astro-nimbus-assets- + + - name: Build (Nimbus target → dist-nimbus) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_TARGET: nimbus + # Match the proven Starlight baseline AND the cutover gate budget + # (rollout gate 1: cold build "within memory budget — today + # --max-old-space-size=8192"). `--max-old-space-size` bounds only the + # V8 heap, not total RSS; sharp/libvips allocate outside it, so on a + # 16 GB ubuntu-latest runner a larger heap risks a kernel OOM (137), + # not a cleaner build. The warm `.astro-cache-nimbus/assets` cache + # keeps image RSS low. If the cold Nimbus tree OOMs at 8192, that's + # the F2 memory item surfacing — and a real, must-know signal that + # the gate budget isn't met yet, not a number to paper over here. + NODE_OPTIONS: "--max-old-space-size=8192" + run: | + set -o pipefail + pnpm run prebuild + pnpm exec astro build 2>&1 | tee nimbus-build.log + + - name: Upload Nimbus build artifact + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: dist-nimbus + path: dist-nimbus + if-no-files-found: ignore + + # The build log is the whole point of a drift check when it fails — + # capture it (don't discard to /tmp like ci.yml does) so a failed + # Nimbus build is inspectable from the run's artifacts. + - name: Upload Nimbus build log + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: nimbus-build-log + path: nimbus-build.log + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 0bf53323328..2b7091d9d86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # build output dist/ +dist-nimbus/ distmd/ distllms/ @@ -8,6 +9,7 @@ distllms/ # Astro build cache .astro-cache/ +.astro-cache-nimbus/ # skills/ is fetched from middlecache via bin/fetch-skills.ts skills/ @@ -64,3 +66,6 @@ tools/relevant_changed_files.txt .flue/.flue-vite/ .flue/.flue-vite.wrangler.jsonc .flue/node_modules/ + +# nimbus-docs generated lint report (.nimbus/routes.json is a committed manifest) +.nimbus/lint.json diff --git a/.nimbus/routes.json b/.nimbus/routes.json new file mode 100644 index 00000000000..6cf1d73a94e --- /dev/null +++ b/.nimbus/routes.json @@ -0,0 +1,8465 @@ +{ + "version": 1, + "base": "/", + "knownRoutes": [ + "/", + "/1.1.1.1", + "/1.1.1.1/additional-options", + "/1.1.1.1/additional-options/dns-in-google-sheets", + "/1.1.1.1/additional-options/dns-over-discord", + "/1.1.1.1/additional-options/dns-over-tor", + "/1.1.1.1/check", + "/1.1.1.1/encryption", + "/1.1.1.1/encryption/dns-over-https", + "/1.1.1.1/encryption/dns-over-https/dns-over-https-client", + "/1.1.1.1/encryption/dns-over-https/encrypted-dns-browsers", + "/1.1.1.1/encryption/dns-over-https/make-api-requests", + "/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json", + "/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-wireformat", + "/1.1.1.1/encryption/dns-over-tls", + "/1.1.1.1/encryption/dnskey", + "/1.1.1.1/encryption/oblivious-dns-over-https", + "/1.1.1.1/faq", + "/1.1.1.1/infrastructure", + "/1.1.1.1/infrastructure/extended-dns-error-codes", + "/1.1.1.1/infrastructure/ipv6-networks", + "/1.1.1.1/infrastructure/network-operators", + "/1.1.1.1/infrastructure/sla-and-support", + "/1.1.1.1/ip-addresses", + "/1.1.1.1/privacy", + "/1.1.1.1/privacy/cloudflare-resolver-firefox", + "/1.1.1.1/privacy/public-dns-resolver", + "/1.1.1.1/setup", + "/1.1.1.1/setup/android", + "/1.1.1.1/setup/azure", + "/1.1.1.1/setup/gaming-consoles", + "/1.1.1.1/setup/google-cloud", + "/1.1.1.1/setup/ios", + "/1.1.1.1/setup/linux", + "/1.1.1.1/setup/macos", + "/1.1.1.1/setup/router", + "/1.1.1.1/setup/windows", + "/1.1.1.1/terms-of-use", + "/1.1.1.1/troubleshooting", + "/1.1.1.1/upstream-resolution", + "/agent-lee", + "/agent-memory", + "/agent-memory/api", + "/agent-memory/api/http-api", + "/agent-memory/api/workers-api", + "/agent-memory/concepts", + "/agent-memory/concepts/how-agent-memory-works", + "/agent-memory/concepts/namespaces-profiles", + "/agent-memory/get-started", + "/agent-memory/platform", + "/agent-memory/platform/limits", + "/agent-memory/platform/pricing", + "/agent-setup", + "/agent-setup/claude-code", + "/agent-setup/codex", + "/agent-setup/cursor", + "/agent-setup/github-copilot", + "/agent-setup/opencode", + "/agent-setup/windsurf", + "/agents", + "/agents/communication-channels", + "/agents/communication-channels/chat", + "/agents/communication-channels/chat/autonomous-responses", + "/agents/communication-channels/chat/chat-agents", + "/agents/communication-channels/chat/client-sdk", + "/agents/communication-channels/email", + "/agents/communication-channels/slack", + "/agents/communication-channels/voice", + "/agents/communication-channels/webhooks", + "/agents/communication-channels/webhooks/push-notifications", + "/agents/concepts", + "/agents/concepts/agentic-patterns", + "/agents/concepts/agentic-patterns/human-in-the-loop", + "/agents/concepts/agentic-patterns/long-running-agents", + "/agents/concepts/calling-llms", + "/agents/concepts/conversation-state-and-memory", + "/agents/concepts/tools", + "/agents/concepts/what-are-agents", + "/agents/concepts/workflows", + "/agents/examples", + "/agents/examples/browser-agent", + "/agents/examples/chat-agent", + "/agents/examples/email-agent", + "/agents/examples/slack-agent", + "/agents/examples/voice-agent", + "/agents/getting-started", + "/agents/getting-started/add-to-existing-project", + "/agents/getting-started/quick-start", + "/agents/getting-started/testing-your-agent", + "/agents/harnesses", + "/agents/harnesses/think", + "/agents/harnesses/think/client-tools", + "/agents/harnesses/think/configuration", + "/agents/harnesses/think/getting-started", + "/agents/harnesses/think/lifecycle-hooks", + "/agents/harnesses/think/messengers", + "/agents/harnesses/think/programmatic-submissions", + "/agents/harnesses/think/recovery", + "/agents/harnesses/think/scheduled-tasks", + "/agents/harnesses/think/sub-agents", + "/agents/harnesses/think/tools", + "/agents/harnesses/think/workflows", + "/agents/model-context-protocol", + "/agents/model-context-protocol/apis", + "/agents/model-context-protocol/apis/agent-api", + "/agents/model-context-protocol/apis/client-api", + "/agents/model-context-protocol/apis/handler-api", + "/agents/model-context-protocol/cloudflare", + "/agents/model-context-protocol/cloudflare/mcp-portal", + "/agents/model-context-protocol/cloudflare/servers-for-cloudflare", + "/agents/model-context-protocol/cloudflare/servers-for-cloudflare/community-mcp-server", + "/agents/model-context-protocol/codemode", + "/agents/model-context-protocol/guides", + "/agents/model-context-protocol/guides/build-codemode-mcp-server", + "/agents/model-context-protocol/guides/build-codemode-openapi-mcp-server", + "/agents/model-context-protocol/guides/build-mcp-client", + "/agents/model-context-protocol/guides/connect-mcp-client", + "/agents/model-context-protocol/guides/oauth-mcp-client", + "/agents/model-context-protocol/guides/remote-mcp-server", + "/agents/model-context-protocol/guides/securing-mcp-server", + "/agents/model-context-protocol/guides/test-remote-mcp-server", + "/agents/model-context-protocol/protocol", + "/agents/model-context-protocol/protocol/authorization", + "/agents/model-context-protocol/protocol/governance", + "/agents/model-context-protocol/protocol/tools", + "/agents/model-context-protocol/protocol/transport", + "/agents/platform", + "/agents/platform/limits", + "/agents/runtime", + "/agents/runtime/agents-api", + "/agents/runtime/communication", + "/agents/runtime/communication/chat-sdk", + "/agents/runtime/communication/http-sse", + "/agents/runtime/communication/protocol-messages", + "/agents/runtime/communication/readonly-connections", + "/agents/runtime/communication/routing", + "/agents/runtime/communication/websockets", + "/agents/runtime/execution", + "/agents/runtime/execution/agent-skills", + "/agents/runtime/execution/agent-tools", + "/agents/runtime/execution/durable-execution", + "/agents/runtime/execution/queue-tasks", + "/agents/runtime/execution/retries", + "/agents/runtime/execution/run-workflows", + "/agents/runtime/execution/schedule-tasks", + "/agents/runtime/execution/sub-agents", + "/agents/runtime/lifecycle", + "/agents/runtime/lifecycle/agent-class", + "/agents/runtime/lifecycle/callable-methods", + "/agents/runtime/lifecycle/get-current-agent", + "/agents/runtime/lifecycle/sessions", + "/agents/runtime/lifecycle/state", + "/agents/runtime/operations", + "/agents/runtime/operations/configuration", + "/agents/runtime/operations/cross-domain-authentication", + "/agents/runtime/operations/observability", + "/agents/runtime/operations/using-ai-models", + "/agents/tools", + "/agents/tools/ai-search", + "/agents/tools/browser", + "/agents/tools/codemode", + "/agents/tools/codemode/ai-sdk", + "/agents/tools/codemode/api-reference", + "/agents/tools/codemode/browser", + "/agents/tools/codemode/durable-runtime", + "/agents/tools/codemode/how-it-works", + "/agents/tools/codemode/mcp", + "/agents/tools/codemode/openapi", + "/agents/tools/codemode/tanstack-ai", + "/agents/tools/mcp", + "/agents/tools/payments", + "/agents/tools/payments/mpp", + "/agents/tools/payments/mpp-charge-for-http-content", + "/agents/tools/payments/x402", + "/agents/tools/payments/x402/charge-for-http-content", + "/agents/tools/payments/x402/charge-for-mcp-tools", + "/agents/tools/payments/x402/pay-from-agents-sdk", + "/agents/tools/payments/x402/pay-with-tool-plugins", + "/agents/tools/sandbox", + "/ai", + "/ai-crawl-control", + "/ai-crawl-control/changelog", + "/ai-crawl-control/configuration", + "/ai-crawl-control/configuration/ai-crawl-control-with-bots", + "/ai-crawl-control/configuration/ai-crawl-control-with-transform-rules", + "/ai-crawl-control/configuration/ai-crawl-control-with-waf", + "/ai-crawl-control/features", + "/ai-crawl-control/features/analyze-ai-traffic", + "/ai-crawl-control/features/manage-ai-crawlers", + "/ai-crawl-control/features/pay-per-crawl", + "/ai-crawl-control/features/pay-per-crawl/faq", + "/ai-crawl-control/features/pay-per-crawl/use-pay-per-crawl-as-ai-owner", + "/ai-crawl-control/features/pay-per-crawl/use-pay-per-crawl-as-ai-owner/connect-to-stripe", + "/ai-crawl-control/features/pay-per-crawl/use-pay-per-crawl-as-ai-owner/crawl-pages", + "/ai-crawl-control/features/pay-per-crawl/use-pay-per-crawl-as-ai-owner/discover-payable-content", + "/ai-crawl-control/features/pay-per-crawl/use-pay-per-crawl-as-ai-owner/error-codes", + "/ai-crawl-control/features/pay-per-crawl/use-pay-per-crawl-as-ai-owner/set-up-cloudflare-account", + "/ai-crawl-control/features/pay-per-crawl/use-pay-per-crawl-as-ai-owner/verify-ai-crawler", + "/ai-crawl-control/features/pay-per-crawl/use-pay-per-crawl-as-site-owner", + "/ai-crawl-control/features/pay-per-crawl/use-pay-per-crawl-as-site-owner/advanced-configuration", + "/ai-crawl-control/features/pay-per-crawl/use-pay-per-crawl-as-site-owner/enable-in-account-settings", + "/ai-crawl-control/features/pay-per-crawl/use-pay-per-crawl-as-site-owner/manage-payouts", + "/ai-crawl-control/features/pay-per-crawl/use-pay-per-crawl-as-site-owner/monitor-activity", + "/ai-crawl-control/features/pay-per-crawl/use-pay-per-crawl-as-site-owner/select-crawlers-to-charge", + "/ai-crawl-control/features/pay-per-crawl/use-pay-per-crawl-as-site-owner/set-a-pay-per-crawl-price", + "/ai-crawl-control/features/pay-per-crawl/what-is-pay-per-crawl", + "/ai-crawl-control/features/track-robots-txt", + "/ai-crawl-control/get-started", + "/ai-crawl-control/reference", + "/ai-crawl-control/reference/bots", + "/ai-crawl-control/reference/glossary", + "/ai-crawl-control/reference/graphql-api", + "/ai-crawl-control/reference/redirects-for-ai-training", + "/ai-crawl-control/reference/worker-templates", + "/ai-gateway", + "/ai-gateway/api-reference", + "/ai-gateway/changelog", + "/ai-gateway/configuration", + "/ai-gateway/configuration/authentication", + "/ai-gateway/configuration/bring-your-own-keys", + "/ai-gateway/configuration/custom-costs", + "/ai-gateway/configuration/custom-providers", + "/ai-gateway/configuration/fallbacks", + "/ai-gateway/configuration/manage-gateway", + "/ai-gateway/configuration/request-handling", + "/ai-gateway/demos", + "/ai-gateway/evaluations", + "/ai-gateway/evaluations/add-human-feedback", + "/ai-gateway/evaluations/add-human-feedback-api", + "/ai-gateway/evaluations/add-human-feedback-bindings", + "/ai-gateway/evaluations/set-up-evaluations", + "/ai-gateway/features", + "/ai-gateway/features/caching", + "/ai-gateway/features/dlp", + "/ai-gateway/features/dlp/set-up-dlp", + "/ai-gateway/features/dynamic-routing", + "/ai-gateway/features/dynamic-routing/json-configuration", + "/ai-gateway/features/dynamic-routing/usage", + "/ai-gateway/features/guardrails", + "/ai-gateway/features/guardrails/set-up-guardrail", + "/ai-gateway/features/guardrails/supported-model-types", + "/ai-gateway/features/guardrails/usage-considerations", + "/ai-gateway/features/rate-limiting", + "/ai-gateway/features/spend-limits", + "/ai-gateway/features/unified-billing", + "/ai-gateway/get-started", + "/ai-gateway/glossary", + "/ai-gateway/integrations", + "/ai-gateway/integrations/agents", + "/ai-gateway/integrations/aig-workers-ai-binding", + "/ai-gateway/integrations/coding-agents", + "/ai-gateway/integrations/coding-agents/claude-code", + "/ai-gateway/integrations/coding-agents/github-copilot-cli", + "/ai-gateway/integrations/coding-agents/openai-codex", + "/ai-gateway/integrations/coding-agents/pi", + "/ai-gateway/integrations/vercel-ai-sdk", + "/ai-gateway/mcp-server", + "/ai-gateway/models", + "/ai-gateway/observability", + "/ai-gateway/observability/analytics", + "/ai-gateway/observability/costs", + "/ai-gateway/observability/custom-metadata", + "/ai-gateway/observability/logging", + "/ai-gateway/observability/logging/logpush", + "/ai-gateway/observability/otel-integration", + "/ai-gateway/reference", + "/ai-gateway/reference/audit-logs", + "/ai-gateway/reference/limits", + "/ai-gateway/reference/pricing", + "/ai-gateway/reference/troubleshooting", + "/ai-gateway/tutorials", + "/ai-gateway/tutorials/create-first-aig-workers", + "/ai-gateway/tutorials/pruna-p-video", + "/ai-gateway/usage", + "/ai-gateway/usage/chat-completion", + "/ai-gateway/usage/providers", + "/ai-gateway/usage/providers/anthropic", + "/ai-gateway/usage/providers/azureopenai", + "/ai-gateway/usage/providers/baseten", + "/ai-gateway/usage/providers/bedrock", + "/ai-gateway/usage/providers/cartesia", + "/ai-gateway/usage/providers/cerebras", + "/ai-gateway/usage/providers/cohere", + "/ai-gateway/usage/providers/deepgram", + "/ai-gateway/usage/providers/deepseek", + "/ai-gateway/usage/providers/elevenlabs", + "/ai-gateway/usage/providers/fal", + "/ai-gateway/usage/providers/google-ai-studio", + "/ai-gateway/usage/providers/grok", + "/ai-gateway/usage/providers/groq", + "/ai-gateway/usage/providers/huggingface", + "/ai-gateway/usage/providers/ideogram", + "/ai-gateway/usage/providers/mistral", + "/ai-gateway/usage/providers/openai", + "/ai-gateway/usage/providers/openrouter", + "/ai-gateway/usage/providers/parallel", + "/ai-gateway/usage/providers/perplexity", + "/ai-gateway/usage/providers/replicate", + "/ai-gateway/usage/providers/vertex", + "/ai-gateway/usage/providers/workersai", + "/ai-gateway/usage/rest-api", + "/ai-gateway/usage/universal", + "/ai-gateway/usage/websockets-api", + "/ai-gateway/usage/websockets-api/non-realtime-api", + "/ai-gateway/usage/websockets-api/realtime-api", + "/ai-gateway/usage/worker-binding-methods", + "/ai-search", + "/ai-search/ai-search-api", + "/ai-search/api", + "/ai-search/api/instances", + "/ai-search/api/instances/rest-api", + "/ai-search/api/instances/workers-binding", + "/ai-search/api/items", + "/ai-search/api/items/rest-api", + "/ai-search/api/items/workers-binding", + "/ai-search/api/migration", + "/ai-search/api/migration/autorag-filter-format", + "/ai-search/api/migration/rest-api", + "/ai-search/api/migration/workers-binding", + "/ai-search/api/migration/workers-binding-legacy", + "/ai-search/api/search", + "/ai-search/api/search/mcp", + "/ai-search/api/search/public-endpoint", + "/ai-search/api/search/rest-api", + "/ai-search/api/search/workers-binding", + "/ai-search/concepts", + "/ai-search/concepts/how-ai-search-works", + "/ai-search/concepts/namespaces", + "/ai-search/concepts/search-modes", + "/ai-search/configuration", + "/ai-search/configuration/data-source", + "/ai-search/configuration/data-source/built-in-storage", + "/ai-search/configuration/data-source/r2", + "/ai-search/configuration/data-source/website", + "/ai-search/configuration/indexing", + "/ai-search/configuration/indexing/chunking", + "/ai-search/configuration/indexing/hybrid-search", + "/ai-search/configuration/indexing/keyword-search", + "/ai-search/configuration/indexing/metadata", + "/ai-search/configuration/indexing/path-filtering", + "/ai-search/configuration/indexing/service-api-token", + "/ai-search/configuration/indexing/syncing", + "/ai-search/configuration/indexing/vector-search", + "/ai-search/configuration/models", + "/ai-search/configuration/models/supported-models", + "/ai-search/configuration/retrieval", + "/ai-search/configuration/retrieval/boosting", + "/ai-search/configuration/retrieval/cache", + "/ai-search/configuration/retrieval/embed-search-snippets", + "/ai-search/configuration/retrieval/filtering", + "/ai-search/configuration/retrieval/public-endpoint", + "/ai-search/configuration/retrieval/query-rewriting", + "/ai-search/configuration/retrieval/reranking", + "/ai-search/configuration/retrieval/result-controls", + "/ai-search/configuration/retrieval/system-prompt", + "/ai-search/get-started", + "/ai-search/get-started/api", + "/ai-search/get-started/dashboard", + "/ai-search/get-started/wrangler", + "/ai-search/how-to", + "/ai-search/how-to/bring-your-own-generation-model", + "/ai-search/how-to/nlweb", + "/ai-search/how-to/per-tenant-search", + "/ai-search/how-to/simple-search-engine", + "/ai-search/mcp-server", + "/ai-search/platform", + "/ai-search/platform/limits-pricing", + "/ai-search/platform/release-note", + "/ai-search/wrangler-commands", + "/ai/models", + "/ai/models/@cf/ai4bharat/indictrans2-en-indic-1B", + "/ai/models/@cf/aisingapore/gemma-sea-lion-v4-27b-it", + "/ai/models/@cf/baai/bge-base-en-v1.5", + "/ai/models/@cf/baai/bge-large-en-v1.5", + "/ai/models/@cf/baai/bge-m3", + "/ai/models/@cf/baai/bge-reranker-base", + "/ai/models/@cf/baai/bge-small-en-v1.5", + "/ai/models/@cf/black-forest-labs/flux-1-schnell", + "/ai/models/@cf/black-forest-labs/flux-2-dev", + "/ai/models/@cf/black-forest-labs/flux-2-klein-4b", + "/ai/models/@cf/black-forest-labs/flux-2-klein-9b", + "/ai/models/@cf/bytedance/stable-diffusion-xl-lightning", + "/ai/models/@cf/deepgram/aura-1", + "/ai/models/@cf/deepgram/aura-2-en", + "/ai/models/@cf/deepgram/aura-2-es", + "/ai/models/@cf/deepgram/flux", + "/ai/models/@cf/deepgram/nova-3", + "/ai/models/@cf/deepseek-ai/deepseek-r1-distill-qwen-32b", + "/ai/models/@cf/defog/sqlcoder-7b-2", + "/ai/models/@cf/facebook/bart-large-cnn", + "/ai/models/@cf/facebook/detr-resnet-50", + "/ai/models/@cf/google/embeddinggemma-300m", + "/ai/models/@cf/google/gemma-2b-it-lora", + "/ai/models/@cf/google/gemma-3-12b-it", + "/ai/models/@cf/google/gemma-4-26b-a4b-it", + "/ai/models/@cf/google/gemma-7b-it-lora", + "/ai/models/@cf/huggingface/distilbert-sst-2-int8", + "/ai/models/@cf/ibm-granite/granite-4.0-h-micro", + "/ai/models/@cf/leonardo/lucid-origin", + "/ai/models/@cf/leonardo/phoenix-1.0", + "/ai/models/@cf/llava-hf/llava-1.5-7b-hf", + "/ai/models/@cf/lykon/dreamshaper-8-lcm", + "/ai/models/@cf/meta-llama/llama-2-7b-chat-hf-lora", + "/ai/models/@cf/meta/llama-2-7b-chat-fp16", + "/ai/models/@cf/meta/llama-2-7b-chat-int8", + "/ai/models/@cf/meta/llama-3-8b-instruct", + "/ai/models/@cf/meta/llama-3-8b-instruct-awq", + "/ai/models/@cf/meta/llama-3.1-70b-instruct", + "/ai/models/@cf/meta/llama-3.1-8b-instruct", + "/ai/models/@cf/meta/llama-3.1-8b-instruct-awq", + "/ai/models/@cf/meta/llama-3.1-8b-instruct-fast", + "/ai/models/@cf/meta/llama-3.1-8b-instruct-fp8", + "/ai/models/@cf/meta/llama-3.2-11b-vision-instruct", + "/ai/models/@cf/meta/llama-3.2-1b-instruct", + "/ai/models/@cf/meta/llama-3.2-3b-instruct", + "/ai/models/@cf/meta/llama-3.3-70b-instruct-fp8-fast", + "/ai/models/@cf/meta/llama-4-scout-17b-16e-instruct", + "/ai/models/@cf/meta/llama-guard-3-8b", + "/ai/models/@cf/meta/m2m100-1.2b", + "/ai/models/@cf/microsoft/phi-2", + "/ai/models/@cf/microsoft/resnet-50", + "/ai/models/@cf/mistral/mistral-7b-instruct-v0.1", + "/ai/models/@cf/mistral/mistral-7b-instruct-v0.2-lora", + "/ai/models/@cf/mistralai/mistral-small-3.1-24b-instruct", + "/ai/models/@cf/moonshotai/kimi-k2.5", + "/ai/models/@cf/moonshotai/kimi-k2.6", + "/ai/models/@cf/moonshotai/kimi-k2.7-code", + "/ai/models/@cf/myshell-ai/melotts", + "/ai/models/@cf/nvidia/nemotron-3-120b-a12b", + "/ai/models/@cf/openai/gpt-oss-120b", + "/ai/models/@cf/openai/gpt-oss-20b", + "/ai/models/@cf/openai/whisper", + "/ai/models/@cf/openai/whisper-large-v3-turbo", + "/ai/models/@cf/openai/whisper-tiny-en", + "/ai/models/@cf/pfnet/plamo-embedding-1b", + "/ai/models/@cf/pipecat-ai/smart-turn-v2", + "/ai/models/@cf/qwen/qwen2.5-coder-32b-instruct", + "/ai/models/@cf/qwen/qwen3-30b-a3b-fp8", + "/ai/models/@cf/qwen/qwen3-embedding-0.6b", + "/ai/models/@cf/qwen/qwq-32b", + "/ai/models/@cf/runwayml/stable-diffusion-v1-5-img2img", + "/ai/models/@cf/runwayml/stable-diffusion-v1-5-inpainting", + "/ai/models/@cf/stabilityai/stable-diffusion-xl-base-1.0", + "/ai/models/@cf/unum/uform-gen2-qwen-500m", + "/ai/models/@cf/zai-org/glm-4.7-flash", + "/ai/models/@cf/zai-org/glm-5.2", + "/ai/models/@hf/google/gemma-7b-it", + "/ai/models/@hf/meta-llama/meta-llama-3-8b-instruct", + "/ai/models/@hf/mistral/mistral-7b-instruct-v0.2", + "/ai/models/@hf/nousresearch/hermes-2-pro-mistral-7b", + "/ai/models/alibaba/hh1-i2v", + "/ai/models/alibaba/hh1-t2v", + "/ai/models/alibaba/qwen3-max", + "/ai/models/alibaba/qwen3.5-397b-a17b", + "/ai/models/alibaba/wan-2.6-image", + "/ai/models/alibaba/wan-2.7-i2v", + "/ai/models/anthropic/claude-fable-5", + "/ai/models/anthropic/claude-haiku-4.5", + "/ai/models/anthropic/claude-opus-4.5", + "/ai/models/anthropic/claude-opus-4.6", + "/ai/models/anthropic/claude-opus-4.7", + "/ai/models/anthropic/claude-opus-4.8", + "/ai/models/anthropic/claude-sonnet-4.5", + "/ai/models/anthropic/claude-sonnet-4.6", + "/ai/models/assemblyai/universal-3-pro", + "/ai/models/black-forest-labs/flux-2-flex", + "/ai/models/black-forest-labs/flux-2-max", + "/ai/models/black-forest-labs/flux-2-pro-preview", + "/ai/models/bytedance/seedance-2.0", + "/ai/models/bytedance/seedance-2.0-fast", + "/ai/models/bytedance/seedream-4.0", + "/ai/models/bytedance/seedream-4.5", + "/ai/models/bytedance/seedream-5-lite", + "/ai/models/deepseek/deepseek-v4-pro", + "/ai/models/google/gemini-2.5-flash", + "/ai/models/google/gemini-2.5-flash-lite", + "/ai/models/google/gemini-2.5-pro", + "/ai/models/google/gemini-3-flash", + "/ai/models/google/gemini-3.1-flash-lite", + "/ai/models/google/gemini-3.1-flash-tts", + "/ai/models/google/gemini-3.1-pro", + "/ai/models/google/imagen-4", + "/ai/models/google/nano-banana", + "/ai/models/google/nano-banana-2", + "/ai/models/google/nano-banana-pro", + "/ai/models/google/veo-3", + "/ai/models/google/veo-3-fast", + "/ai/models/google/veo-3.1", + "/ai/models/google/veo-3.1-fast", + "/ai/models/inworld/tts-1.5-max", + "/ai/models/inworld/tts-1.5-mini", + "/ai/models/inworld/tts-2", + "/ai/models/krea/krea-2-large", + "/ai/models/krea/krea-2-medium", + "/ai/models/krea/krea-2-medium-turbo", + "/ai/models/minimax/hailuo-2.3", + "/ai/models/minimax/hailuo-2.3-fast", + "/ai/models/minimax/m2.7", + "/ai/models/minimax/m3", + "/ai/models/minimax/music-2.6", + "/ai/models/minimax/speech-2.8-hd", + "/ai/models/minimax/speech-2.8-turbo", + "/ai/models/openai/gpt-4.1", + "/ai/models/openai/gpt-4.1-mini", + "/ai/models/openai/gpt-4.1-nano", + "/ai/models/openai/gpt-4o", + "/ai/models/openai/gpt-4o-mini", + "/ai/models/openai/gpt-4o-transcribe", + "/ai/models/openai/gpt-5", + "/ai/models/openai/gpt-5-chat", + "/ai/models/openai/gpt-5-mini", + "/ai/models/openai/gpt-5-nano", + "/ai/models/openai/gpt-5.1", + "/ai/models/openai/gpt-5.1-chat", + "/ai/models/openai/gpt-5.4", + "/ai/models/openai/gpt-5.4-mini", + "/ai/models/openai/gpt-5.4-nano", + "/ai/models/openai/gpt-5.4-pro", + "/ai/models/openai/gpt-5.5", + "/ai/models/openai/gpt-5.5-pro", + "/ai/models/openai/gpt-image-1.5", + "/ai/models/openai/gpt-image-2", + "/ai/models/openai/o3", + "/ai/models/openai/o3-mini", + "/ai/models/openai/o4-mini", + "/ai/models/openai/tts-1", + "/ai/models/openai/tts-1-hd", + "/ai/models/pixverse/v5.6", + "/ai/models/pixverse/v6", + "/ai/models/pruna/p-image", + "/ai/models/pruna/p-image-edit", + "/ai/models/pruna/p-image-try-on", + "/ai/models/pruna/p-image-upscale", + "/ai/models/pruna/p-video", + "/ai/models/pruna/p-video-animate", + "/ai/models/pruna/p-video-avatar", + "/ai/models/pruna/p-video-replace", + "/ai/models/recraft/recraftv3", + "/ai/models/recraft/recraftv4", + "/ai/models/recraft/recraftv4-1", + "/ai/models/recraft/recraftv4-1-pro", + "/ai/models/recraft/recraftv4-1-pro-vector", + "/ai/models/recraft/recraftv4-1-utility", + "/ai/models/recraft/recraftv4-1-utility-pro", + "/ai/models/recraft/recraftv4-1-utility-pro-vector", + "/ai/models/recraft/recraftv4-1-utility-vector", + "/ai/models/recraft/recraftv4-1-vector", + "/ai/models/recraft/recraftv4-pro", + "/ai/models/recraft/recraftv4-pro-vector", + "/ai/models/recraft/recraftv4-vector", + "/ai/models/runwayml/aleph-2", + "/ai/models/runwayml/gen-4.5", + "/ai/models/vidu/q3-pro", + "/ai/models/vidu/q3-turbo", + "/ai/models/xai/grok-4.20-0309-non-reasoning", + "/ai/models/xai/grok-4.20-0309-reasoning", + "/ai/models/xai/grok-4.20-multi-agent-0309", + "/ai/models/xai/grok-4.3", + "/ai/models/xai/grok-imagine-image", + "/ai/models/xai/grok-imagine-image-quality", + "/ai/models/xai/grok-imagine-video", + "/ai/models/xai/grok-imagine-video-1.5-preview", + "/ai/models/xai/grok-stt", + "/ai/models/xai/grok-tts", + "/ai/models/xai/grok-voice", + "/ai/related-products", + "/ai/related-products/agents", + "/ai/related-products/ai-crawl-control", + "/ai/related-products/ai-gateway", + "/ai/related-products/ai-search", + "/ai/related-products/browser-rendering", + "/ai/related-products/cloudflare-agent", + "/ai/related-products/dynamic-workers", + "/ai/related-products/sandbox-sdk", + "/ai/related-products/vectorize", + "/ai/related-products/workers-ai", + "/analytics", + "/analytics/account-and-zone-analytics", + "/analytics/account-and-zone-analytics/account-analytics", + "/analytics/account-and-zone-analytics/analytics-with-workers", + "/analytics/account-and-zone-analytics/app-security-reports", + "/analytics/account-and-zone-analytics/status-codes", + "/analytics/account-and-zone-analytics/threat-types", + "/analytics/account-and-zone-analytics/total-threats-stopped", + "/analytics/account-and-zone-analytics/zone-analytics", + "/analytics/analytics-engine", + "/analytics/analytics-engine/get-started", + "/analytics/analytics-engine/grafana", + "/analytics/analytics-engine/limits", + "/analytics/analytics-engine/pricing", + "/analytics/analytics-engine/recipes", + "/analytics/analytics-engine/recipes/usage-based-billing-for-your-saas-product", + "/analytics/analytics-engine/sampling", + "/analytics/analytics-engine/sql-api", + "/analytics/analytics-engine/sql-reference", + "/analytics/analytics-engine/sql-reference/aggregate-functions", + "/analytics/analytics-engine/sql-reference/bit-functions", + "/analytics/analytics-engine/sql-reference/conditional-functions", + "/analytics/analytics-engine/sql-reference/date-time-functions", + "/analytics/analytics-engine/sql-reference/encoding-functions", + "/analytics/analytics-engine/sql-reference/literals", + "/analytics/analytics-engine/sql-reference/mathematical-functions", + "/analytics/analytics-engine/sql-reference/operators", + "/analytics/analytics-engine/sql-reference/statements", + "/analytics/analytics-engine/sql-reference/string-functions", + "/analytics/analytics-engine/sql-reference/type-conversion-functions", + "/analytics/analytics-engine/worker-querying", + "/analytics/analytics-integrations", + "/analytics/analytics-integrations/datadog", + "/analytics/analytics-integrations/graylog", + "/analytics/analytics-integrations/new-relic", + "/analytics/analytics-integrations/prometheus", + "/analytics/analytics-integrations/sentinel", + "/analytics/analytics-integrations/splunk", + "/analytics/custom-dashboards", + "/analytics/faq", + "/analytics/faq/about-analytics", + "/analytics/faq/graphql-api-inconsistent-results", + "/analytics/faq/other-faqs", + "/analytics/faq/wae-faqs", + "/analytics/graphql-api", + "/analytics/graphql-api/errors", + "/analytics/graphql-api/features", + "/analytics/graphql-api/features/confidence-intervals", + "/analytics/graphql-api/features/data-sets", + "/analytics/graphql-api/features/discovery", + "/analytics/graphql-api/features/discovery/introspection", + "/analytics/graphql-api/features/discovery/settings", + "/analytics/graphql-api/features/filtering", + "/analytics/graphql-api/features/nested-structures", + "/analytics/graphql-api/features/pagination", + "/analytics/graphql-api/features/sorting", + "/analytics/graphql-api/getting-started", + "/analytics/graphql-api/getting-started/authentication", + "/analytics/graphql-api/getting-started/authentication/api-key-auth", + "/analytics/graphql-api/getting-started/authentication/api-token-auth", + "/analytics/graphql-api/getting-started/authentication/graphql-client-headers", + "/analytics/graphql-api/getting-started/compose-graphql-query", + "/analytics/graphql-api/getting-started/execute-graphql-query", + "/analytics/graphql-api/getting-started/explore-graphql-schema", + "/analytics/graphql-api/getting-started/querying-basics", + "/analytics/graphql-api/limits", + "/analytics/graphql-api/mcp-server", + "/analytics/graphql-api/migration-guides", + "/analytics/graphql-api/migration-guides/graphql-api-analytics", + "/analytics/graphql-api/migration-guides/network-analytics-v2", + "/analytics/graphql-api/migration-guides/network-analytics-v2/differences", + "/analytics/graphql-api/migration-guides/network-analytics-v2/node-reference", + "/analytics/graphql-api/migration-guides/network-analytics-v2/schema-map", + "/analytics/graphql-api/migration-guides/zone-analytics", + "/analytics/graphql-api/migration-guides/zone-analytics-colos", + "/analytics/graphql-api/sampling", + "/analytics/graphql-api/tutorials", + "/analytics/graphql-api/tutorials/capture-graphql-queries-from-dashboard", + "/analytics/graphql-api/tutorials/end-customer-analytics", + "/analytics/graphql-api/tutorials/querying-access-login-events", + "/analytics/graphql-api/tutorials/querying-container-metrics", + "/analytics/graphql-api/tutorials/querying-email-routing", + "/analytics/graphql-api/tutorials/querying-firewall-events", + "/analytics/graphql-api/tutorials/querying-magic-transit-endpoint-healthcheck-results", + "/analytics/graphql-api/tutorials/querying-magic-transit-tunnel-bandwidth-analytics", + "/analytics/graphql-api/tutorials/querying-magic-transit-tunnel-healthcheck-results", + "/analytics/graphql-api/tutorials/querying-network-firewall-ids-samples", + "/analytics/graphql-api/tutorials/querying-network-firewall-samples", + "/analytics/graphql-api/tutorials/querying-workers-metrics", + "/analytics/graphql-api/tutorials/use-graphql-create-widgets", + "/analytics/network-analytics", + "/analytics/network-analytics/configure", + "/analytics/network-analytics/configure/displayed-data", + "/analytics/network-analytics/configure/share-export", + "/analytics/network-analytics/configure/time-range", + "/analytics/network-analytics/get-started", + "/analytics/network-analytics/reference", + "/analytics/network-analytics/reference/data-collection", + "/analytics/network-analytics/understand", + "/analytics/network-analytics/understand/concepts", + "/analytics/network-analytics/understand/main-dashboard", + "/analytics/sampling", + "/analytics/types-of-analytics", + "/api-shield", + "/api-shield/api-gateway", + "/api-shield/changelog", + "/api-shield/get-started", + "/api-shield/glossary", + "/api-shield/management-and-monitoring", + "/api-shield/management-and-monitoring/api-routing", + "/api-shield/management-and-monitoring/developer-portal", + "/api-shield/management-and-monitoring/endpoint-labels", + "/api-shield/management-and-monitoring/endpoint-management", + "/api-shield/management-and-monitoring/endpoint-management/schema-learning", + "/api-shield/management-and-monitoring/session-identifiers", + "/api-shield/plans", + "/api-shield/reference", + "/api-shield/reference/classic-schema-validation", + "/api-shield/reference/terraform", + "/api-shield/security", + "/api-shield/security/api-discovery", + "/api-shield/security/authentication-posture", + "/api-shield/security/bola-vulnerability-detection", + "/api-shield/security/graphql-protection", + "/api-shield/security/graphql-protection/api", + "/api-shield/security/jwt-validation", + "/api-shield/security/jwt-validation/api", + "/api-shield/security/jwt-validation/jwt-worker", + "/api-shield/security/jwt-validation/transform-rules", + "/api-shield/security/mtls", + "/api-shield/security/mtls/byo-ca", + "/api-shield/security/mtls/configure", + "/api-shield/security/schema-validation", + "/api-shield/security/schema-validation/api", + "/api-shield/security/sequence-analytics", + "/api-shield/security/sequence-mitigation", + "/api-shield/security/sequence-mitigation/api", + "/api-shield/security/sequence-mitigation/custom-rules", + "/api-shield/security/sequence-mitigation/manage-sequence-rules", + "/api-shield/security/volumetric-abuse-detection", + "/api-shield/security/vulnerability-scanner", + "/argo-smart-routing", + "/argo-smart-routing/analytics", + "/argo-smart-routing/argo-for-packets", + "/argo-smart-routing/get-started", + "/artifacts", + "/artifacts/api", + "/artifacts/api/errors", + "/artifacts/api/git-protocol", + "/artifacts/api/rest-api", + "/artifacts/api/workers-binding", + "/artifacts/api/wrangler", + "/artifacts/concepts", + "/artifacts/concepts/best-practices", + "/artifacts/concepts/how-artifacts-works", + "/artifacts/concepts/namespaces", + "/artifacts/concepts/repositories", + "/artifacts/examples", + "/artifacts/examples/git-client", + "/artifacts/examples/isomorphic-git", + "/artifacts/examples/sandbox-sdk-artifacts", + "/artifacts/get-started", + "/artifacts/get-started/rest-api", + "/artifacts/get-started/workers", + "/artifacts/guides", + "/artifacts/guides/artifact-fs", + "/artifacts/guides/authentication", + "/artifacts/guides/event-subscriptions", + "/artifacts/guides/import-repositories", + "/artifacts/observability", + "/artifacts/observability/metrics", + "/artifacts/platform", + "/artifacts/platform/changelog", + "/artifacts/platform/limits", + "/artifacts/platform/pricing", + "/automatic-platform-optimization", + "/automatic-platform-optimization/about", + "/automatic-platform-optimization/about/plugin-compatibility", + "/automatic-platform-optimization/about/test-current-speed", + "/automatic-platform-optimization/get-started", + "/automatic-platform-optimization/get-started/activate-cf-wp-plugin", + "/automatic-platform-optimization/get-started/change-nameservers", + "/automatic-platform-optimization/get-started/confirm-dns-records", + "/automatic-platform-optimization/get-started/verify-apo-works", + "/automatic-platform-optimization/reference", + "/automatic-platform-optimization/reference/cache-device-type", + "/automatic-platform-optimization/reference/page-rule-integration", + "/automatic-platform-optimization/reference/query-parameters", + "/automatic-platform-optimization/reference/subdomain-subdirectories", + "/automatic-platform-optimization/troubleshooting", + "/automatic-platform-optimization/troubleshooting/faq", + "/billing", + "/billing/get-started", + "/billing/get-started/create-billing-profile", + "/billing/get-started/update-billing-info", + "/billing/manage", + "/billing/manage/billable-usage", + "/billing/manage/budget-alerts", + "/billing/manage/cancel-subscription", + "/billing/manage/change-plan", + "/billing/manage/invoices", + "/billing/manage/optimize-costs", + "/billing/manage/pay-invoices-overdue-balances", + "/billing/payment-methods", + "/billing/payment-methods/additional-payment-method-auto-retry", + "/billing/payment-methods/instant-bank-payments-link", + "/billing/payment-methods/stablecoin-payments", + "/billing/threshold-billing", + "/billing/troubleshoot", + "/billing/troubleshoot/error-reference", + "/billing/troubleshoot/resolve-cannot-remove-payment-method", + "/billing/troubleshoot/resolve-you-cannot-modify-this-subscription", + "/billing/troubleshoot/resolve-zone-cannot-be-upgraded", + "/billing/troubleshoot/troubleshoot-failed-payments", + "/billing/troubleshoot/troubleshoot-invoices", + "/billing/understand", + "/billing/understand/billing-permissions", + "/billing/understand/billing-policy", + "/billing/understand/faq", + "/billing/understand/how-billing-works", + "/billing/understand/how-charges-accrue", + "/billing/understand/preview-services", + "/billing/understand/sales-tax", + "/billing/understand/usage-based-billing", + "/bots", + "/bots/account-abuse-protection", + "/bots/additional-configurations", + "/bots/additional-configurations/ai-labyrinth", + "/bots/additional-configurations/block-ai-bots", + "/bots/additional-configurations/custom-rules", + "/bots/additional-configurations/detection-ids", + "/bots/additional-configurations/detection-ids/account-takeover-detections", + "/bots/additional-configurations/detection-ids/additional-detections", + "/bots/additional-configurations/detection-ids/scraping-detections", + "/bots/additional-configurations/ja3-ja4-fingerprint", + "/bots/additional-configurations/ja3-ja4-fingerprint/signals-intelligence", + "/bots/additional-configurations/javascript-detections", + "/bots/additional-configurations/managed-robots-txt", + "/bots/additional-configurations/sequence-rules", + "/bots/additional-configurations/static-resources", + "/bots/bot-analytics", + "/bots/changelog", + "/bots/concepts", + "/bots/concepts/bot", + "/bots/concepts/bot-detection-engines", + "/bots/concepts/bot-score", + "/bots/concepts/bot-tags", + "/bots/concepts/bot/signed-agents", + "/bots/concepts/bot/signed-agents/policy", + "/bots/concepts/bot/verified-bots", + "/bots/concepts/bot/verified-bots/policy", + "/bots/concepts/feedback-loop", + "/bots/get-started", + "/bots/get-started/bot-fight-mode", + "/bots/get-started/bot-management", + "/bots/get-started/super-bot-fight-mode", + "/bots/glossary", + "/bots/plans", + "/bots/plans/biz-and-ent", + "/bots/plans/bm-subscription", + "/bots/plans/free", + "/bots/plans/pro", + "/bots/reference", + "/bots/reference/alerts", + "/bots/reference/bot-management-variables", + "/bots/reference/bot-verification", + "/bots/reference/bot-verification/ip-validation", + "/bots/reference/bot-verification/web-bot-auth", + "/bots/reference/machine-learning-models", + "/bots/reference/sample-terms", + "/bots/troubleshooting", + "/bots/troubleshooting/bot-management-skips", + "/bots/troubleshooting/false-positives", + "/bots/troubleshooting/wordpress-loopback-issue", + "/bots/workers-templates", + "/bots/workers-templates/delay-action", + "/browser-run", + "/browser-run/cdp", + "/browser-run/cdp/mcp-clients", + "/browser-run/cdp/playwright", + "/browser-run/cdp/puppeteer", + "/browser-run/cdp/session-management", + "/browser-run/changelog", + "/browser-run/examples", + "/browser-run/faq", + "/browser-run/features", + "/browser-run/features/custom-fonts", + "/browser-run/features/human-in-the-loop", + "/browser-run/features/live-view", + "/browser-run/features/reuse-sessions", + "/browser-run/features/session-recording", + "/browser-run/features/webmcp", + "/browser-run/get-started", + "/browser-run/how-to", + "/browser-run/how-to/browser-run-with-do", + "/browser-run/how-to/deploy-worker", + "/browser-run/how-to/og-images-astro", + "/browser-run/how-to/pdf-generation", + "/browser-run/how-to/pre-render-pages", + "/browser-run/how-to/queues", + "/browser-run/limits", + "/browser-run/mcp-server", + "/browser-run/playwright", + "/browser-run/playwright/playwright-mcp", + "/browser-run/pricing", + "/browser-run/puppeteer", + "/browser-run/quick-actions", + "/browser-run/quick-actions/api-reference", + "/browser-run/quick-actions/content-endpoint", + "/browser-run/quick-actions/crawl-endpoint", + "/browser-run/quick-actions/json-endpoint", + "/browser-run/quick-actions/links-endpoint", + "/browser-run/quick-actions/markdown-endpoint", + "/browser-run/quick-actions/pdf-endpoint", + "/browser-run/quick-actions/scrape-endpoint", + "/browser-run/quick-actions/screenshot-endpoint", + "/browser-run/quick-actions/snapshot", + "/browser-run/reference", + "/browser-run/reference/automatic-request-headers", + "/browser-run/reference/browser-close-reasons", + "/browser-run/reference/robots-txt", + "/browser-run/reference/supported-fonts", + "/browser-run/reference/timeouts", + "/browser-run/reference/wrangler", + "/browser-run/reference/wrangler-commands", + "/browser-run/stagehand", + "/byoip", + "/byoip/address-maps", + "/byoip/address-maps/setup", + "/byoip/changelog", + "/byoip/concepts", + "/byoip/concepts/dynamic-advertisement", + "/byoip/concepts/dynamic-advertisement/best-practices", + "/byoip/concepts/irr-entries", + "/byoip/concepts/irr-entries/best-practices", + "/byoip/concepts/loa", + "/byoip/concepts/prefix-delegations", + "/byoip/concepts/route-filtering-rpki", + "/byoip/concepts/static-ips", + "/byoip/get-started", + "/byoip/glossary", + "/byoip/route-leak-detection", + "/byoip/service-bindings", + "/byoip/service-bindings/cdn-and-spectrum", + "/byoip/service-bindings/magic-transit-with-cdn", + "/byoip/troubleshooting", + "/byoip/troubleshooting/prefix-validation", + "/cache", + "/cache/advanced-configuration", + "/cache/advanced-configuration/cache-reserve", + "/cache/advanced-configuration/crawler-hints", + "/cache/advanced-configuration/early-hints", + "/cache/advanced-configuration/query-string-sort", + "/cache/advanced-configuration/serve-tailored-content", + "/cache/advanced-configuration/vary-for-images", + "/cache/cache-security", + "/cache/cache-security/avoid-web-poisoning", + "/cache/cache-security/cache-deception-armor", + "/cache/cache-security/cors", + "/cache/changelog", + "/cache/concepts", + "/cache/concepts/cache-behavior", + "/cache/concepts/cache-control", + "/cache/concepts/cache-responses", + "/cache/concepts/cdn-cache-control", + "/cache/concepts/customize-cache", + "/cache/concepts/default-cache-behavior", + "/cache/concepts/retention-vs-freshness", + "/cache/concepts/revalidation", + "/cache/get-started", + "/cache/glossary", + "/cache/how-to", + "/cache/how-to/always-online", + "/cache/how-to/cache-keys", + "/cache/how-to/cache-response-rules", + "/cache/how-to/cache-response-rules/create-api", + "/cache/how-to/cache-response-rules/create-dashboard", + "/cache/how-to/cache-response-rules/settings", + "/cache/how-to/cache-response-rules/terraform-example", + "/cache/how-to/cache-rules", + "/cache/how-to/cache-rules/create-api", + "/cache/how-to/cache-rules/create-dashboard", + "/cache/how-to/cache-rules/examples", + "/cache/how-to/cache-rules/examples/browser-cache-ttl", + "/cache/how-to/cache-rules/examples/bypass-cache-on-cookie", + "/cache/how-to/cache-rules/examples/cache-by-hostname-list", + "/cache/how-to/cache-rules/examples/cache-deception-armor", + "/cache/how-to/cache-rules/examples/cache-device-type", + "/cache/how-to/cache-rules/examples/cache-everything", + "/cache/how-to/cache-rules/examples/cache-everything-ignore-query-strings", + "/cache/how-to/cache-rules/examples/cache-ttl-by-status-code", + "/cache/how-to/cache-rules/examples/custom-cache-key", + "/cache/how-to/cache-rules/examples/edge-ttl", + "/cache/how-to/cache-rules/examples/origin-cache-control", + "/cache/how-to/cache-rules/examples/query-string-sort", + "/cache/how-to/cache-rules/examples/respect-strong-etags", + "/cache/how-to/cache-rules/order", + "/cache/how-to/cache-rules/page-rules-migration", + "/cache/how-to/cache-rules/settings", + "/cache/how-to/cache-rules/terraform-example", + "/cache/how-to/configure-cache-status-code", + "/cache/how-to/edge-browser-cache-ttl", + "/cache/how-to/edge-browser-cache-ttl/set-browser-ttl", + "/cache/how-to/purge-cache", + "/cache/how-to/purge-cache/purge-by-hostname", + "/cache/how-to/purge-cache/purge-by-single-file", + "/cache/how-to/purge-cache/purge-by-tags", + "/cache/how-to/purge-cache/purge-cache-key", + "/cache/how-to/purge-cache/purge-everything", + "/cache/how-to/purge-cache/purge-varied-images", + "/cache/how-to/purge-cache/purge-zone-versions", + "/cache/how-to/purge-cache/purge_by_prefix", + "/cache/how-to/set-caching-levels", + "/cache/how-to/tiered-cache", + "/cache/interaction-cloudflare-products", + "/cache/interaction-cloudflare-products/r2", + "/cache/interaction-cloudflare-products/waf-snippets", + "/cache/interaction-cloudflare-products/workers", + "/cache/interaction-cloudflare-products/workers-cache-rules", + "/cache/performance-review", + "/cache/performance-review/cache-analytics", + "/cache/performance-review/cache-performance", + "/cache/plans", + "/cache/reference", + "/cache/reference/cdn-reference-architecture", + "/cache/reference/csam-scanning", + "/cache/reference/development-mode", + "/cache/reference/etag-headers", + "/cache/troubleshooting", + "/cache/troubleshooting/always-online", + "/cache/troubleshooting/dynamic-content-and-login-issues", + "/cache/troubleshooting/mp4-videos-on-ios-and-safari", + "/changelog", + "/changelog/10", + "/changelog/11", + "/changelog/12", + "/changelog/13", + "/changelog/14", + "/changelog/15", + "/changelog/16", + "/changelog/17", + "/changelog/18", + "/changelog/19", + "/changelog/2", + "/changelog/20", + "/changelog/21", + "/changelog/22", + "/changelog/23", + "/changelog/24", + "/changelog/25", + "/changelog/26", + "/changelog/27", + "/changelog/28", + "/changelog/29", + "/changelog/3", + "/changelog/30", + "/changelog/31", + "/changelog/32", + "/changelog/33", + "/changelog/34", + "/changelog/35", + "/changelog/36", + "/changelog/37", + "/changelog/38", + "/changelog/39", + "/changelog/4", + "/changelog/40", + "/changelog/41", + "/changelog/42", + "/changelog/43", + "/changelog/44", + "/changelog/45", + "/changelog/46", + "/changelog/47", + "/changelog/48", + "/changelog/49", + "/changelog/5", + "/changelog/50", + "/changelog/6", + "/changelog/7", + "/changelog/8", + "/changelog/9", + "/changelog/post/2024-06-16-cloudflare-one", + "/changelog/post/2024-06-17-okta-risk-exchange", + "/changelog/post/2024-07-19-regionalized-generic-tiered-cache", + "/changelog/post/2024-09-05-cache-rules-templates", + "/changelog/post/2024-09-05-rules-templates", + "/changelog/post/2024-09-23-ai-audit-launch", + "/changelog/post/2024-09-24-magic-network-monitoring", + "/changelog/post/2024-10-01-ssh-with-access-for-infrastructure", + "/changelog/post/2024-10-02-custom-rule-search", + "/changelog/post/2024-10-08-new-gateway-fields", + "/changelog/post/2024-10-23-url-rewrites-wildcard", + "/changelog/post/2024-10-24-workflows-beta", + "/changelog/post/2024-11-07-cache-versioning", + "/changelog/post/2024-11-07-logpush-user-actions", + "/changelog/post/2024-11-07-shard-cache-by-cache-key", + "/changelog/post/2024-11-11-cache-no-store", + "/changelog/post/2024-11-20-smart-tiered-cache-for-r2", + "/changelog/post/2024-11-21-non-english-keyboard", + "/changelog/post/2024-11-22-cloud-connector-r2", + "/changelog/post/2024-11-22-cloud-data-extraction-aws", + "/changelog/post/2024-12-05-cloud-onramp-terraform", + "/changelog/post/2024-12-11-hyperdrive-caching-at-edge", + "/changelog/post/2024-12-11-terraform-snippets", + "/changelog/post/2024-12-17-bgp-support-cni", + "/changelog/post/2024-12-19-diagnostic-logs", + "/changelog/post/2024-12-19-escalate-user-submissions", + "/changelog/post/2024-12-19-reclassification-tab", + "/changelog/post/2024-12-29-faster-builds", + "/changelog/post/2025-01-03-source-code-confidence-level", + "/changelog/post/2025-01-06-waf-release", + "/changelog/post/2025-01-07-aig-provider-deepseek", + "/changelog/post/2025-01-07-d1-faster-query", + "/changelog/post/2025-01-08-smart-tiered-cache-for-load-balancing", + "/changelog/post/2025-01-09-rules-overview", + "/changelog/post/2025-01-13-waf-release", + "/changelog/post/2025-01-15-ssh-logs-and-logpush", + "/changelog/post/2025-01-15-workflows-more-steps", + "/changelog/post/2025-01-21-waf-release", + "/changelog/post/2025-01-26-worker-binding-methods", + "/changelog/post/2025-01-27-kv-increased-namespaces-limits", + "/changelog/post/2025-01-28-hyperdrive-automated-private-database-configuration", + "/changelog/post/2025-01-28-nodejs-compat-improvements", + "/changelog/post/2025-01-29-snippets-code-editor", + "/changelog/post/2025-01-30-browser-rendering-more-instances", + "/changelog/post/2025-01-30-stream-generated-captions-new-languages", + "/changelog/post/2025-01-31-html-rewriter-streaming", + "/changelog/post/2025-01-31-workers-platforms-static-assets", + "/changelog/post/2025-02-02-removed-meta-fields", + "/changelog/post/2025-02-03-terraform-v5-provider", + "/changelog/post/2025-02-03-workers-metrics-revamp", + "/changelog/post/2025-02-04-aig-provider-cartesia-eleven-cerebras", + "/changelog/post/2025-02-04-easier-onboarding-for-csam-scanning-tool", + "/changelog/post/2025-02-04-radar-ai-insights", + "/changelog/post/2025-02-04-updated-leaked-credentials-database", + "/changelog/post/2025-02-05-aig-request-handling", + "/changelog/post/2025-02-07-check-status", + "/changelog/post/2025-02-07-new-ways-to-get-started-on-workers", + "/changelog/post/2025-02-07-open-links-security-center", + "/changelog/post/2025-02-11-custom-errors-beta", + "/changelog/post/2025-02-11-waf-release", + "/changelog/post/2025-02-12-configurable-multiplexing-http2-to-origin", + "/changelog/post/2025-02-12-rules-upgraded-limits", + "/changelog/post/2025-02-13-improvements-unscannable-files", + "/changelog/post/2025-02-14-cert-bundling-for-custom-hostnames", + "/changelog/post/2025-02-14-customize-queue-retention-period", + "/changelog/post/2025-02-14-example-ai-prompts", + "/changelog/post/2025-02-14-introducing-dvr-for-stream-live", + "/changelog/post/2025-02-14-local-console-access", + "/changelog/post/2025-02-14-r2-super-slurper-faster-migrations", + "/changelog/post/2025-02-18-waf-release", + "/changelog/post/2025-02-20-builds-name-conflict", + "/changelog/post/2025-02-20-synchronous-uploads", + "/changelog/post/2025-02-20-updated-pricing-docs", + "/changelog/post/2025-02-21-images-bindings-in-workers", + "/changelog/post/2025-02-24-context-windows", + "/changelog/post/2025-02-24-r2-super-slurper-s3-compatible-support", + "/changelog/post/2025-02-24-waf-release", + "/changelog/post/2025-02-24-zaraz-dash-placement", + "/changelog/post/2025-02-25-agents-sdk", + "/changelog/post/2025-02-25-dlp-assist-for-m365", + "/changelog/post/2025-02-25-json-mode", + "/changelog/post/2025-02-25-rum-exclude-eu", + "/changelog/post/2025-02-25-workflows-concurrency-increased", + "/changelog/post/2025-02-26-guardrails", + "/changelog/post/2025-02-27-br-rest-api-beta", + "/changelog/post/2025-02-27-radar-dns-insights", + "/changelog/post/2025-02-28-wrangler-v4-rc", + "/changelog/post/2025-03-01-logpush-detections", + "/changelog/post/2025-03-03-saml-oidc-fields-saml-transformations", + "/changelog/post/2025-03-03-user-action-logging", + "/changelog/post/2025-03-03-waf-release", + "/changelog/post/2025-03-04-hyperdrive-pooling-near-database-and-ip-range-egress", + "/changelog/post/2025-03-06-media-transformations", + "/changelog/post/2025-03-06-oneclick-logpush", + "/changelog/post/2025-03-06-r2-bucket-locks", + "/changelog/post/2025-03-07-cloudflare-one-device-health-monitoring", + "/changelog/post/2025-03-07-updated-leaked-credentials-database", + "/changelog/post/2025-03-10-waf-release", + "/changelog/post/2025-03-11-emergency-waf-release", + "/changelog/post/2025-03-11-process-env-support", + "/changelog/post/2025-03-12-reply-limits", + "/changelog/post/2025-03-13-new-managed-iplist", + "/changelog/post/2025-03-13-wrangler-v4", + "/changelog/post/2025-03-14-breakpoint-debugging-with-vitest", + "/changelog/post/2025-03-17-importable-env", + "/changelog/post/2025-03-17-new-workers-ai-models", + "/changelog/post/2025-03-17-rerun-build", + "/changelog/post/2025-03-17-waf-release", + "/changelog/post/2025-03-17-warp-ga-android", + "/changelog/post/2025-03-17-warp-ga-ios", + "/changelog/post/2025-03-18-api-posture-management", + "/changelog/post/2025-03-18-npm-i-agents", + "/changelog/post/2025-03-18-radar-leaked-credentials-insights", + "/changelog/post/2025-03-19-emergency-waf-release", + "/changelog/post/2025-03-20-markdown-conversion", + "/changelog/post/2025-03-20-websockets", + "/changelog/post/2025-03-21-pdns-user-locations-role", + "/changelog/post/2025-03-21-resource-force-replacement-bug", + "/changelog/post/2025-03-21-sensitive-values-redacted", + "/changelog/post/2025-03-22-emergency-waf-release", + "/changelog/post/2025-03-22-next-js-vulnerability-waf", + "/changelog/post/2025-03-22-smart-placement-stablization", + "/changelog/post/2025-03-25-gzip-source-maps", + "/changelog/post/2025-03-25-higher-cpu-limits", + "/changelog/post/2025-03-25-pause-purge-queues", + "/changelog/post/2025-03-26-account-home-updates", + "/changelog/post/2025-03-27-ai-domains-available", + "/changelog/post/2025-03-27-automatic-audit-logs-beta-release", + "/changelog/post/2025-04-01-casb-email-security", + "/changelog/post/2025-04-01-purge-for-all", + "/changelog/post/2025-04-02-waf-release", + "/changelog/post/2025-04-04-playwright-beta", + "/changelog/post/2025-04-04-workers-fetch-api-override-cache-rules", + "/changelog/post/2025-04-07-autorag-open-beta", + "/changelog/post/2025-04-07-br-free-ga-playwright", + "/changelog/post/2025-04-07-durable-objects-free-tier", + "/changelog/post/2025-04-07-increase-trace-events-limit", + "/changelog/post/2025-04-07-mcp-servers-agents-sdk-updates", + "/changelog/post/2025-04-07-sqlite-in-durable-objects-ga", + "/changelog/post/2025-04-07-workflows-ga", + "/changelog/post/2025-04-08-deploy-to-cloudflare-button", + "/changelog/post/2025-04-08-fullstack-on-workers", + "/changelog/post/2025-04-08-hyperdrive-free-plan", + "/changelog/post/2025-04-08-hyperdrive-mysql-support", + "/changelog/post/2025-04-08-local-development", + "/changelog/post/2025-04-08-nodejs-crypto-and-tls", + "/changelog/post/2025-04-08-vite-plugin", + "/changelog/post/2025-04-09-hyperdrive-custom-certificate-support", + "/changelog/post/2025-04-09-qb-workers-logs-ga", + "/changelog/post/2025-04-09-scim-provisioning-logs", + "/changelog/post/2025-04-09-secrets-store-beta", + "/changelog/post/2025-04-09-snippets-ga", + "/changelog/post/2025-04-09-workers-timing", + "/changelog/post/2025-04-10-d1-read-replication-beta", + "/changelog/post/2025-04-10-kv-bulk-reads", + "/changelog/post/2025-04-10-launching-pipelines", + "/changelog/post/2025-04-10-r2-data-catalog-beta", + "/changelog/post/2025-04-11-http-redirect-custom-block-page-redirect", + "/changelog/post/2025-04-11-new-models-faster-inference", + "/changelog/post/2025-04-14-account-level-dlp-settings", + "/changelog/post/2025-04-14-icd11-support", + "/changelog/post/2025-04-14-waf-release", + "/changelog/post/2025-04-14-webrtc-beta-signed-urls", + "/changelog/post/2025-04-15-workers-api-fixes", + "/changelog/post/2025-04-17-pull-consumer-limits", + "/changelog/post/2025-04-18-custom-fields-raw-transformed-values", + "/changelog/post/2025-04-21-access-bulk-policy-tester", + "/changelog/post/2025-04-22-python-worker-cron-triggers", + "/changelog/post/2025-04-22-waf-release", + "/changelog/post/2025-04-23-autorag-metadata-filtering", + "/changelog/post/2025-04-24-custom-errors-ga", + "/changelog/post/2025-04-26-emergency-waf-release", + "/changelog/post/2025-04-28-fdqn-filtering-egress-policies", + "/changelog/post/2025-04-30-appliance-multiple-dns-servers", + "/changelog/post/2025-04-30-zero-trust-dashboard-dark-mode", + "/changelog/post/2025-05-01-browser-isolation-overview-page", + "/changelog/post/2025-05-01-r2-dashboard-updates", + "/changelog/post/2025-05-05-waf-release", + "/changelog/post/2025-05-06-private-health-monitoring-methods", + "/changelog/post/2025-05-06-terraform-v540-provider", + "/changelog/post/2025-05-07-forensic-copy-update", + "/changelog/post/2025-05-07-url-scanner-geoegress", + "/changelog/post/2025-05-08-finalization-registry", + "/changelog/post/2025-05-08-improved-payload-logging", + "/changelog/post/2025-05-08-open-attachments-with-browser-isolation", + "/changelog/post/2025-05-09-publish-to-queues-via-http", + "/changelog/post/2025-05-09-snippets-cloud-connector-lists-waf-bot-scores", + "/changelog/post/2025-05-12-case-sensitive-cwl", + "/changelog/post/2025-05-13-new-applications-added", + "/changelog/post/2025-05-13-rbi-saml-post-support", + "/changelog/post/2025-05-14-domain-category-improvements", + "/changelog/post/2025-05-14-hyperdrive-fedramp", + "/changelog/post/2025-05-14-media-transformations-origin-restrictions", + "/changelog/post/2025-05-14-python-worker-durable-object", + "/changelog/post/2025-05-15-open-links-browser-isolation", + "/changelog/post/2025-05-19-paygo-updates", + "/changelog/post/2025-05-19-terraform-v550-provider", + "/changelog/post/2025-05-19-waf-release", + "/changelog/post/2025-05-21-vite-plugin-chrome-devtools", + "/changelog/post/2025-05-22-handle-request-cancellation", + "/changelog/post/2025-05-23-graphql-api-explorer", + "/changelog/post/2025-05-27-protocol-detection-availability", + "/changelog/post/2025-05-27-waf-release", + "/changelog/post/2025-05-28-playwright-mcp", + "/changelog/post/2025-05-28-updated-attack-score-model", + "/changelog/post/2025-05-30-configuration-rules-webp", + "/changelog/post/2025-05-30-d1-rest-api-latency", + "/changelog/post/2025-05-30-pages-build-image-v3", + "/changelog/post/2025-06-02-user-groups-beta", + "/changelog/post/2025-06-02-waf-release", + "/changelog/post/2025-06-03-aig-openai-compatible-endpoint", + "/changelog/post/2025-06-03-shopify-o2o-improvements", + "/changelog/post/2025-06-03-visualize-your-worker-architecture", + "/changelog/post/2025-06-04-account-load-balancing-ui", + "/changelog/post/2025-06-05-open-next-size", + "/changelog/post/2025-06-09-custom-errors-fetch-4xx-5xx-assets", + "/changelog/post/2025-06-09-transform-rule-subrequest-matching", + "/changelog/post/2025-06-09-waf-release", + "/changelog/post/2025-06-09-workers-integrations-changes", + "/changelog/post/2025-06-10-default-env-vars", + "/changelog/post/2025-06-10-media-transformations-limits-increase", + "/changelog/post/2025-06-11-nsec3-support", + "/changelog/post/2025-06-16-internal-dns-beta-ui", + "/changelog/post/2025-06-16-waf-release", + "/changelog/post/2025-06-16-workers-platform-admin-role", + "/changelog/post/2025-06-17-advanced-routing", + "/changelog/post/2025-06-17-new-order-of-enforcement", + "/changelog/post/2025-06-17-open-next-ssrf", + "/changelog/post/2025-06-17-terraform-v560-provider", + "/changelog/post/2025-06-17-workers-terraform-sdk-api-fixes", + "/changelog/post/2025-06-18-log-explorer-ga", + "/changelog/post/2025-06-18-remote-bindings-beta", + "/changelog/post/2025-06-19-autorag-custom-metadata-and-context", + "/changelog/post/2025-06-19-autorag-filename-filter", + "/changelog/post/2025-06-20-cni-maintenance-alerts", + "/changelog/post/2025-06-20-increased-blob-size-limits-in-workers-analytics", + "/changelog/post/2025-06-23-account-level-dns-analytics-api", + "/changelog/post/2025-06-23-user-groups-ga", + "/changelog/post/2025-06-24-announcing-sandboxes", + "/changelog/post/2025-06-25-actors-package-alpha", + "/changelog/post/2025-06-25-getplatformproxy-support-remote-bindings", + "/changelog/post/2025-06-26-vite-plugin-cross-commands-binding", + "/changelog/post/2025-06-30-graceful-byoip-withdrawal", + "/changelog/post/2025-06-30-mail-authentication", + "/changelog/post/2025-06-30-warp-ga-android", + "/changelog/post/2025-06-30-warp-ga-ios", + "/changelog/post/2025-06-30-warp-linux-ga", + "/changelog/post/2025-06-30-warp-macos-ga", + "/changelog/post/2025-06-30-warp-windows-ga", + "/changelog/post/2025-07-01-access-supports-customer-metadata-boundary", + "/changelog/post/2025-07-01-browser-based-rdp-open-beta", + "/changelog/post/2025-07-01-pay-per-crawl", + "/changelog/post/2025-07-01-radar-bots-insights", + "/changelog/post/2025-07-01-refresh", + "/changelog/post/2025-07-01-vite-plugin-enhanced-assets-support", + "/changelog/post/2025-07-01-workers-deploy-button-supports-environment-variables-and-secrets", + "/changelog/post/2025-07-02-hyperdrive-configurable-connection-count", + "/changelog/post/2025-07-04-javascript-debug-terminals", + "/changelog/post/2025-07-07-cloudy-summaries-access-gateway", + "/changelog/post/2025-07-07-dashboard-app-library", + "/changelog/post/2025-07-07-increased-ip-list-limits", + "/changelog/post/2025-07-07-waf-release", + "/changelog/post/2025-07-08-autorag-jobs-view", + "/changelog/post/2025-07-09-onboarding-resources", + "/changelog/post/2025-07-09-usage-tracking", + "/changelog/post/2025-07-11-terraform-v570-provider", + "/changelog/post/2025-07-14-waf-release", + "/changelog/post/2025-07-15-udp-improvements", + "/changelog/post/2025-07-17-document-matching", + "/changelog/post/2025-07-17-vite-plugin-vite-7-support", + "/changelog/post/2025-07-18-brand-protection-api", + "/changelog/post/2025-07-21-emergency", + "/changelog/post/2025-07-21-subaddressing", + "/changelog/post/2025-07-21-virtual-appliance-kvm-proxmox", + "/changelog/post/2025-07-21-waf-release", + "/changelog/post/2025-07-22-br-local-dev", + "/changelog/post/2025-07-22-media-transformations-audio-mode", + "/changelog/post/2025-07-23-warp-linux-ga", + "/changelog/post/2025-07-23-warp-macos-ga", + "/changelog/post/2025-07-23-warp-windows-ga", + "/changelog/post/2025-07-23-workers-preview-urls", + "/changelog/post/2025-07-24-http-inspection-on-all-ports", + "/changelog/post/2025-07-24-warp-macos-beta", + "/changelog/post/2025-07-24-warp-windows-beta", + "/changelog/post/2025-07-28-br-pricing", + "/changelog/post/2025-07-28-spam-domain-category-introduced", + "/changelog/post/2025-07-28-waf-release", + "/changelog/post/2025-07-29-audit-logs-v2-ui-beta", + "/changelog/post/2025-07-30-mt-mwan-health-check-cmb-eu", + "/changelog/post/2025-07-31-terraform-v5-tunnels-routes", + "/changelog/post/2025-08-01-containers-in-vite-dev", + "/changelog/post/2025-08-01-terraform-v582-provider", + "/changelog/post/2025-08-04-builds-increased-disk-size", + "/changelog/post/2025-08-04-radar-ct-insights", + "/changelog/post/2025-08-04-waf-release", + "/changelog/post/2025-08-05-agents-mcp-update", + "/changelog/post/2025-08-05-openai-open-models", + "/changelog/post/2025-08-05-sandbox-sdk-major-update", + "/changelog/post/2025-08-06-zone-monitoring-improvements", + "/changelog/post/2025-08-07-cache-no-cache", + "/changelog/post/2025-08-07-emergency-waf-release", + "/changelog/post/2025-08-07-expanded-link-isolation", + "/changelog/post/2025-08-08-add-waituntil-cloudflare-workers", + "/changelog/post/2025-08-08-dot-env-in-local-dev", + "/changelog/post/2025-08-08-stream-live-observability", + "/changelog/post/2025-08-08-support-long-branch-names-preview-aliases", + "/changelog/post/2025-08-11-messagechannel", + "/changelog/post/2025-08-11-waf-release", + "/changelog/post/2025-08-13-ibm-cloud-logs-destination", + "/changelog/post/2025-08-14-new-python-handlers", + "/changelog/post/2025-08-14-workers-terraform-and-sdk-improvements", + "/changelog/post/2025-08-15-asnum-support-in-custom-rules", + "/changelog/post/2025-08-15-brand-protection-bulk-endpoint", + "/changelog/post/2025-08-15-extended-retention", + "/changelog/post/2025-08-15-gemini-application-replaces-bard", + "/changelog/post/2025-08-15-monitor-groups-for-load-balancing", + "/changelog/post/2025-08-15-nodejs-fs", + "/changelog/post/2025-08-15-sftp", + "/changelog/post/2025-08-15-static-assets-redirect-url", + "/changelog/post/2025-08-15-terraform-v584-provider", + "/changelog/post/2025-08-18-waf-release", + "/changelog/post/2025-08-19-event-subscriptions", + "/changelog/post/2025-08-19-improved-wrangler-error-screen", + "/changelog/post/2025-08-19-warp-linux-ga", + "/changelog/post/2025-08-19-warp-macos-ga", + "/changelog/post/2025-08-19-warp-windows-ga", + "/changelog/post/2025-08-21-byoip-dedicated-egress-ip", + "/changelog/post/2025-08-21-durable-objects-get-by-name", + "/changelog/post/2025-08-21-warp-windows-ga", + "/changelog/post/2025-08-22-audit-logs-v2-logpush", + "/changelog/post/2025-08-22-dedicated-egress-ip-logpush", + "/changelog/post/2025-08-22-kv-performance-improvements", + "/changelog/post/2025-08-22-waf-release", + "/changelog/post/2025-08-22-workflows-python-beta", + "/changelog/post/2025-08-25-ai-prompt-protection", + "/changelog/post/2025-08-25-secrets-store-ai-gateway", + "/changelog/post/2025-08-25-waf-release", + "/changelog/post/2025-08-25-workers-assets-javascript-content-type", + "/changelog/post/2025-08-26-access-mcp-oauth", + "/changelog/post/2025-08-26-casb-ai-integrations", + "/changelog/post/2025-08-26-mcp-server-portals", + "/changelog/post/2025-08-26-vectorize-list-vectors", + "/changelog/post/2025-08-27-ai-crawl-control-launch", + "/changelog/post/2025-08-27-partner-models", + "/changelog/post/2025-08-27-shadow-it-analytics", + "/changelog/post/2025-08-29-dex-mcp-server", + "/changelog/post/2025-08-29-emergency-waf-release", + "/changelog/post/2025-08-29-smart-tiered-cache-fallback-to-generic", + "/changelog/post/2025-08-29-terrform-v59-provider", + "/changelog/post/2025-08-29-warp-ai-diag-analyzer", + "/changelog/post/2025-09-01-updated-new-roles", + "/changelog/post/2025-09-01-waf-release", + "/changelog/post/2025-09-02-increased-static-asset-limits", + "/changelog/post/2025-09-02-tunnel-networks-list-endpoints-new-default", + "/changelog/post/2025-09-03-agents-sdk-beta-v5", + "/changelog/post/2025-09-03-log-headers-and-cookies", + "/changelog/post/2025-09-03-new-workers-api", + "/changelog/post/2025-09-03-rate-limiting-improvement", + "/changelog/post/2025-09-04-emergency-waf-release", + "/changelog/post/2025-09-05-bidirectional-health-check-any-on-ramp", + "/changelog/post/2025-09-05-embeddinggemma", + "/changelog/post/2025-09-07-builds-increased-cpu-paid", + "/changelog/post/2025-09-08-custom-ike-id-ipsec-tunnels", + "/changelog/post/2025-09-08-reminders-about-two-factor-authentication-backup-codes", + "/changelog/post/2025-09-08-waf-release", + "/changelog/post/2025-09-09-interactive-wrangler-assets", + "/changelog/post/2025-09-10-built-with-cloudflare-button", + "/changelog/post/2025-09-10-warp-macos-beta", + "/changelog/post/2025-09-10-warp-windows-beta", + "/changelog/post/2025-09-11-contextual-pivots", + "/changelog/post/2025-09-11-d1-automatic-read-retries", + "/changelog/post/2025-09-11-dns-filtering-for-private-network-onramps", + "/changelog/post/2025-09-11-increased-version-rollback-limit", + "/changelog/post/2025-09-11-new-results-table-view", + "/changelog/post/2025-09-11-regional-email-processing-gia", + "/changelog/post/2025-09-15-waf-release", + "/changelog/post/2025-09-16-dnsfw-analytics-ui", + "/changelog/post/2025-09-16-new-ai-enabled-search-for-zero-trust-dashboard", + "/changelog/post/2025-09-16-remote-bindings-ga", + "/changelog/post/2025-09-17-update-preview-url-setting", + "/changelog/post/2025-09-18-tunnel-hostname-routing", + "/changelog/post/2025-09-19-autorag-metrics", + "/changelog/post/2025-09-19-ratelimit-workers-ga", + "/changelog/post/2025-09-19-workers-rs-panic-recovery", + "/changelog/post/2025-09-22-browser-based-rdp-ga", + "/changelog/post/2025-09-22-waf-release", + "/changelog/post/2025-09-23-invalid-submissions", + "/changelog/post/2025-09-23-wrangler-dev-multi-config-cross-command-support", + "/changelog/post/2025-09-24-emergency-waf-release", + "/changelog/post/2025-09-24-higher-container-resource-limits", + "/changelog/post/2025-09-25-ai-search-more-models", + "/changelog/post/2025-09-25-announcing-r2-sql-open-beta", + "/changelog/post/2025-09-25-body-phase-selector", + "/changelog/post/2025-09-25-br-playwright-ga-stagehand-limits", + "/changelog/post/2025-09-25-data-catalog-compaction", + "/changelog/post/2025-09-25-new-granular-controls-for-saas-applications", + "/changelog/post/2025-09-25-pipelines-sql", + "/changelog/post/2025-09-25-sign-in-with-github", + "/changelog/post/2025-09-25-sso-for-all", + "/changelog/post/2025-09-25-workers-vpc", + "/changelog/post/2025-09-26-analytics-engine-sql-enhancements", + "/changelog/post/2025-09-26-ctx-exports", + "/changelog/post/2025-09-26-waf-release", + "/changelog/post/2025-09-28-emergency-waf-release", + "/changelog/post/2025-09-29-radar-regional-data", + "/changelog/post/2025-09-29-waf-release", + "/changelog/post/2025-09-30-warp-linux-ga", + "/changelog/post/2025-09-30-warp-macos-ga", + "/changelog/post/2025-09-30-warp-windows-ga", + "/changelog/post/2025-10-01-confidence-intervals", + "/changelog/post/2025-10-01-fine-grained-permissioning-beta", + "/changelog/post/2025-10-01-md-returned", + "/changelog/post/2025-10-01-new-container-instance-types", + "/changelog/post/2025-10-01-new-file-type-support", + "/changelog/post/2025-10-02-deepgram-flux", + "/changelog/post/2025-10-03-one-click-access-for-workers", + "/changelog/post/2025-10-03-waf-release", + "/changelog/post/2025-10-06-data-catalog-table-compaction", + "/changelog/post/2025-10-06-new-worker-overview-page", + "/changelog/post/2025-10-06-radar-pq-encryption-test", + "/changelog/post/2025-10-06-waf-release", + "/changelog/post/2025-10-07-emergency-waf-release", + "/changelog/post/2025-10-07-recovery-codes", + "/changelog/post/2025-10-07-warp-linux-ga", + "/changelog/post/2025-10-07-warp-macos-ga", + "/changelog/post/2025-10-07-warp-windows-ga", + "/changelog/post/2025-10-09-assets-terraform", + "/changelog/post/2025-10-09-radar-ct-log-activity-insights", + "/changelog/post/2025-10-09-workflows-terraform", + "/changelog/post/2025-10-10-increased-startup-time", + "/changelog/post/2025-10-10-new-domain-categories", + "/changelog/post/2025-10-13-waf-release", + "/changelog/post/2025-10-14-enhanced-metrics-drilldowns", + "/changelog/post/2025-10-14-sso-self-service-ux", + "/changelog/post/2025-10-16-durable-objects-data-studio", + "/changelog/post/2025-10-16-header-limit-increase", + "/changelog/post/2025-10-16-on-demand-security-report", + "/changelog/post/2025-10-16-warp-macos-beta", + "/changelog/post/2025-10-16-warp-windows-beta", + "/changelog/post/2025-10-17-app-sec-reports", + "/changelog/post/2025-10-17-emergency-waf-release", + "/changelog/post/2025-10-20-schedule-dns-policies-from-the-ui", + "/changelog/post/2025-10-20-waf-release", + "/changelog/post/2025-10-21-track-robots-txt", + "/changelog/post/2025-10-23-emergency-waf-release", + "/changelog/post/2025-10-23-new-markdown-conversion-endpoint", + "/changelog/post/2025-10-23-preview-url-default-behavior", + "/changelog/post/2025-10-24-automatic-resource-provisioning", + "/changelog/post/2025-10-24-emergency-waf-release", + "/changelog/post/2025-10-24-tanstack-start", + "/changelog/post/2025-10-27-ai-search-reranking-system-prompt", + "/changelog/post/2025-10-27-radar-tld-insights", + "/changelog/post/2025-10-27-rfi-tokens-in-dash", + "/changelog/post/2025-10-27-sentinel-connector", + "/changelog/post/2025-10-28-access-application-support-for-all-ports-and-protocols", + "/changelog/post/2025-10-28-casb-roles", + "/changelog/post/2025-10-28-raising-limits", + "/changelog/post/2025-10-30-builds-preview", + "/changelog/post/2025-10-30-email-2fa", + "/changelog/post/2025-10-30-emergency-waf-release", + "/changelog/post/2025-10-30-member-management-improvements", + "/changelog/post/2025-10-30-tcp-rtt-and-tcp-fields", + "/changelog/post/2025-10-31-brand-protection-logo-dashboard-report-abuse", + "/changelog/post/2025-10-31-increased-websocket-message-size-limit", + "/changelog/post/2025-11-03-waf-release", + "/changelog/post/2025-11-03-wrangler-output-file", + "/changelog/post/2025-11-04-query-cancellation", + "/changelog/post/2025-11-05-d1-jurisdiction", + "/changelog/post/2025-11-05-emergency-waf-release", + "/changelog/post/2025-11-05-logpush-permissions-update", + "/changelog/post/2025-11-06-applications-recategorised-plan", + "/changelog/post/2025-11-06-automatic-return-routing-beta", + "/changelog/post/2025-11-06-connector-designate-wan-link-breakout", + "/changelog/post/2025-11-07-automatic-tracing", + "/changelog/post/2025-11-07-cache-keys-for-cloudflare-trace", + "/changelog/post/2025-11-09-cloudflare-env-variable", + "/changelog/post/2025-11-10-ai-crawl-control-crawler-info", + "/changelog/post/2025-11-10-waf-release", + "/changelog/post/2025-11-11-cloudflared-proxy-dns", + "/changelog/post/2025-11-11-health-dashboards", + "/changelog/post/2025-11-11-resize-sql-window", + "/changelog/post/2025-11-11-warp-linux-ga", + "/changelog/post/2025-11-11-warp-macos-ga", + "/changelog/post/2025-11-11-warp-windows-ga", + "/changelog/post/2025-11-12-analytics-engine-further-sql-enhancements", + "/changelog/post/2025-11-12-bola-attack-detection", + "/changelog/post/2025-11-12-dex-logpush-jobs", + "/changelog/post/2025-11-13-fixed-custom-date", + "/changelog/post/2025-11-13-new-datasets", + "/changelog/post/2025-11-13-query-result-distribution", + "/changelog/post/2025-11-14-casb-digest", + "/changelog/post/2025-11-14-ssh-ca-enhancements", + "/changelog/post/2025-11-17-waf-release", + "/changelog/post/2025-11-18-temporary-adjustment-to-final-disposition-column", + "/changelog/post/2025-11-19-add-extra-headers-for-website-crawling", + "/changelog/post/2025-11-20-terraform-v5130-provider", + "/changelog/post/2025-11-21-builds-env-var-increase", + "/changelog/post/2025-11-21-emergency-waf-release", + "/changelog/post/2025-11-21-fuse-support-in-containers", + "/changelog/post/2025-11-21-new-cpu-pricing", + "/changelog/post/2025-11-21-threat-events-now-show-events-insights", + "/changelog/post/2025-11-21-wrangler-deploy-remote-config-management", + "/changelog/post/2025-11-24-radar-cloud-observability", + "/changelog/post/2025-11-24-waf-release", + "/changelog/post/2025-11-25-audit-logs-for-cache-purge-events", + "/changelog/post/2025-11-25-flux-2-dev-workers-ai", + "/changelog/post/2025-11-25-zombie-endpoint-risk-label", + "/changelog/post/2025-11-26-agents-resumable-streaming", + "/changelog/post/2025-12-01-build-image-policies-dev-plat", + "/changelog/post/2025-12-01-waf-release", + "/changelog/post/2025-12-02-emergency-waf-release", + "/changelog/post/2025-12-03-emergency-waf-release", + "/changelog/post/2025-12-03-reusable-access-policies", + "/changelog/post/2025-12-03-submission-terminology-update", + "/changelog/post/2025-12-04-hyperdrive-remote-database-local-dev", + "/changelog/post/2025-12-05-rcs-vuln", + "/changelog/post/2025-12-05-terraform-v5140-provider", + "/changelog/post/2025-12-05-waf-max-payload-size-change", + "/changelog/post/2025-12-08-python-cold-start-improvements", + "/changelog/post/2025-12-08-python-pywrangler", + "/changelog/post/2025-12-08-vite-optional-config", + "/changelog/post/2025-12-08-vite-programmatic-config", + "/changelog/post/2025-12-09-warp-macos-beta", + "/changelog/post/2025-12-09-warp-windows-beta", + "/changelog/post/2025-12-10-emergency-waf-release", + "/changelog/post/2025-12-10-pay-per-crawl-enhancements", + "/changelog/post/2025-12-11-builds-event-subscriptions", + "/changelog/post/2025-12-11-emergency-waf-release", + "/changelog/post/2025-12-11-sentinelone-destination", + "/changelog/post/2025-12-12-aggregation-support-and-more", + "/changelog/post/2025-12-12-durable-objects-sqlite-storage-billing", + "/changelog/post/2025-12-15-rules-of-durable-objects", + "/changelog/post/2025-12-16-new-duplicate-action-for-supported-cloudflare-one-resources", + "/changelog/post/2025-12-16-vitest-ctx-exports-support", + "/changelog/post/2025-12-16-wrangler-autoconfig", + "/changelog/post/2025-12-17-shadow-it-domain-analytics", + "/changelog/post/2025-12-18-cached-request-classification", + "/changelog/post/2025-12-18-dashboard-improvements", + "/changelog/post/2025-12-18-overview-tab", + "/changelog/post/2025-12-18-r2-data-catalog-snapshot-expiration", + "/changelog/post/2025-12-18-waf-release", + "/changelog/post/2025-12-18-wrangler-auth-token", + "/changelog/post/2025-12-19-tanstack-start-prerendering", + "/changelog/post/2025-12-19-terraform-v5150-provider", + "/changelog/post/2025-12-22-agents-sdk-ai-sdk-v6", + "/changelog/post/2025-12-31-connector-breakout-traffic-netflow", + "/changelog/post/2026-01-01-microfrontends", + "/changelog/post/2026-01-05-custom-instance-types", + "/changelog/post/2026-01-07-analytics-engine-support-for-like-and-having", + "/changelog/post/2026-01-08-access-audit-log-for-doh-users", + "/changelog/post/2026-01-09-wrangler-tab-completion", + "/changelog/post/2026-01-11-wrangler-types-check", + "/changelog/post/2026-01-12-dma-metro-code-field", + "/changelog/post/2026-01-12-enhanced-visibility-post-delivery-actions", + "/changelog/post/2026-01-12-stix2-available-for-threat-events-api", + "/changelog/post/2026-01-12-waf-release", + "/changelog/post/2026-01-13-ai-crawl-control-read-only-role", + "/changelog/post/2026-01-13-warp-linux-ga", + "/changelog/post/2026-01-13-warp-macos-ga", + "/changelog/post/2026-01-13-warp-windows-ga", + "/changelog/post/2026-01-13-wrangler-types-multi-environment", + "/changelog/post/2026-01-14-download-url-scanner-report-pdf", + "/changelog/post/2026-01-15-flux-2-klein-4b-workers-ai", + "/changelog/post/2026-01-15-networking-navigation-update", + "/changelog/post/2026-01-15-waf-release", + "/changelog/post/2026-01-15-warp-connector-ping-support", + "/changelog/post/2026-01-19-http3-499-reporting-improvement", + "/changelog/post/2026-01-20-ai-search-path-filtering", + "/changelog/post/2026-01-20-ai-search-simplified-api", + "/changelog/post/2026-01-20-array-map-functions", + "/changelog/post/2026-01-20-auxiliary-workers", + "/changelog/post/2026-01-20-cloudflare-typescript-v600-beta1", + "/changelog/post/2026-01-20-kv-dash-ui-homepage", + "/changelog/post/2026-01-20-sql-module-rule", + "/changelog/post/2026-01-20-terraform-v5160-provider", + "/changelog/post/2026-01-20-waf-release", + "/changelog/post/2026-01-22-deny-by-default-for-zones", + "/changelog/post/2026-01-22-explicit-placement-hints", + "/changelog/post/2026-01-22-granular-api-token-permissions", + "/changelog/post/2026-01-22-sha256-base64-encode-functions", + "/changelog/post/2026-01-23-increased-index-capacity", + "/changelog/post/2026-01-23-new-2fa-experience", + "/changelog/post/2026-01-23-pages-file-limit-increase", + "/changelog/post/2026-01-26-waf-release", + "/changelog/post/2026-01-27-body-buffering-settings", + "/changelog/post/2026-01-27-configure-cloudflare-source-ips", + "/changelog/post/2026-01-27-timezone-preferences", + "/changelog/post/2026-01-27-warp-macos-beta", + "/changelog/post/2026-01-27-warp-windows-beta", + "/changelog/post/2026-01-28-flux-2-klein-9b-workers-ai", + "/changelog/post/2026-01-30-bgp-over-tunnels", + "/changelog/post/2026-01-30-kv-reduced-minimum-cachettl", + "/changelog/post/2026-02-02-improved-accessibility-search-for-monitoring", + "/changelog/post/2026-02-02-waf-release", + "/changelog/post/2026-02-03-agents-workflows-integration", + "/changelog/post/2026-02-03-r2-local-uploads", + "/changelog/post/2026-02-03-threat-actor-name-mapping", + "/changelog/post/2026-02-03-workflows-visualizer", + "/changelog/post/2026-02-04-queues-free-plan", + "/changelog/post/2026-02-06-observability-ui-refresh", + "/changelog/post/2026-02-09-agents-sdk-v040", + "/changelog/post/2026-02-09-analytics-enhancements", + "/changelog/post/2026-02-09-approximate-aggregation-functions", + "/changelog/post/2026-02-09-indexing-improvements", + "/changelog/post/2026-02-09-pty-terminal-support", + "/changelog/post/2026-02-09-reference-documentation", + "/changelog/post/2026-02-09-tabs-and-pivots", + "/changelog/post/2026-02-10-waf-release", + "/changelog/post/2026-02-11-appliance-post-quantum-encryption", + "/changelog/post/2026-02-11-subrequests-limit", + "/changelog/post/2026-02-11-vite-plugin-child-environments", + "/changelog/post/2026-02-12-anycast-ips-on-dashboard", + "/changelog/post/2026-02-12-brand-protection-logo-matching-percentage-selector", + "/changelog/post/2026-02-12-markdown-for-agents", + "/changelog/post/2026-02-12-quick-editor-dev-tools-deprecation", + "/changelog/post/2026-02-12-radar-ai-bots-content-type", + "/changelog/post/2026-02-12-terraform-v5170-provider", + "/changelog/post/2026-02-13-access-policy-service-token-permissions", + "/changelog/post/2026-02-13-cloudflare-python-v500-beta1", + "/changelog/post/2026-02-13-glm-47-flash-workers-ai", + "/changelog/post/2026-02-13-origin-ca-certificate-support", + "/changelog/post/2026-02-13-pywrangler-windows-support", + "/changelog/post/2026-02-15-workers-best-practices", + "/changelog/post/2026-02-16-markdown-for-agents-improvements", + "/changelog/post/2026-02-16-waf-release", + "/changelog/post/2026-02-17-agents-sdk-v050", + "/changelog/post/2026-02-17-clientless-access-for-private-apps", + "/changelog/post/2026-02-17-docker-in-docker", + "/changelog/post/2026-02-17-policies-for-bookmarks", + "/changelog/post/2026-02-17-product-name-updates", + "/changelog/post/2026-02-18-cfworker-server-timing", + "/changelog/post/2026-02-19-ai-dashboard-experience-improvements", + "/changelog/post/2026-02-19-dex-supports-cmb-eu", + "/changelog/post/2026-02-19-threat-events-graphs", + "/changelog/post/2026-02-20-cloudy-in-casb", + "/changelog/post/2026-02-20-codemode-sdk-rewrite", + "/changelog/post/2026-02-20-tunnel-core-dashboard", + "/changelog/post/2026-02-23-hyperdrive-stable-functions-uncacheable", + "/changelog/post/2026-02-23-sandbox-backup-restore-api", + "/changelog/post/2026-02-23-saved-views-in-threat-events", + "/changelog/post/2026-02-24-deleteall-deletes-alarms", + "/changelog/post/2026-02-24-disable-live-inputs", + "/changelog/post/2026-02-24-observability-query-language", + "/changelog/post/2026-02-24-typed-bindings-setup-improvements-error-metrics", + "/changelog/post/2026-02-24-warp-linux-ga", + "/changelog/post/2026-02-24-warp-macos-ga", + "/changelog/post/2026-02-24-warp-windows-ga", + "/changelog/post/2026-02-25-agents-sdk-v060", + "/changelog/post/2026-02-25-higher-container-resource-limits", + "/changelog/post/2026-02-25-radar-aspa-insights", + "/changelog/post/2026-02-25-wrangler-autoconfig-ga", + "/changelog/post/2026-02-26-async-stale-while-revalidate", + "/changelog/post/2026-02-26-markdown-responses-for-1xxx-errors", + "/changelog/post/2026-02-27-mcp-portal-logpush", + "/changelog/post/2026-02-27-new-protocol-detection-protocols", + "/changelog/post/2026-02-27-radar-pq-key-transparency", + "/changelog/post/2026-03-01-rdp-clipboard-controls", + "/changelog/post/2026-03-02-agents-sdk-v070", + "/changelog/post/2026-03-02-default-gateway", + "/changelog/post/2026-03-02-waf-release", + "/changelog/post/2026-03-03-radar-network-quality-test", + "/changelog/post/2026-03-03-sandbox-watch-file-events", + "/changelog/post/2026-03-03-step-limits-to-25k", + "/changelog/post/2026-03-04-br-rest-api-limit-increase", + "/changelog/post/2026-03-04-gateway-authorization-proxy-open-beta", + "/changelog/post/2026-03-04-new-markdown-conversion-options", + "/changelog/post/2026-03-04-user-risk-score-access-policies", + "/changelog/post/2026-03-06-brand-protection-dismiss-match", + "/changelog/post/2026-03-06-radar-region-filtering-traffic-volume-navigation", + "/changelog/post/2026-03-06-realtimekit-multilingual-transcription", + "/changelog/post/2026-03-06-step-context-available", + "/changelog/post/2026-03-09-log-fields-updated", + "/changelog/post/2026-03-09-vulnerability-scanner", + "/changelog/post/2026-03-10-audit-logs-v2-ga", + "/changelog/post/2026-03-10-br-crawl-endpoint", + "/changelog/post/2026-03-10-warp-macos-beta", + "/changelog/post/2026-03-10-warp-windows-beta", + "/changelog/post/2026-03-11-ingest-field-selection", + "/changelog/post/2026-03-11-json-rfc9457-responses-for-1xxx-errors", + "/changelog/post/2026-03-11-nemotron-3-super-workers-ai", + "/changelog/post/2026-03-12-emergency-waf-release", + "/changelog/post/2026-03-12-retry-after-header-for-1xxx-errors", + "/changelog/post/2026-03-12-ssh-support", + "/changelog/post/2026-03-12-wrangler-containers-instances", + "/changelog/post/2026-03-15-durable-object-id-name", + "/changelog/post/2026-03-15-infinite-paging-investigations", + "/changelog/post/2026-03-16-topk-limit-increased-to-50", + "/changelog/post/2026-03-17-codemode-sdk-v021", + "/changelog/post/2026-03-17-collect-log-payload-header", + "/changelog/post/2026-03-17-new-security-overview-ui", + "/changelog/post/2026-03-17-scim-authentik-support", + "/changelog/post/2026-03-18-brand-protection-logo-match-preview", + "/changelog/post/2026-03-18-media-transformations-workers-binding", + "/changelog/post/2026-03-18-scim-audit-logging", + "/changelog/post/2026-03-18-worker-timing-field", + "/changelog/post/2026-03-19-hyperdrive-mysql-custom-certificate-support", + "/changelog/post/2026-03-19-kimi-k2-5-workers-ai", + "/changelog/post/2026-03-19-service-key-authentication-deprecated", + "/changelog/post/2026-03-19-wrangler-tunnel-commands", + "/changelog/post/2026-03-20-dns-analytics-cmb-eu", + "/changelog/post/2026-03-20-managed-oauth", + "/changelog/post/2026-03-20-mcp-portal-gateway-routing", + "/changelog/post/2026-03-20-metrics-and-settings-dashboard", + "/changelog/post/2026-03-20-tunnel-replica-overview-and-multi-log-streaming", + "/changelog/post/2026-03-23-agents-sdk-v080", + "/changelog/post/2026-03-23-ai-search-new-rest-api", + "/changelog/post/2026-03-23-ai-search-public-endpoint-and-snippets", + "/changelog/post/2026-03-23-custom-metadata-filtering", + "/changelog/post/2026-03-23-expanded-sql-functions-expressions-complex-types", + "/changelog/post/2026-03-23-local-dev-instance-methods", + "/changelog/post/2026-03-23-waf-release", + "/changelog/post/2026-03-23-web-assets-graphql-fields", + "/changelog/post/2026-03-24-cache-response-rules", + "/changelog/post/2026-03-24-docker-hub-images", + "/changelog/post/2026-03-24-dynamic-workers-open-beta", + "/changelog/post/2026-03-24-interconnects-navigation-update", + "/changelog/post/2026-03-24-oidc-claims-filtering-gateway-policies", + "/changelog/post/2026-03-24-secrets-config-property", + "/changelog/post/2026-03-24-waf-rule-preservation", + "/changelog/post/2026-03-25-har-file-detection-and-sanitization", + "/changelog/post/2026-03-25-logpush-granular-timestamps", + "/changelog/post/2026-03-25-rfc9440-mtls-fields", + "/changelog/post/2026-03-26-durable-object-id-jurisdiction", + "/changelog/post/2026-03-26-mcp-portal-code-mode", + "/changelog/post/2026-03-26-mcp-portal-context-optimization", + "/changelog/post/2026-03-26-outbound-workers", + "/changelog/post/2026-03-26-streaming-zip-handler", + "/changelog/post/2026-03-26-url-scanner-improvements", + "/changelog/post/2026-03-27-rfc9440-mtls-fields", + "/changelog/post/2026-03-30-waf-release", + "/changelog/post/2026-03-31-internal-dns-open-beta", + "/changelog/post/2026-03-copy-resources-as-json-or-post-requests", + "/changelog/post/2026-04-01-ai-search-wrangler-commands", + "/changelog/post/2026-04-01-deploy-hooks", + "/changelog/post/2026-04-01-l4-transport-telemetry-fields", + "/changelog/post/2026-04-01-logs-ui-refresh", + "/changelog/post/2026-04-01-radar-routing-section", + "/changelog/post/2026-04-01-wrangler-workflows-local", + "/changelog/post/2026-04-02-auto-retry-upstream-failures", + "/changelog/post/2026-04-02-bigquery-destination", + "/changelog/post/2026-04-02-mcp-portal-session-management", + "/changelog/post/2026-04-02-warp-linux-ga", + "/changelog/post/2026-04-02-warp-macos-ga", + "/changelog/post/2026-04-04-gemma-4-26b-a4b-workers-ai", + "/changelog/post/2026-04-05-regional-placement", + "/changelog/post/2026-04-06-dane-support-mx-deployments", + "/changelog/post/2026-04-06-gateway-dns-response-time-ms", + "/changelog/post/2026-04-06-organizations-public-beta", + "/changelog/post/2026-04-06-redesigned-support-portal", + "/changelog/post/2026-04-07-link-aggregation-lacp-appliance", + "/changelog/post/2026-04-07-mtls-byoca-dashboard", + "/changelog/post/2026-04-07-triage-status-tracking", + "/changelog/post/2026-04-07-waf-release", + "/changelog/post/2026-04-07-warp-windows-ga", + "/changelog/post/2026-04-07-websocket-auto-reply-to-close", + "/changelog/post/2026-04-08-high-risk-browsing", + "/changelog/post/2026-04-08-threat-events-notification", + "/changelog/post/2026-04-09-ai-search-content-selectors", + "/changelog/post/2026-04-09-casb-webhooks", + "/changelog/post/2026-04-09-new-workers-ai-models", + "/changelog/post/2026-04-09-relaxed-connection-limiting", + "/changelog/post/2026-04-10-browser-rendering-cdp-endpoint", + "/changelog/post/2026-04-10-canvas-remoting-performance", + "/changelog/post/2026-04-10-secret-scanning-support", + "/changelog/post/2026-04-13-billable-usage-dashboard-and-budget-alerts", + "/changelog/post/2026-04-13-containers-sandbox-ga", + "/changelog/post/2026-04-13-local-explorer", + "/changelog/post/2026-04-13-sandbox-outbound-workers-tls-auth", + "/changelog/post/2026-04-14-bigquery-dashboard-support", + "/changelog/post/2026-04-14-browser-wrangler-commands", + "/changelog/post/2026-04-14-cloudflare-api-token-detections", + "/changelog/post/2026-04-14-cloudflare-mesh", + "/changelog/post/2026-04-14-configurable-payload-log-masking", + "/changelog/post/2026-04-14-email-obfuscation-defer", + "/changelog/post/2026-04-14-oauth-consent-and-revoke", + "/changelog/post/2026-04-14-radar-citations", + "/changelog/post/2026-04-14-vpc-networks", + "/changelog/post/2026-04-15-agentlee-writeops-genui", + "/changelog/post/2026-04-15-br-observability", + "/changelog/post/2026-04-15-br-rename", + "/changelog/post/2026-04-15-br-webmcp", + "/changelog/post/2026-04-15-dex-consistent-last-seen-timestamps", + "/changelog/post/2026-04-15-graphql-analytics-api", + "/changelog/post/2026-04-15-independent-mfa", + "/changelog/post/2026-04-15-logpush-new-fields", + "/changelog/post/2026-04-15-new-rule-and-application-builders", + "/changelog/post/2026-04-15-registrar-api-beta", + "/changelog/post/2026-04-15-waf-release", + "/changelog/post/2026-04-15-workflows-limits-raised", + "/changelog/post/2026-04-16-ai-search-namespace-binding", + "/changelog/post/2026-04-16-artifacts-now-in-beta", + "/changelog/post/2026-04-16-email-sending-public-beta", + "/changelog/post/2026-04-16-hybrid-search-and-relevance-boosting", + "/changelog/post/2026-04-17-mcp-portal-homepage-and-sign-out", + "/changelog/post/2026-04-17-radar-ai-insights-updates", + "/changelog/post/2026-04-17-redirects-for-ai-training", + "/changelog/post/2026-04-17-smart-tiered-cache-for-public-cloud", + "/changelog/post/2026-04-17-tools-for-agentic-internet", + "/changelog/post/2026-04-20-kimi-k2-6-workers-ai", + "/changelog/post/2026-04-20-network-session-analytics", + "/changelog/post/2026-04-20-pipelines-logpush-destination", + "/changelog/post/2026-04-20-r2-sql-json-functions-explain-format", + "/changelog/post/2026-04-21-correlated-worker-durable-object-logs", + "/changelog/post/2026-04-21-logpush-subrequests-merging", + "/changelog/post/2026-04-21-network-overview-page", + "/changelog/post/2026-04-21-step-context-and-readable-streams", + "/changelog/post/2026-04-21-unified-routing-geoip-country-rules", + "/changelog/post/2026-04-21-waf-release", + "/changelog/post/2026-04-21-websocket-standard-binary-type", + "/changelog/post/2026-04-22-custom-dashboards-ga", + "/changelog/post/2026-04-22-snapshot-expiration-cleans-data-files", + "/changelog/post/2026-04-23-audit-logs-v2-organization-level", + "/changelog/post/2026-04-23-go-sdk-v6100", + "/changelog/post/2026-04-23-independent-mfa-aaguid-amr", + "/changelog/post/2026-04-24-nsl-all-onramps", + "/changelog/post/2026-04-24-terraform-v5190-provider", + "/changelog/post/2026-04-24-tf-migrate-tool-released", + "/changelog/post/2026-04-27-archive-and-audit-security-action-items", + "/changelog/post/2026-04-27-cache-response-rules-zone-versioning", + "/changelog/post/2026-04-27-resource-tagging-public-beta", + "/changelog/post/2026-04-27-structured-responses-for-5xx-errors", + "/changelog/post/2026-04-27-terraform-support", + "/changelog/post/2026-04-27-unified-workspace-brand-protection", + "/changelog/post/2026-04-27-waf-release", + "/changelog/post/2026-04-28-detection-entries-outside-profiles", + "/changelog/post/2026-04-28-dex-internet-outage-notification", + "/changelog/post/2026-04-28-dex-speed-test", + "/changelog/post/2026-04-28-direct-support-navigation", + "/changelog/post/2026-04-28-enforce-dns-only", + "/changelog/post/2026-04-28-improved-queues-metrics", + "/changelog/post/2026-04-28-pii-record-profile", + "/changelog/post/2026-04-29-dex-tests-to-auth", + "/changelog/post/2026-04-29-gateway-authorization-proxy-pac-files-ga", + "/changelog/post/2026-04-29-hyperdrive-vpc-private-databases", + "/changelog/post/2026-04-29-instant-bank-payments-via-link", + "/changelog/post/2026-04-30-cloudflare-python-v500", + "/changelog/post/2026-04-30-cloudflare-typescript-v600", + "/changelog/post/2026-04-30-data-classification", + "/changelog/post/2026-04-30-emergency-waf-release", + "/changelog/post/2026-04-30-go-sdk-v700", + "/changelog/post/2026-04-30-ipsec-post-quantum-third-party", + "/changelog/post/2026-04-30-r2-empty-bucket-folder-delete", + "/changelog/post/2026-04-30-radar-cloud-observatory-connection-metrics", + "/changelog/post/2026-04-30-radar-dark-mode", + "/changelog/post/2026-04-30-rum-navigation-types", + "/changelog/post/2026-04-30-shared-dictionaries-passthrough-beta", + "/changelog/post/2026-04-30-standalone-predefined-detection-entries", + "/changelog/post/2026-05-01-dynamic-workflows", + "/changelog/post/2026-05-04-keyboard-shortcuts", + "/changelog/post/2026-05-04-pingora-powers-cache", + "/changelog/post/2026-05-04-radar-routing-widgets", + "/changelog/post/2026-05-04-waf-release", + "/changelog/post/2026-05-06-cloudy-summaries-in-phishnet_o365", + "/changelog/post/2026-05-06-mesh-ipv6-routes", + "/changelog/post/2026-05-06-radar-tld-nameserver-performance", + "/changelog/post/2026-05-06-react-nextjs-vulnerabilities", + "/changelog/post/2026-05-06-taxii-support-for-threat-events-api", + "/changelog/post/2026-05-07-appliance-dhcp-options", + "/changelog/post/2026-05-07-appliance-source-based-breakout", + "/changelog/post/2026-05-07-automatic-tracing-across-do-and-worker-subrequests", + "/changelog/post/2026-05-07-csv-export-for-rfis", + "/changelog/post/2026-05-07-emergency-waf-release", + "/changelog/post/2026-05-07-stream-workers-binding", + "/changelog/post/2026-05-07-virtual-appliance-self-serve-api", + "/changelog/post/2026-05-08-planned-model-deprecations", + "/changelog/post/2026-05-11-nat-t-port-500", + "/changelog/post/2026-05-11-pipelines-pricing-announced", + "/changelog/post/2026-05-11-r2-data-catalog-pricing-announced", + "/changelog/post/2026-05-11-r2-sql-pricing-announced", + "/changelog/post/2026-05-11-waf-release", + "/changelog/post/2026-05-11-warp-linux-ga", + "/changelog/post/2026-05-11-warp-macos-ga", + "/changelog/post/2026-05-11-warp-windows-ga", + "/changelog/post/2026-05-12-access-login-page-refresh", + "/changelog/post/2026-05-12-natural-language-policy-creation", + "/changelog/post/2026-05-12-r2-data-catalog-graphql-analytics", + "/changelog/post/2026-05-12-single-anycast-ip-default", + "/changelog/post/2026-05-12-ssh-enabled-by-default", + "/changelog/post/2026-05-12-url-scanner-report-agent-readiness", + "/changelog/post/2026-05-13-agents-sdk-v0124", + "/changelog/post/2026-05-13-log-fields-updated", + "/changelog/post/2026-05-13-rum-405-method-not-allowed", + "/changelog/post/2026-05-14-domains-tab", + "/changelog/post/2026-05-14-joins-subqueries-multi-table-queries", + "/changelog/post/2026-05-15-emergency-waf-release", + "/changelog/post/2026-05-15-hyperdrive-pool-size-metrics", + "/changelog/post/2026-05-18-local-dev-tunnels", + "/changelog/post/2026-05-18-unified-routing-network-analytics", + "/changelog/post/2026-05-18-wrangler-support", + "/changelog/post/2026-05-19-casb-claude-compliance-api", + "/changelog/post/2026-05-19-cloudflare-as-identity-provider", + "/changelog/post/2026-05-19-event-subscriptions", + "/changelog/post/2026-05-19-radar-mrt-explorer", + "/changelog/post/2026-05-20-new-dns-records-ux", + "/changelog/post/2026-05-20-radar-content-type-and-api-traffic", + "/changelog/post/2026-05-20-waf-release", + "/changelog/post/2026-05-21-modernised-billing-profile", + "/changelog/post/2026-05-21-rest-api", + "/changelog/post/2026-05-21-tunnel-mesh-granular-permissions", + "/changelog/post/2026-05-21-vpc-networks-cloudflare-wan", + "/changelog/post/2026-05-26-bypass-status-for-uncacheable-responses", + "/changelog/post/2026-05-26-public-beta", + "/changelog/post/2026-05-26-warp-linux-ga", + "/changelog/post/2026-05-26-warp-macos-ga", + "/changelog/post/2026-05-26-warp-windows-ga", + "/changelog/post/2026-05-27-cloudflared-connectivity-prechecks", + "/changelog/post/2026-05-27-cloudy-regex-assistance", + "/changelog/post/2026-05-27-pipeline-binding-stream-field", + "/changelog/post/2026-05-27-transformation-flows", + "/changelog/post/2026-05-28-mcp-portal-tool-prompt-aliases", + "/changelog/post/2026-05-28-mesh-ha-replica-ui", + "/changelog/post/2026-05-28-named-email-recipients", + "/changelog/post/2026-05-28-r2-data-catalog-dashboard", + "/changelog/post/2026-05-28-realtimekit-track-recording", + "/changelog/post/2026-05-28-ssh-proxy-command", + "/changelog/post/2026-05-28-use-browser-run-quick-actions-directly-from-workers", + "/changelog/post/2026-05-29-log-fields-updated", + "/changelog/post/2026-05-29-radar-pq-tls-bug-detection", + "/changelog/post/2026-05-29-sandbox-named-tunnels", + "/changelog/post/2026-05-29-security-insights-default-scans", + "/changelog/post/2026-05-29-warp-macos-beta", + "/changelog/post/2026-05-29-warp-windows-beta", + "/changelog/post/2026-05-29-websocket-adapter-auto-reconnect", + "/changelog/post/2026-06-01-log-fields-updated", + "/changelog/post/2026-06-02-agents-sdk-v0140", + "/changelog/post/2026-06-02-cisco-ios-xe", + "/changelog/post/2026-06-02-cron-workflows", + "/changelog/post/2026-06-03-bulk-secrets-api", + "/changelog/post/2026-06-03-public-oauth-clients", + "/changelog/post/2026-06-03-saml-assertion-encryption", + "/changelog/post/2026-06-04-billable-usage-product-sidebar", + "/changelog/post/2026-06-04-idp-federation", + "/changelog/post/2026-06-04-migrations-pattern", + "/changelog/post/2026-06-05-gateway-egress", + "/changelog/post/2026-06-05-radar-traffic-chart-granularity", + "/changelog/post/2026-06-05-saga-rollbacks", + "/changelog/post/2026-06-05-spend-limits", + "/changelog/post/2026-06-05-union-intersect-except-select-distinct", + "/changelog/post/2026-06-08-brand-protection-cease-and-desist-letters", + "/changelog/post/2026-06-08-create-waf-rules-from-threat-events", + "/changelog/post/2026-06-08-realtimekit-post-meeting-transcription-ga", + "/changelog/post/2026-06-08-smtp-submission", + "/changelog/post/2026-06-08-threat-actor-profiles", + "/changelog/post/2026-06-09-deprecating-sandbox-sdk-features", + "/changelog/post/2026-06-09-waf-release", + "/changelog/post/2026-06-10-account-level-record-quota", + "/changelog/post/2026-06-10-ai-search-namespace-wrangler-commands", + "/changelog/post/2026-06-10-api-reference", + "/changelog/post/2026-06-10-hosted-images-binding", + "/changelog/post/2026-06-11-browser-run-snapshot-formats", + "/changelog/post/2026-06-11-custom-ai-prompt-topics", + "/changelog/post/2026-06-11-dynamic-workers-count", + "/changelog/post/2026-06-12-durable-objects-metrics-filter-by-id-name", + "/changelog/post/2026-06-12-kimi-k2-7-code-workers-ai", + "/changelog/post/2026-06-12-terraform-v5200-provider", + "/changelog/post/2026-06-12-user-agent-logging", + "/changelog/post/2026-06-15-threat-intelligence-fields", + "/changelog/post/2026-06-15-waf-release", + "/changelog/post/2026-06-16-agents-sdk-v0161", + "/changelog/post/2026-06-16-custom-spans", + "/changelog/post/2026-06-16-glm-52-workers-ai", + "/changelog/post/2026-06-16-new-optimization-features", + "/changelog/post/2026-06-16-rollback-options", + "/changelog/post/2026-06-16-tcp-connect-vpc-networks", + "/changelog/post/2026-06-17-dashboard-management", + "/changelog/post/2026-06-17-pqc-mldsa-aop-cots", + "/changelog/post/2026-06-18-cloudflare-fonts-error-handling-security", + "/changelog/post/2026-06-18-cloudflare-idp-default", + "/changelog/post/2026-06-18-planetscale-databases-cloudflare-billing", + "/changelog/post/2026-06-18-radar-workers-ai-inference-metric", + "/changelog/post/2026-06-19-apac-ne-apac-se-location-hints", + "/changelog/post/2026-06-19-outbound-connections-keep-dos-alive", + "/changelog/post/2026-06-19-temporary-accounts-for-agents", + "/changelog/post/2026-06-19-unified-routes-page", + "/changelog/post/2026-06-21-window-functions-distinct-set-operations", + "/changelog/post/2026-06-23-waf-release", + "/changelog/post/2026-06-24-ai-search-similarity-cache-controls", + "/changelog/post/2026-06-24-radar-ip-page-improvements", + "/changelog/post/2026-06-24-warp-macos-beta", + "/changelog/post/2026-1-15-crowdstrike-score", + "/changelog/post/access-analytics-v2", + "/changelog/post/cf1-data-security-analytics-v1", + "/changelog/post/dashboards-access-report", + "/changelog/post/gateway-analytics-v2", + "/changelog/post/gateway-application-categories-added", + "/changelog/post/heic-support", + "/changelog/post/new-applications-71825", + "/changelog/post/new-cloudflare-one-navigation-and-product-experience", + "/changelog/post/scheduled-waf-release", + "/changelog/product-group/ai", + "/changelog/product-group/ai/2", + "/changelog/product-group/ai/3", + "/changelog/product-group/ai/4", + "/changelog/product-group/ai/5", + "/changelog/product-group/ai/6", + "/changelog/product-group/ai/7", + "/changelog/product-group/analytics", + "/changelog/product-group/analytics/2", + "/changelog/product-group/analytics/3", + "/changelog/product-group/analytics/4", + "/changelog/product-group/analytics/5", + "/changelog/product-group/analytics/6", + "/changelog/product-group/application-performance", + "/changelog/product-group/application-performance/2", + "/changelog/product-group/application-performance/3", + "/changelog/product-group/application-performance/4", + "/changelog/product-group/application-security", + "/changelog/product-group/application-security/2", + "/changelog/product-group/application-security/3", + "/changelog/product-group/application-security/4", + "/changelog/product-group/application-security/5", + "/changelog/product-group/application-security/6", + "/changelog/product-group/application-security/7", + "/changelog/product-group/application-security/8", + "/changelog/product-group/application-security/9", + "/changelog/product-group/cloudflare-one", + "/changelog/product-group/cloudflare-one/10", + "/changelog/product-group/cloudflare-one/11", + "/changelog/product-group/cloudflare-one/12", + "/changelog/product-group/cloudflare-one/13", + "/changelog/product-group/cloudflare-one/2", + "/changelog/product-group/cloudflare-one/3", + "/changelog/product-group/cloudflare-one/4", + "/changelog/product-group/cloudflare-one/5", + "/changelog/product-group/cloudflare-one/6", + "/changelog/product-group/cloudflare-one/7", + "/changelog/product-group/cloudflare-one/8", + "/changelog/product-group/cloudflare-one/9", + "/changelog/product-group/consumer-services", + "/changelog/product-group/consumer-services/2", + "/changelog/product-group/core-platform", + "/changelog/product-group/core-platform/2", + "/changelog/product-group/core-platform/3", + "/changelog/product-group/core-platform/4", + "/changelog/product-group/core-platform/5", + "/changelog/product-group/core-platform/6", + "/changelog/product-group/core-platform/7", + "/changelog/product-group/core-platform/8", + "/changelog/product-group/developer-platform", + "/changelog/product-group/developer-platform/10", + "/changelog/product-group/developer-platform/11", + "/changelog/product-group/developer-platform/12", + "/changelog/product-group/developer-platform/13", + "/changelog/product-group/developer-platform/14", + "/changelog/product-group/developer-platform/15", + "/changelog/product-group/developer-platform/16", + "/changelog/product-group/developer-platform/17", + "/changelog/product-group/developer-platform/18", + "/changelog/product-group/developer-platform/19", + "/changelog/product-group/developer-platform/2", + "/changelog/product-group/developer-platform/20", + "/changelog/product-group/developer-platform/21", + "/changelog/product-group/developer-platform/22", + "/changelog/product-group/developer-platform/3", + "/changelog/product-group/developer-platform/4", + "/changelog/product-group/developer-platform/5", + "/changelog/product-group/developer-platform/6", + "/changelog/product-group/developer-platform/7", + "/changelog/product-group/developer-platform/8", + "/changelog/product-group/developer-platform/9", + "/changelog/product-group/docs-collections", + "/changelog/product-group/docs-collections/2", + "/changelog/product-group/docs-collections/3", + "/changelog/product-group/media", + "/changelog/product-group/network-security", + "/changelog/product-group/network-security/2", + "/changelog/product-group/network-security/3", + "/changelog/product-group/network-security/4", + "/changelog/product-group/privacy", + "/changelog/product-group/storage", + "/changelog/product-group/storage/2", + "/changelog/product-group/storage/3", + "/changelog/product-group/storage/4", + "/changelog/product/access", + "/changelog/product/access/2", + "/changelog/product/access/3", + "/changelog/product/agents", + "/changelog/product/agents/2", + "/changelog/product/ai-crawl-control", + "/changelog/product/ai-gateway", + "/changelog/product/ai-search", + "/changelog/product/ai-search/2", + "/changelog/product/analytics", + "/changelog/product/api-shield", + "/changelog/product/artifacts", + "/changelog/product/audit-logs", + "/changelog/product/billing", + "/changelog/product/browser-isolation", + "/changelog/product/browser-run", + "/changelog/product/cache", + "/changelog/product/casb", + "/changelog/product/cloudflare-for-saas", + "/changelog/product/cloudflare-network-firewall", + "/changelog/product/cloudflare-one", + "/changelog/product/cloudflare-one-appliance", + "/changelog/product/cloudflare-one-client", + "/changelog/product/cloudflare-one-client/2", + "/changelog/product/cloudflare-one-client/3", + "/changelog/product/cloudflare-one/2", + "/changelog/product/cloudflare-one/3", + "/changelog/product/cloudflare-tunnel-sase", + "/changelog/product/cloudflare-wan", + "/changelog/product/cloudflare-wan/2", + "/changelog/product/containers", + "/changelog/product/d1", + "/changelog/product/dex", + "/changelog/product/dlp", + "/changelog/product/dlp/2", + "/changelog/product/dns", + "/changelog/product/durable-objects", + "/changelog/product/email-security-cf1", + "/changelog/product/email-security-cf1/2", + "/changelog/product/email-service", + "/changelog/product/flagship", + "/changelog/product/fundamentals", + "/changelog/product/fundamentals/2", + "/changelog/product/fundamentals/3", + "/changelog/product/gateway", + "/changelog/product/gateway/2", + "/changelog/product/go-sdk", + "/changelog/product/hyperdrive", + "/changelog/product/images", + "/changelog/product/kv", + "/changelog/product/load-balancing", + "/changelog/product/log-explorer", + "/changelog/product/logpush", + "/changelog/product/logs", + "/changelog/product/magic-transit", + "/changelog/product/mesh", + "/changelog/product/multi-cloud-networking", + "/changelog/product/network-flow", + "/changelog/product/network-interconnect", + "/changelog/product/pages", + "/changelog/product/pipelines", + "/changelog/product/privacy-proxy", + "/changelog/product/queues", + "/changelog/product/r2", + "/changelog/product/r2-sql", + "/changelog/product/radar", + "/changelog/product/radar/2", + "/changelog/product/realtime", + "/changelog/product/registrar", + "/changelog/product/resource-tagging", + "/changelog/product/risk-score", + "/changelog/product/rules", + "/changelog/product/rules/2", + "/changelog/product/sandbox", + "/changelog/product/sdk", + "/changelog/product/secrets-store", + "/changelog/product/security-center", + "/changelog/product/security-center/2", + "/changelog/product/security-overview", + "/changelog/product/speed", + "/changelog/product/ssl", + "/changelog/product/stream", + "/changelog/product/support", + "/changelog/product/terraform", + "/changelog/product/tunnel", + "/changelog/product/vectorize", + "/changelog/product/waf", + "/changelog/product/waf/2", + "/changelog/product/waf/3", + "/changelog/product/waf/4", + "/changelog/product/waf/5", + "/changelog/product/web-analytics", + "/changelog/product/workers", + "/changelog/product/workers-ai", + "/changelog/product/workers-ai/2", + "/changelog/product/workers-analytics-engine", + "/changelog/product/workers-for-platforms", + "/changelog/product/workers-vpc", + "/changelog/product/workers/10", + "/changelog/product/workers/2", + "/changelog/product/workers/3", + "/changelog/product/workers/4", + "/changelog/product/workers/5", + "/changelog/product/workers/6", + "/changelog/product/workers/7", + "/changelog/product/workers/8", + "/changelog/product/workers/9", + "/changelog/product/workflows", + "/changelog/product/zaraz", + "/china-network", + "/china-network/concepts", + "/china-network/concepts/china-dns", + "/china-network/concepts/global-acceleration", + "/china-network/concepts/icp", + "/china-network/faq", + "/china-network/get-started", + "/china-network/reference", + "/china-network/reference/available-products", + "/china-network/reference/infrastructure", + "/china-network/videos", + "/client-ip-geolocation", + "/client-ip-geolocation/about", + "/client-ip-geolocation/faq", + "/client-ip-geolocation/get-started", + "/client-side-security", + "/client-side-security/alerts", + "/client-side-security/alerts/alert-types", + "/client-side-security/alerts/configure", + "/client-side-security/best-practices", + "/client-side-security/best-practices/deploy-rules-in-production", + "/client-side-security/best-practices/handle-an-alert", + "/client-side-security/detection", + "/client-side-security/detection/monitor-connections-scripts", + "/client-side-security/detection/review-changed-scripts", + "/client-side-security/detection/review-malicious-scripts", + "/client-side-security/faq", + "/client-side-security/get-started", + "/client-side-security/how-it-works", + "/client-side-security/how-it-works/malicious-script-detection", + "/client-side-security/reference", + "/client-side-security/reference/api", + "/client-side-security/reference/csp-header", + "/client-side-security/reference/pci-dss", + "/client-side-security/reference/roles-and-permissions", + "/client-side-security/reference/script-statuses", + "/client-side-security/reference/settings", + "/client-side-security/release-notes", + "/client-side-security/rules", + "/client-side-security/rules/create-api", + "/client-side-security/rules/create-dashboard", + "/client-side-security/rules/csp-directives", + "/client-side-security/rules/violations", + "/client-side-security/troubleshooting", + "/cloudflare-challenges", + "/cloudflare-challenges/challenge-types", + "/cloudflare-challenges/challenge-types/challenge-pages", + "/cloudflare-challenges/challenge-types/challenge-pages/additional-configuration", + "/cloudflare-challenges/challenge-types/challenge-pages/challenge-passage", + "/cloudflare-challenges/challenge-types/challenge-pages/create-custom-rule", + "/cloudflare-challenges/challenge-types/challenge-pages/detect-response", + "/cloudflare-challenges/challenge-types/challenge-pages/resolve-challenge", + "/cloudflare-challenges/challenge-types/javascript-detections", + "/cloudflare-challenges/challenge-types/turnstile", + "/cloudflare-challenges/concepts", + "/cloudflare-challenges/concepts/clearance", + "/cloudflare-challenges/concepts/how-challenges-work", + "/cloudflare-challenges/reference", + "/cloudflare-challenges/reference/challenge-solve-rate", + "/cloudflare-challenges/reference/private-access-tokens", + "/cloudflare-challenges/reference/supported-browsers", + "/cloudflare-challenges/reference/supported-languages", + "/cloudflare-challenges/troubleshooting", + "/cloudflare-challenges/troubleshooting/challenge-solve-issues", + "/cloudflare-for-platforms", + "/cloudflare-for-platforms/cloudflare-for-saas", + "/cloudflare-for-platforms/cloudflare-for-saas/api-reference", + "/cloudflare-for-platforms/cloudflare-for-saas/design-guide", + "/cloudflare-for-platforms/cloudflare-for-saas/domain-support", + "/cloudflare-for-platforms/cloudflare-for-saas/domain-support/create-custom-hostnames", + "/cloudflare-for-platforms/cloudflare-for-saas/domain-support/custom-metadata", + "/cloudflare-for-platforms/cloudflare-for-saas/domain-support/hostname-validation", + "/cloudflare-for-platforms/cloudflare-for-saas/domain-support/hostname-validation/backoff-schedule", + "/cloudflare-for-platforms/cloudflare-for-saas/domain-support/hostname-validation/error-codes", + "/cloudflare-for-platforms/cloudflare-for-saas/domain-support/hostname-validation/pre-validation", + "/cloudflare-for-platforms/cloudflare-for-saas/domain-support/hostname-validation/realtime-validation", + "/cloudflare-for-platforms/cloudflare-for-saas/domain-support/hostname-validation/validation-status", + "/cloudflare-for-platforms/cloudflare-for-saas/domain-support/migrating-custom-hostnames", + "/cloudflare-for-platforms/cloudflare-for-saas/domain-support/remove-custom-hostnames", + "/cloudflare-for-platforms/cloudflare-for-saas/hostname-analytics", + "/cloudflare-for-platforms/cloudflare-for-saas/performance", + "/cloudflare-for-platforms/cloudflare-for-saas/performance/argo-for-saas", + "/cloudflare-for-platforms/cloudflare-for-saas/performance/cache-for-saas", + "/cloudflare-for-platforms/cloudflare-for-saas/performance/early-hints-for-saas", + "/cloudflare-for-platforms/cloudflare-for-saas/plans", + "/cloudflare-for-platforms/cloudflare-for-saas/reference", + "/cloudflare-for-platforms/cloudflare-for-saas/reference/certificate-authorities", + "/cloudflare-for-platforms/cloudflare-for-saas/reference/certificate-statuses", + "/cloudflare-for-platforms/cloudflare-for-saas/reference/connection-details", + "/cloudflare-for-platforms/cloudflare-for-saas/reference/dcv-validation-backoff", + "/cloudflare-for-platforms/cloudflare-for-saas/reference/hostname-priority", + "/cloudflare-for-platforms/cloudflare-for-saas/reference/status-codes", + "/cloudflare-for-platforms/cloudflare-for-saas/reference/status-codes/custom-csrs", + "/cloudflare-for-platforms/cloudflare-for-saas/reference/status-codes/custom-hostnames", + "/cloudflare-for-platforms/cloudflare-for-saas/reference/token-validity-periods", + "/cloudflare-for-platforms/cloudflare-for-saas/reference/troubleshooting", + "/cloudflare-for-platforms/cloudflare-for-saas/reference/versioning", + "/cloudflare-for-platforms/cloudflare-for-saas/saas-customers", + "/cloudflare-for-platforms/cloudflare-for-saas/saas-customers/how-it-works", + "/cloudflare-for-platforms/cloudflare-for-saas/saas-customers/product-compatibility", + "/cloudflare-for-platforms/cloudflare-for-saas/saas-customers/provider-guides", + "/cloudflare-for-platforms/cloudflare-for-saas/saas-customers/provider-guides/bigcommerce", + "/cloudflare-for-platforms/cloudflare-for-saas/saas-customers/provider-guides/hubspot", + "/cloudflare-for-platforms/cloudflare-for-saas/saas-customers/provider-guides/kinsta", + "/cloudflare-for-platforms/cloudflare-for-saas/saas-customers/provider-guides/render", + "/cloudflare-for-platforms/cloudflare-for-saas/saas-customers/provider-guides/salesforce-commerce-cloud", + "/cloudflare-for-platforms/cloudflare-for-saas/saas-customers/provider-guides/shopify", + "/cloudflare-for-platforms/cloudflare-for-saas/saas-customers/provider-guides/webflow", + "/cloudflare-for-platforms/cloudflare-for-saas/saas-customers/provider-guides/wpengine", + "/cloudflare-for-platforms/cloudflare-for-saas/saas-customers/remove-domain", + "/cloudflare-for-platforms/cloudflare-for-saas/security", + "/cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management", + "/cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/certificate-statuses", + "/cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/custom-certificates", + "/cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/custom-certificates/certificate-signing-requests", + "/cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/custom-certificates/uploading-certificates", + "/cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/enforce-mtls", + "/cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/issue-and-validate", + "/cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/issue-and-validate/issue-certificates", + "/cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/issue-and-validate/renew-certificates", + "/cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/issue-and-validate/validate-certificates", + "/cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/issue-and-validate/validate-certificates/delegated-dcv", + "/cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/issue-and-validate/validate-certificates/http", + "/cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/issue-and-validate/validate-certificates/troubleshooting", + "/cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/issue-and-validate/validate-certificates/txt", + "/cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/webhook-definitions", + "/cloudflare-for-platforms/cloudflare-for-saas/security/secure-with-access", + "/cloudflare-for-platforms/cloudflare-for-saas/security/waf-for-saas", + "/cloudflare-for-platforms/cloudflare-for-saas/security/waf-for-saas/managed-rulesets", + "/cloudflare-for-platforms/cloudflare-for-saas/start", + "/cloudflare-for-platforms/cloudflare-for-saas/start/advanced-settings", + "/cloudflare-for-platforms/cloudflare-for-saas/start/advanced-settings/apex-proxying", + "/cloudflare-for-platforms/cloudflare-for-saas/start/advanced-settings/apex-proxying/setup", + "/cloudflare-for-platforms/cloudflare-for-saas/start/advanced-settings/custom-origin", + "/cloudflare-for-platforms/cloudflare-for-saas/start/advanced-settings/regional-services-for-saas", + "/cloudflare-for-platforms/cloudflare-for-saas/start/advanced-settings/worker-as-origin", + "/cloudflare-for-platforms/cloudflare-for-saas/start/common-api-calls", + "/cloudflare-for-platforms/cloudflare-for-saas/start/enable", + "/cloudflare-for-platforms/cloudflare-for-saas/start/getting-started", + "/cloudflare-for-platforms/workers-for-platforms", + "/cloudflare-for-platforms/workers-for-platforms/configuration", + "/cloudflare-for-platforms/workers-for-platforms/configuration/bindings", + "/cloudflare-for-platforms/workers-for-platforms/configuration/custom-limits", + "/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch", + "/cloudflare-for-platforms/workers-for-platforms/configuration/hostname-routing", + "/cloudflare-for-platforms/workers-for-platforms/configuration/observability", + "/cloudflare-for-platforms/workers-for-platforms/configuration/outbound-workers", + "/cloudflare-for-platforms/workers-for-platforms/configuration/static-assets", + "/cloudflare-for-platforms/workers-for-platforms/configuration/tags", + "/cloudflare-for-platforms/workers-for-platforms/get-started", + "/cloudflare-for-platforms/workers-for-platforms/how-workers-for-platforms-works", + "/cloudflare-for-platforms/workers-for-platforms/platform-templates", + "/cloudflare-for-platforms/workers-for-platforms/platform-templates/platform-starter-kit", + "/cloudflare-for-platforms/workers-for-platforms/platform-templates/vibesdk", + "/cloudflare-for-platforms/workers-for-platforms/reference", + "/cloudflare-for-platforms/workers-for-platforms/reference/limits", + "/cloudflare-for-platforms/workers-for-platforms/reference/local-development", + "/cloudflare-for-platforms/workers-for-platforms/reference/metadata", + "/cloudflare-for-platforms/workers-for-platforms/reference/platform-examples", + "/cloudflare-for-platforms/workers-for-platforms/reference/pricing", + "/cloudflare-for-platforms/workers-for-platforms/reference/worker-isolation", + "/cloudflare-for-platforms/workers-for-platforms/wfp-api", + "/cloudflare-network-firewall", + "/cloudflare-network-firewall/about", + "/cloudflare-network-firewall/about/analytics", + "/cloudflare-network-firewall/about/ids", + "/cloudflare-network-firewall/about/list-types", + "/cloudflare-network-firewall/about/protocol-validation-rules", + "/cloudflare-network-firewall/about/ruleset-logic", + "/cloudflare-network-firewall/about/traffic-types", + "/cloudflare-network-firewall/best-practices", + "/cloudflare-network-firewall/best-practices/extended-ruleset", + "/cloudflare-network-firewall/best-practices/magic-transit-egress", + "/cloudflare-network-firewall/best-practices/minimal-ruleset", + "/cloudflare-network-firewall/changelog", + "/cloudflare-network-firewall/how-to", + "/cloudflare-network-firewall/how-to/add-policies", + "/cloudflare-network-firewall/how-to/create-rate-limiting-policies", + "/cloudflare-network-firewall/how-to/enable-ids", + "/cloudflare-network-firewall/how-to/enable-managed-rulesets", + "/cloudflare-network-firewall/how-to/enable-roles", + "/cloudflare-network-firewall/how-to/filter-views", + "/cloudflare-network-firewall/how-to/form-expressions", + "/cloudflare-network-firewall/how-to/use-logpush-with-ids", + "/cloudflare-network-firewall/how-to/use-rules-list", + "/cloudflare-network-firewall/packet-captures", + "/cloudflare-network-firewall/packet-captures/collect-pcaps", + "/cloudflare-network-firewall/packet-captures/pcaps-bucket-setup", + "/cloudflare-network-firewall/plans", + "/cloudflare-network-firewall/reference", + "/cloudflare-network-firewall/reference/network-firewall-fields", + "/cloudflare-network-firewall/reference/network-firewall-functions", + "/cloudflare-network-firewall/troubleshooting", + "/cloudflare-network-firewall/troubleshooting/diagnose-traffic-decisions", + "/cloudflare-network-firewall/tutorials", + "/cloudflare-network-firewall/tutorials/graphql-analytics", + "/cloudflare-one", + "/cloudflare-one/access-controls", + "/cloudflare-one/access-controls/access-settings", + "/cloudflare-one/access-controls/access-settings/app-launcher", + "/cloudflare-one/access-controls/access-settings/independent-mfa", + "/cloudflare-one/access-controls/access-settings/require-access-protection", + "/cloudflare-one/access-controls/access-settings/session-management", + "/cloudflare-one/access-controls/ai-controls", + "/cloudflare-one/access-controls/ai-controls/linked-apps", + "/cloudflare-one/access-controls/ai-controls/mcp-portals", + "/cloudflare-one/access-controls/ai-controls/secure-mcp-servers", + "/cloudflare-one/access-controls/applications", + "/cloudflare-one/access-controls/applications/bookmarks", + "/cloudflare-one/access-controls/applications/choose-application-type", + "/cloudflare-one/access-controls/applications/http-apps", + "/cloudflare-one/access-controls/applications/http-apps/authorization-cookie", + "/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/application-token", + "/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/cors", + "/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/validating-json", + "/cloudflare-one/access-controls/applications/http-apps/managed-oauth", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/adobe-sign-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/area-1", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/asana-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/atlassian-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/aws-sso-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/braintree-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/coupa-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/digicert-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/docusign-access", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/dropbox-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/generic-oidc-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/generic-saml-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/github-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/google-cloud-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/google-workspace-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/grafana-cloud-saas-oidc", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/grafana-saas-oidc", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/greenhouse-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/hubspot-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/ironclad-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/jamf-pro-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/miro-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/pagerduty-saml-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/pingboard-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/salesforce-saas-oidc", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/salesforce-saas-saml", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/servicenow-saas-oidc", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/servicenow-saas-saml", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/slack-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/smartsheet-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/sparkpost-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/tableau-saml-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/workday-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/zendesk-sso-saas", + "/cloudflare-one/access-controls/applications/http-apps/saas-apps/zoom-saas", + "/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app", + "/cloudflare-one/access-controls/applications/linked-app-token", + "/cloudflare-one/access-controls/applications/non-http", + "/cloudflare-one/access-controls/applications/non-http/browser-rendering", + "/cloudflare-one/access-controls/applications/non-http/cloudflared-authentication", + "/cloudflare-one/access-controls/applications/non-http/cloudflared-authentication/arbitrary-tcp", + "/cloudflare-one/access-controls/applications/non-http/cloudflared-authentication/automatic-cloudflared-authentication", + "/cloudflare-one/access-controls/applications/non-http/infrastructure-apps", + "/cloudflare-one/access-controls/applications/non-http/legacy-private-network-app", + "/cloudflare-one/access-controls/applications/non-http/self-hosted-private-app", + "/cloudflare-one/access-controls/applications/non-http/short-lived-certificates-legacy", + "/cloudflare-one/access-controls/authenticate-agents", + "/cloudflare-one/access-controls/event-subscriptions", + "/cloudflare-one/access-controls/policies", + "/cloudflare-one/access-controls/policies/app-paths", + "/cloudflare-one/access-controls/policies/common-policies", + "/cloudflare-one/access-controls/policies/external-evaluation", + "/cloudflare-one/access-controls/policies/groups", + "/cloudflare-one/access-controls/policies/isolate-application", + "/cloudflare-one/access-controls/policies/mfa-requirements", + "/cloudflare-one/access-controls/policies/policy-management", + "/cloudflare-one/access-controls/policies/require-purpose-justification", + "/cloudflare-one/access-controls/policies/temporary-auth", + "/cloudflare-one/access-controls/service-credentials", + "/cloudflare-one/access-controls/service-credentials/mutual-tls-authentication", + "/cloudflare-one/access-controls/service-credentials/service-tokens", + "/cloudflare-one/access-controls/troubleshooting", + "/cloudflare-one/account-limits", + "/cloudflare-one/api-terraform", + "/cloudflare-one/changelog", + "/cloudflare-one/changelog/access", + "/cloudflare-one/changelog/browser-isolation", + "/cloudflare-one/changelog/casb", + "/cloudflare-one/changelog/cloudflare-network-firewall", + "/cloudflare-one/changelog/cloudflare-one-client", + "/cloudflare-one/changelog/dex", + "/cloudflare-one/changelog/dlp", + "/cloudflare-one/changelog/email-security", + "/cloudflare-one/changelog/gateway", + "/cloudflare-one/changelog/risk-score", + "/cloudflare-one/changelog/tunnel", + "/cloudflare-one/cloud-and-saas-findings", + "/cloudflare-one/cloud-and-saas-findings/casb-dlp", + "/cloudflare-one/cloud-and-saas-findings/manage-findings", + "/cloudflare-one/cloud-and-saas-findings/troubleshoot-casb", + "/cloudflare-one/data-loss-prevention", + "/cloudflare-one/data-loss-prevention/ai-gateway", + "/cloudflare-one/data-loss-prevention/data-classification", + "/cloudflare-one/data-loss-prevention/data-classification/build-a-data-class", + "/cloudflare-one/data-loss-prevention/data-classification/configure-labels-and-templates", + "/cloudflare-one/data-loss-prevention/detection-entries", + "/cloudflare-one/data-loss-prevention/detection-entries/configure-detection-entries", + "/cloudflare-one/data-loss-prevention/detection-entries/predefined-detection-entries", + "/cloudflare-one/data-loss-prevention/dlp-policies", + "/cloudflare-one/data-loss-prevention/dlp-policies/common-policies", + "/cloudflare-one/data-loss-prevention/dlp-policies/logging-options", + "/cloudflare-one/data-loss-prevention/dlp-profiles", + "/cloudflare-one/data-loss-prevention/dlp-profiles/advanced-settings", + "/cloudflare-one/data-loss-prevention/dlp-profiles/integration-profiles", + "/cloudflare-one/data-loss-prevention/dlp-profiles/predefined-profiles", + "/cloudflare-one/data-loss-prevention/dlp-settings", + "/cloudflare-one/data-loss-prevention/saas-apps", + "/cloudflare-one/data-loss-prevention/troubleshoot-dlp", + "/cloudflare-one/email-security", + "/cloudflare-one/email-security/directories", + "/cloudflare-one/email-security/directories/manage-es-directories", + "/cloudflare-one/email-security/directories/manage-integrated-directories", + "/cloudflare-one/email-security/directories/manage-integrated-directories/manage-groups-directory", + "/cloudflare-one/email-security/directories/manage-integrated-directories/manage-users-directory", + "/cloudflare-one/email-security/email-security-api-docs", + "/cloudflare-one/email-security/investigation", + "/cloudflare-one/email-security/investigation/search-email", + "/cloudflare-one/email-security/monitoring", + "/cloudflare-one/email-security/monitoring/download-report", + "/cloudflare-one/email-security/outbound-dlp", + "/cloudflare-one/email-security/phishguard", + "/cloudflare-one/email-security/reference", + "/cloudflare-one/email-security/reference/dispositions-and-attributes", + "/cloudflare-one/email-security/reference/how-es-detects-phish", + "/cloudflare-one/email-security/reference/regional-processing", + "/cloudflare-one/email-security/retro-scan", + "/cloudflare-one/email-security/settings", + "/cloudflare-one/email-security/settings/auto-moves", + "/cloudflare-one/email-security/settings/detection-settings", + "/cloudflare-one/email-security/settings/detection-settings/additional-detections", + "/cloudflare-one/email-security/settings/detection-settings/allow-policies", + "/cloudflare-one/email-security/settings/detection-settings/best-practices", + "/cloudflare-one/email-security/settings/detection-settings/blocked-senders", + "/cloudflare-one/email-security/settings/detection-settings/configure-link-actions", + "/cloudflare-one/email-security/settings/detection-settings/configure-text-add-ons", + "/cloudflare-one/email-security/settings/detection-settings/impersonation-registry", + "/cloudflare-one/email-security/settings/detection-settings/trusted-domains", + "/cloudflare-one/email-security/settings/domain-management", + "/cloudflare-one/email-security/settings/domain-management/domain", + "/cloudflare-one/email-security/settings/phish-submissions", + "/cloudflare-one/email-security/settings/phish-submissions/phishnet-365", + "/cloudflare-one/email-security/settings/phish-submissions/phishnet-google-workspace", + "/cloudflare-one/email-security/settings/phish-submissions/submission-addresses", + "/cloudflare-one/email-security/setup", + "/cloudflare-one/email-security/setup/manage-domains", + "/cloudflare-one/email-security/setup/post-delivery-deployment", + "/cloudflare-one/email-security/setup/post-delivery-deployment/api", + "/cloudflare-one/email-security/setup/post-delivery-deployment/api/m365-api", + "/cloudflare-one/email-security/setup/post-delivery-deployment/bcc-journaling", + "/cloudflare-one/email-security/setup/post-delivery-deployment/bcc-journaling/bcc-setup", + "/cloudflare-one/email-security/setup/post-delivery-deployment/bcc-journaling/bcc-setup/bcc-microsoft-exchange", + "/cloudflare-one/email-security/setup/post-delivery-deployment/bcc-journaling/bcc-setup/gmail-bcc-setup", + "/cloudflare-one/email-security/setup/post-delivery-deployment/bcc-journaling/bcc-setup/gmail-bcc-setup/add-bcc-rules", + "/cloudflare-one/email-security/setup/post-delivery-deployment/bcc-journaling/bcc-setup/gmail-bcc-setup/connect-domains", + "/cloudflare-one/email-security/setup/post-delivery-deployment/bcc-journaling/bcc-setup/gmail-bcc-setup/enable-auto-moves", + "/cloudflare-one/email-security/setup/post-delivery-deployment/bcc-journaling/bcc-setup/gmail-bcc-setup/enable-gmail-integration", + "/cloudflare-one/email-security/setup/post-delivery-deployment/bcc-journaling/bcc-setup/gmail-bcc-setup/gmail-bcc-setup", + "/cloudflare-one/email-security/setup/post-delivery-deployment/bcc-journaling/journaling-setup", + "/cloudflare-one/email-security/setup/post-delivery-deployment/bcc-journaling/journaling-setup/m365-journaling", + "/cloudflare-one/email-security/setup/post-delivery-deployment/bcc-journaling/journaling-setup/manual-add", + "/cloudflare-one/email-security/setup/pre-delivery-deployment", + "/cloudflare-one/email-security/setup/pre-delivery-deployment/egress-ips", + "/cloudflare-one/email-security/setup/pre-delivery-deployment/mx-inline-deployment", + "/cloudflare-one/email-security/setup/pre-delivery-deployment/mx-inline-deployment-setup", + "/cloudflare-one/email-security/setup/pre-delivery-deployment/partner-domain-tls", + "/cloudflare-one/email-security/setup/pre-delivery-deployment/prerequisites", + "/cloudflare-one/email-security/setup/pre-delivery-deployment/prerequisites/cisco-email-security-mx", + "/cloudflare-one/email-security/setup/pre-delivery-deployment/prerequisites/cisco-mx", + "/cloudflare-one/email-security/setup/pre-delivery-deployment/prerequisites/gsuite-email-security-mx", + "/cloudflare-one/email-security/setup/pre-delivery-deployment/prerequisites/m365-email-security-mx", + "/cloudflare-one/email-security/setup/pre-delivery-deployment/prerequisites/m365-email-security-mx/use-cases", + "/cloudflare-one/email-security/setup/pre-delivery-deployment/prerequisites/m365-email-security-mx/use-cases/five-junk-admin-quarantine", + "/cloudflare-one/email-security/setup/pre-delivery-deployment/prerequisites/m365-email-security-mx/use-cases/four-user-quarantine-admin-quarantine", + "/cloudflare-one/email-security/setup/pre-delivery-deployment/prerequisites/m365-email-security-mx/use-cases/one-junk-admin-quarantine", + "/cloudflare-one/email-security/setup/pre-delivery-deployment/prerequisites/m365-email-security-mx/use-cases/three-junk-admin-quarantine", + "/cloudflare-one/email-security/setup/pre-delivery-deployment/prerequisites/m365-email-security-mx/use-cases/two-junk-user-quarantine", + "/cloudflare-one/email-security/submissions", + "/cloudflare-one/email-security/submissions/invalid-submissions", + "/cloudflare-one/email-security/submissions/team-submissions", + "/cloudflare-one/email-security/submissions/user-submissions", + "/cloudflare-one/email-security/troubleshooting", + "/cloudflare-one/faq", + "/cloudflare-one/faq/authentication-faq", + "/cloudflare-one/faq/cloudflare-tunnels-faq", + "/cloudflare-one/faq/devices-faq", + "/cloudflare-one/faq/general-faq", + "/cloudflare-one/faq/getting-started-faq", + "/cloudflare-one/faq/policies-faq", + "/cloudflare-one/glossary", + "/cloudflare-one/implementation-guides", + "/cloudflare-one/implementation-guides/clientless-access", + "/cloudflare-one/implementation-guides/holistic-ai-security", + "/cloudflare-one/implementation-guides/replace-vpn", + "/cloudflare-one/implementation-guides/secure-internet-traffic", + "/cloudflare-one/implementation-guides/secure-your-email", + "/cloudflare-one/insights", + "/cloudflare-one/insights/analytics", + "/cloudflare-one/insights/analytics-overview", + "/cloudflare-one/insights/analytics/access", + "/cloudflare-one/insights/analytics/ai-prompt-logs", + "/cloudflare-one/insights/analytics/ai-security", + "/cloudflare-one/insights/analytics/application-access", + "/cloudflare-one/insights/analytics/data-analytics", + "/cloudflare-one/insights/analytics/gateway", + "/cloudflare-one/insights/analytics/network-sessions", + "/cloudflare-one/insights/analytics/shadow-it-discovery", + "/cloudflare-one/insights/dex", + "/cloudflare-one/insights/dex/dex-mcp-server", + "/cloudflare-one/insights/dex/diagnostics", + "/cloudflare-one/insights/dex/diagnostics/client-packet-capture", + "/cloudflare-one/insights/dex/diagnostics/speed-test", + "/cloudflare-one/insights/dex/ip-visibility", + "/cloudflare-one/insights/dex/mcp-server", + "/cloudflare-one/insights/dex/monitoring", + "/cloudflare-one/insights/dex/notifications", + "/cloudflare-one/insights/dex/rules", + "/cloudflare-one/insights/dex/tests", + "/cloudflare-one/insights/dex/tests/http", + "/cloudflare-one/insights/dex/tests/traceroute", + "/cloudflare-one/insights/dex/tests/view-results", + "/cloudflare-one/insights/dex/troubleshooting", + "/cloudflare-one/insights/logs", + "/cloudflare-one/insights/logs/dashboard-logs", + "/cloudflare-one/insights/logs/dashboard-logs/access-authentication-logs", + "/cloudflare-one/insights/logs/dashboard-logs/admin-activity-logs", + "/cloudflare-one/insights/logs/dashboard-logs/gateway-logs", + "/cloudflare-one/insights/logs/dashboard-logs/gateway-logs/manage-pii", + "/cloudflare-one/insights/logs/dashboard-logs/posture-logs", + "/cloudflare-one/insights/logs/dashboard-logs/scim-logs", + "/cloudflare-one/insights/logs/dashboard-logs/ssh-command-logs", + "/cloudflare-one/insights/logs/dashboard-logs/tunnel-audit-logs", + "/cloudflare-one/insights/logs/logpush", + "/cloudflare-one/insights/logs/logpush/email-security-logs", + "/cloudflare-one/insights/logs/logpush/ids-logs", + "/cloudflare-one/insights/logs/logpush/network-firewall-log-filters", + "/cloudflare-one/insights/network-visibility", + "/cloudflare-one/insights/network-visibility/diagnostics", + "/cloudflare-one/insights/network-visibility/diagnostics/buckets", + "/cloudflare-one/insights/network-visibility/diagnostics/packet-captures", + "/cloudflare-one/integrations", + "/cloudflare-one/integrations/cloud-and-saas", + "/cloudflare-one/integrations/cloud-and-saas/anthropic", + "/cloudflare-one/integrations/cloud-and-saas/atlassian-confluence", + "/cloudflare-one/integrations/cloud-and-saas/atlassian-jira", + "/cloudflare-one/integrations/cloud-and-saas/aws-s3", + "/cloudflare-one/integrations/cloud-and-saas/bitbucket-cloud", + "/cloudflare-one/integrations/cloud-and-saas/box", + "/cloudflare-one/integrations/cloud-and-saas/dropbox", + "/cloudflare-one/integrations/cloud-and-saas/findings", + "/cloudflare-one/integrations/cloud-and-saas/gcp-cloud-storage", + "/cloudflare-one/integrations/cloud-and-saas/github", + "/cloudflare-one/integrations/cloud-and-saas/google-workspace", + "/cloudflare-one/integrations/cloud-and-saas/google-workspace/gemini", + "/cloudflare-one/integrations/cloud-and-saas/google-workspace/gmail", + "/cloudflare-one/integrations/cloud-and-saas/google-workspace/gmail-fedramp", + "/cloudflare-one/integrations/cloud-and-saas/google-workspace/google-admin", + "/cloudflare-one/integrations/cloud-and-saas/google-workspace/google-admin-fedramp", + "/cloudflare-one/integrations/cloud-and-saas/google-workspace/google-calendar", + "/cloudflare-one/integrations/cloud-and-saas/google-workspace/google-calendar-fedramp", + "/cloudflare-one/integrations/cloud-and-saas/google-workspace/google-drive", + "/cloudflare-one/integrations/cloud-and-saas/google-workspace/google-drive-fedramp", + "/cloudflare-one/integrations/cloud-and-saas/microsoft-365", + "/cloudflare-one/integrations/cloud-and-saas/microsoft-365/admin-center", + "/cloudflare-one/integrations/cloud-and-saas/microsoft-365/admin-center-fedramp", + "/cloudflare-one/integrations/cloud-and-saas/microsoft-365/m365-copilot", + "/cloudflare-one/integrations/cloud-and-saas/microsoft-365/m365-copilot-fedramp", + "/cloudflare-one/integrations/cloud-and-saas/microsoft-365/onedrive", + "/cloudflare-one/integrations/cloud-and-saas/microsoft-365/onedrive-fedramp", + "/cloudflare-one/integrations/cloud-and-saas/microsoft-365/outlook", + "/cloudflare-one/integrations/cloud-and-saas/microsoft-365/outlook-fedramp", + "/cloudflare-one/integrations/cloud-and-saas/microsoft-365/sharepoint", + "/cloudflare-one/integrations/cloud-and-saas/microsoft-365/sharepoint-fedramp", + "/cloudflare-one/integrations/cloud-and-saas/openai", + "/cloudflare-one/integrations/cloud-and-saas/salesforce", + "/cloudflare-one/integrations/cloud-and-saas/salesforce-fedramp", + "/cloudflare-one/integrations/cloud-and-saas/servicenow", + "/cloudflare-one/integrations/cloud-and-saas/servicenow-fedramp", + "/cloudflare-one/integrations/cloud-and-saas/slack", + "/cloudflare-one/integrations/cloud-and-saas/troubleshooting", + "/cloudflare-one/integrations/cloud-and-saas/troubleshooting/casb", + "/cloudflare-one/integrations/cloud-and-saas/troubleshooting/troubleshoot-compute-accounts", + "/cloudflare-one/integrations/cloud-and-saas/troubleshooting/troubleshoot-integrations", + "/cloudflare-one/integrations/cloud-and-saas/webhooks", + "/cloudflare-one/integrations/identity-providers", + "/cloudflare-one/integrations/identity-providers/adfs", + "/cloudflare-one/integrations/identity-providers/aws-saml", + "/cloudflare-one/integrations/identity-providers/awscognito-oidc", + "/cloudflare-one/integrations/identity-providers/centrify", + "/cloudflare-one/integrations/identity-providers/centrify-saml", + "/cloudflare-one/integrations/identity-providers/citrixadc-saml", + "/cloudflare-one/integrations/identity-providers/cloudflare", + "/cloudflare-one/integrations/identity-providers/entra-id", + "/cloudflare-one/integrations/identity-providers/facebook-login", + "/cloudflare-one/integrations/identity-providers/generic-oidc", + "/cloudflare-one/integrations/identity-providers/generic-saml", + "/cloudflare-one/integrations/identity-providers/github", + "/cloudflare-one/integrations/identity-providers/google", + "/cloudflare-one/integrations/identity-providers/google-workspace", + "/cloudflare-one/integrations/identity-providers/idp-federation", + "/cloudflare-one/integrations/identity-providers/jumpcloud-saml", + "/cloudflare-one/integrations/identity-providers/keycloak", + "/cloudflare-one/integrations/identity-providers/linkedin", + "/cloudflare-one/integrations/identity-providers/okta", + "/cloudflare-one/integrations/identity-providers/okta-saml", + "/cloudflare-one/integrations/identity-providers/one-time-pin", + "/cloudflare-one/integrations/identity-providers/onelogin-oidc", + "/cloudflare-one/integrations/identity-providers/onelogin-saml", + "/cloudflare-one/integrations/identity-providers/pingfederate-saml", + "/cloudflare-one/integrations/identity-providers/pingone-oidc", + "/cloudflare-one/integrations/identity-providers/pingone-saml", + "/cloudflare-one/integrations/identity-providers/signed_authn", + "/cloudflare-one/integrations/identity-providers/yandex", + "/cloudflare-one/integrations/service-providers", + "/cloudflare-one/integrations/service-providers/crowdstrike", + "/cloudflare-one/integrations/service-providers/custom", + "/cloudflare-one/integrations/service-providers/kolide", + "/cloudflare-one/integrations/service-providers/microsoft", + "/cloudflare-one/integrations/service-providers/sentinelone", + "/cloudflare-one/integrations/service-providers/taniums2s", + "/cloudflare-one/integrations/service-providers/uptycs", + "/cloudflare-one/integrations/service-providers/workspace-one", + "/cloudflare-one/networks", + "/cloudflare-one/networks/connectivity-options", + "/cloudflare-one/networks/connectors", + "/cloudflare-one/networks/connectors/cloudflare-mesh", + "/cloudflare-one/networks/connectors/cloudflare-mesh/client-devices", + "/cloudflare-one/networks/connectors/cloudflare-mesh/get-started", + "/cloudflare-one/networks/connectors/cloudflare-mesh/high-availability", + "/cloudflare-one/networks/connectors/cloudflare-mesh/routes", + "/cloudflare-one/networks/connectors/cloudflare-mesh/tips", + "/cloudflare-one/networks/connectors/cloudflare-tunnel", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/configure-tunnels", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/configure-tunnels/cipher-suites", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/configure-tunnels/origin-parameters", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/configure-tunnels/remote-tunnel-permissions", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/configure-tunnels/run-parameters", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/configure-tunnels/tunnel-availability", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/configure-tunnels/tunnel-availability/deploy-replicas", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/configure-tunnels/tunnel-availability/system-requirements", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/configure-tunnels/tunnel-with-firewall", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/deployment-guides", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/deployment-guides/ansible", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/deployment-guides/aws", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/deployment-guides/azure", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/deployment-guides/google-cloud-platform", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/deployment-guides/kubernetes", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/deployment-guides/terraform", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/local-management", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/local-management/as-a-service", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/local-management/as-a-service/linux", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/local-management/as-a-service/macos", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/local-management/as-a-service/windows", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/local-management/configuration-file", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/local-management/create-local-tunnel", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/local-management/local-tunnel-terms", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/local-management/tunnel-permissions", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/local-management/tunnel-useful-commands", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/copyrights", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/license", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/update-cloudflared", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/create-remote-tunnel", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/create-remote-tunnel-api", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/tunnel-useful-terms", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/monitor-tunnels", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/monitor-tunnels/logs", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/monitor-tunnels/metrics", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/monitor-tunnels/notifications", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/private-net", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/private-net/cloudflared", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/private-net/cloudflared/connect-cidr", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/private-net/cloudflared/connect-private-hostname", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/private-net/cloudflared/private-dns", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/private-net/cloudflared/tunnel-virtual-networks", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/routing-to-tunnel", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/routing-to-tunnel/dns", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/routing-to-tunnel/protocols", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/routing-to-tunnel/public-load-balancers", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/troubleshoot-tunnels", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/troubleshoot-tunnels/common-errors", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/troubleshoot-tunnels/connectivity-prechecks", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/troubleshoot-tunnels/diag-logs", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/troubleshoot-tunnels/private-networks", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/use-cases", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/use-cases/grpc", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/use-cases/rdp", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/use-cases/rdp/rdp-browser", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/use-cases/rdp/rdp-cloudflared-authentication", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/use-cases/rdp/rdp-device-client", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/use-cases/smb", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/use-cases/ssh", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/use-cases/ssh/ssh-browser-rendering", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/use-cases/ssh/ssh-cloudflared-authentication", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/use-cases/ssh/ssh-device-client", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/use-cases/ssh/ssh-infrastructure-access", + "/cloudflare-one/networks/connectors/cloudflare-tunnel/use-cases/vnc-browser-rendering", + "/cloudflare-one/networks/connectors/cloudflare-wan", + "/cloudflare-one/networks/connectors/cloudflare-wan/analytics", + "/cloudflare-one/networks/connectors/cloudflare-wan/analytics/netflow-analytics", + "/cloudflare-one/networks/connectors/cloudflare-wan/analytics/network-analytics", + "/cloudflare-one/networks/connectors/cloudflare-wan/analytics/packet-captures", + "/cloudflare-one/networks/connectors/cloudflare-wan/analytics/query-bandwidth", + "/cloudflare-one/networks/connectors/cloudflare-wan/analytics/query-tunnel-health", + "/cloudflare-one/networks/connectors/cloudflare-wan/analytics/site-analytics", + "/cloudflare-one/networks/connectors/cloudflare-wan/analytics/traceroutes", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/configure-hardware-appliance", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/configure-hardware-appliance/sfp-port-information", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/configure-virtual-appliance", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/device-metrics", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/maintenance", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/maintenance/activate-appliance", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/maintenance/deactivate-appliance", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/maintenance/default-password", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/maintenance/edit-basic-info", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/maintenance/edit-network-settings", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/maintenance/edit-sites", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/maintenance/edit-traffic-steering-settings", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/maintenance/heartbeat", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/maintenance/interrupt-service-window", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/maintenance/register-appliance", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/maintenance/remove-appliances", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/network-options", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/network-options/application-based-policies", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/network-options/application-based-policies/breakout-traffic", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/network-options/application-based-policies/prioritized-traffic", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/network-options/dhcp", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/network-options/dhcp/dhcp-relay", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/network-options/dhcp/dhcp-server", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/network-options/dhcp/dhcp-static-address-reservation", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/network-options/link-aggregation", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/network-options/nat-subnet", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/network-options/network-segmentation", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/network-options/routed-subnets", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/reference", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/troubleshooting", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/common-settings", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/common-settings/check-tunnel-health-dashboard", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/common-settings/configure-tunnel-health-alerts", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/common-settings/custom-ike-id-ipsec", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/common-settings/enable-roles", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/common-settings/sites", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/common-settings/update-tunnel-health-checks-frequency", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/how-to", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/how-to/configure-cloudflare-source-ips", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/how-to/configure-routes", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/how-to/configure-tunnel-endpoints", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/how-to/traceroute", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/alibaba-cloud", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/aruba-edgeconnect", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/aws", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/azure", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/azure/azure-virtual-wan", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/azure/azure-vpn-gateway", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/cisco-ios-xe", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/cisco-meraki-static", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/fitelnet", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/fortinet", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/google", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/juniper", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/oracle", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/palo-alto", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/pfsense", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/sonicwall", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/sophos-firewall", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/strongswan", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/ubiquiti", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/velocloud", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/viptela", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/vyos", + "/cloudflare-one/networks/connectors/cloudflare-wan/configuration/third-party/yamaha", + "/cloudflare-one/networks/connectors/cloudflare-wan/get-started", + "/cloudflare-one/networks/connectors/cloudflare-wan/legal", + "/cloudflare-one/networks/connectors/cloudflare-wan/legal/3rdparty", + "/cloudflare-one/networks/connectors/cloudflare-wan/load-balancing", + "/cloudflare-one/networks/connectors/cloudflare-wan/network-interconnect", + "/cloudflare-one/networks/connectors/cloudflare-wan/on-ramps", + "/cloudflare-one/networks/connectors/cloudflare-wan/private-origin-via-cloudflare-wan", + "/cloudflare-one/networks/connectors/cloudflare-wan/reference", + "/cloudflare-one/networks/connectors/cloudflare-wan/reference/anti-replay-protection", + "/cloudflare-one/networks/connectors/cloudflare-wan/reference/bandwidth-measurement", + "/cloudflare-one/networks/connectors/cloudflare-wan/reference/device-compatibility", + "/cloudflare-one/networks/connectors/cloudflare-wan/reference/gre-ipsec-tunnels", + "/cloudflare-one/networks/connectors/cloudflare-wan/reference/how-cloudflare-calculates-tunnel-health-alerts", + "/cloudflare-one/networks/connectors/cloudflare-wan/reference/mtu-mss", + "/cloudflare-one/networks/connectors/cloudflare-wan/reference/traffic-steering", + "/cloudflare-one/networks/connectors/cloudflare-wan/reference/tunnel-health-checks", + "/cloudflare-one/networks/connectors/cloudflare-wan/security", + "/cloudflare-one/networks/connectors/cloudflare-wan/troubleshooting", + "/cloudflare-one/networks/connectors/cloudflare-wan/troubleshooting/connectivity", + "/cloudflare-one/networks/connectors/cloudflare-wan/troubleshooting/ipsec-troubleshoot", + "/cloudflare-one/networks/connectors/cloudflare-wan/troubleshooting/routing-and-bgp", + "/cloudflare-one/networks/connectors/cloudflare-wan/troubleshooting/tunnel-health", + "/cloudflare-one/networks/connectors/cloudflare-wan/wan-transformation", + "/cloudflare-one/networks/connectors/cloudflare-wan/zero-trust", + "/cloudflare-one/networks/connectors/cloudflare-wan/zero-trust/cloudflare-gateway", + "/cloudflare-one/networks/connectors/cloudflare-wan/zero-trust/cloudflare-one-client", + "/cloudflare-one/networks/connectors/cloudflare-wan/zero-trust/cloudflare-tunnel", + "/cloudflare-one/networks/connectors/cloudflare-wan/zero-trust/security-services", + "/cloudflare-one/networks/connectors/granular-permissions", + "/cloudflare-one/networks/resolvers-and-proxies", + "/cloudflare-one/networks/resolvers-and-proxies/dns", + "/cloudflare-one/networks/resolvers-and-proxies/dns/dns-over-https", + "/cloudflare-one/networks/resolvers-and-proxies/dns/dns-over-tls", + "/cloudflare-one/networks/resolvers-and-proxies/dns/locations", + "/cloudflare-one/networks/resolvers-and-proxies/dns/locations/dns-resolver-ips", + "/cloudflare-one/networks/resolvers-and-proxies/proxy-endpoints", + "/cloudflare-one/networks/resolvers-and-proxies/proxy-endpoints/best-practices", + "/cloudflare-one/networks/resolvers-and-proxies/proxy-endpoints/configure-pac-file-on-device", + "/cloudflare-one/networks/routes", + "/cloudflare-one/networks/routes/add-routes", + "/cloudflare-one/networks/routes/reserved-ips", + "/cloudflare-one/networks/virtual-networks", + "/cloudflare-one/reference-architecture", + "/cloudflare-one/remote-browser-isolation", + "/cloudflare-one/remote-browser-isolation/accessibility", + "/cloudflare-one/remote-browser-isolation/canvas-remoting", + "/cloudflare-one/remote-browser-isolation/extensions", + "/cloudflare-one/remote-browser-isolation/isolation-policies", + "/cloudflare-one/remote-browser-isolation/known-limitations", + "/cloudflare-one/remote-browser-isolation/network-dependencies", + "/cloudflare-one/remote-browser-isolation/setup", + "/cloudflare-one/remote-browser-isolation/setup/clientless-browser-isolation", + "/cloudflare-one/remote-browser-isolation/setup/non-identity", + "/cloudflare-one/remote-browser-isolation/troubleshooting", + "/cloudflare-one/reusable-components", + "/cloudflare-one/reusable-components/custom-pages", + "/cloudflare-one/reusable-components/custom-pages/access-block-page", + "/cloudflare-one/reusable-components/custom-pages/access-login-page", + "/cloudflare-one/reusable-components/custom-pages/app-launcher-customization", + "/cloudflare-one/reusable-components/custom-pages/gateway-block-page", + "/cloudflare-one/reusable-components/lists", + "/cloudflare-one/reusable-components/packet-filtering-fields", + "/cloudflare-one/reusable-components/posture-checks", + "/cloudflare-one/reusable-components/posture-checks/access-integrations", + "/cloudflare-one/reusable-components/posture-checks/client-checks", + "/cloudflare-one/reusable-components/posture-checks/client-checks/antivirus", + "/cloudflare-one/reusable-components/posture-checks/client-checks/application-check", + "/cloudflare-one/reusable-components/posture-checks/client-checks/carbon-black", + "/cloudflare-one/reusable-components/posture-checks/client-checks/client-certificate", + "/cloudflare-one/reusable-components/posture-checks/client-checks/corp-device", + "/cloudflare-one/reusable-components/posture-checks/client-checks/device-uuid", + "/cloudflare-one/reusable-components/posture-checks/client-checks/disk-encryption", + "/cloudflare-one/reusable-components/posture-checks/client-checks/domain-joined", + "/cloudflare-one/reusable-components/posture-checks/client-checks/file-check", + "/cloudflare-one/reusable-components/posture-checks/client-checks/firewall", + "/cloudflare-one/reusable-components/posture-checks/client-checks/os-version", + "/cloudflare-one/reusable-components/posture-checks/client-checks/require-gateway", + "/cloudflare-one/reusable-components/posture-checks/client-checks/require-warp", + "/cloudflare-one/reusable-components/posture-checks/client-checks/sentinel-one", + "/cloudflare-one/reusable-components/posture-checks/client-checks/tanium", + "/cloudflare-one/reusable-components/posture-checks/service-to-service", + "/cloudflare-one/reusable-components/tags", + "/cloudflare-one/reusable-components/use-rules-list", + "/cloudflare-one/roles-permissions", + "/cloudflare-one/setup", + "/cloudflare-one/setup/replace-vpn", + "/cloudflare-one/setup/replace-vpn/device-to-device", + "/cloudflare-one/setup/replace-vpn/device-to-network", + "/cloudflare-one/setup/replace-vpn/network-to-network", + "/cloudflare-one/setup/secure-private-apps", + "/cloudflare-one/setup/secure-private-apps/clientless-ssh", + "/cloudflare-one/setup/secure-private-apps/in-browser-rdp", + "/cloudflare-one/setup/secure-private-apps/private-web-app", + "/cloudflare-one/team-and-resources", + "/cloudflare-one/team-and-resources/app-library", + "/cloudflare-one/team-and-resources/devices", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/business-continuity", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure/client-sessions", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure/device-ips", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure/device-profiles", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure/managed-networks", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure/modes", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure/modes/device-information-only", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure/route-traffic", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure/route-traffic/client-architecture", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure/route-traffic/local-domains", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure/route-traffic/split-tunnels", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure/settings", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure/settings/captive-portals", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure/settings/emergency-disconnect", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment/device-enrollment", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment/firewall", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment/manual-deployment", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment/mdm-deployment", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment/mdm-deployment/hardware-backed-registration", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment/mdm-deployment/parameters", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment/mdm-deployment/partners", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment/mdm-deployment/partners/fleet", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment/mdm-deployment/partners/hexnode", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment/mdm-deployment/partners/intune", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment/mdm-deployment/partners/jamf", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment/mdm-deployment/partners/jumpcloud", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment/mdm-deployment/partners/kandji", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment/mdm-deployment/path-mtu-discovery", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment/mdm-deployment/protocol-handler", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment/mdm-deployment/switch-organizations", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment/mdm-deployment/windows-multiuser", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment/mdm-deployment/windows-no-auth-no-internet", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment/mdm-deployment/windows-prelogin", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment/vpn", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/download", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/download/beta-releases", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/download/cloudflare-one-agent-migration", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/download/lts-releases", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/download/support-lifecycle", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/download/update", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/set-up", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/troubleshooting", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/troubleshooting/client-errors", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/troubleshooting/common-issues", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/troubleshooting/connectivity-status", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/troubleshooting/diagnostic-logs", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/troubleshooting/known-limitations", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/troubleshooting/troubleshooting-guide", + "/cloudflare-one/team-and-resources/devices/cloudflare-one-client/uninstall", + "/cloudflare-one/team-and-resources/devices/device-registration", + "/cloudflare-one/team-and-resources/devices/user-side-certificates", + "/cloudflare-one/team-and-resources/devices/user-side-certificates/automated-deployment", + "/cloudflare-one/team-and-resources/devices/user-side-certificates/custom-certificate", + "/cloudflare-one/team-and-resources/devices/user-side-certificates/manual-deployment", + "/cloudflare-one/team-and-resources/users", + "/cloudflare-one/team-and-resources/users/risk-score", + "/cloudflare-one/team-and-resources/users/scim", + "/cloudflare-one/team-and-resources/users/seat-management", + "/cloudflare-one/team-and-resources/users/users", + "/cloudflare-one/traffic-policies", + "/cloudflare-one/traffic-policies/application-app-types", + "/cloudflare-one/traffic-policies/dns-policies", + "/cloudflare-one/traffic-policies/dns-policies/common-policies", + "/cloudflare-one/traffic-policies/dns-policies/test-dns-filtering", + "/cloudflare-one/traffic-policies/dns-policies/timed-policies", + "/cloudflare-one/traffic-policies/domain-categories", + "/cloudflare-one/traffic-policies/egress-policies", + "/cloudflare-one/traffic-policies/egress-policies/dedicated-egress-ips", + "/cloudflare-one/traffic-policies/egress-policies/egress-cloudflared", + "/cloudflare-one/traffic-policies/egress-policies/host-selectors", + "/cloudflare-one/traffic-policies/enable-ids", + "/cloudflare-one/traffic-policies/expression-syntax", + "/cloudflare-one/traffic-policies/get-started", + "/cloudflare-one/traffic-policies/get-started/dns", + "/cloudflare-one/traffic-policies/get-started/http", + "/cloudflare-one/traffic-policies/get-started/network", + "/cloudflare-one/traffic-policies/global-policies", + "/cloudflare-one/traffic-policies/http-policies", + "/cloudflare-one/traffic-policies/http-policies/antivirus-scanning", + "/cloudflare-one/traffic-policies/http-policies/common-policies", + "/cloudflare-one/traffic-policies/http-policies/file-sandboxing", + "/cloudflare-one/traffic-policies/http-policies/granular-controls", + "/cloudflare-one/traffic-policies/http-policies/http3", + "/cloudflare-one/traffic-policies/http-policies/tenant-control", + "/cloudflare-one/traffic-policies/http-policies/tls-decryption", + "/cloudflare-one/traffic-policies/identity-selectors", + "/cloudflare-one/traffic-policies/network-policies", + "/cloudflare-one/traffic-policies/network-policies/common-policies", + "/cloudflare-one/traffic-policies/network-policies/protocol-detection", + "/cloudflare-one/traffic-policies/network-policies/ssh-logging", + "/cloudflare-one/traffic-policies/order-of-enforcement", + "/cloudflare-one/traffic-policies/packet-filtering", + "/cloudflare-one/traffic-policies/packet-filtering/add-policies", + "/cloudflare-one/traffic-policies/packet-filtering/best-practices", + "/cloudflare-one/traffic-policies/packet-filtering/best-practices/extended-ruleset", + "/cloudflare-one/traffic-policies/packet-filtering/best-practices/magic-transit-egress", + "/cloudflare-one/traffic-policies/packet-filtering/best-practices/minimal-ruleset", + "/cloudflare-one/traffic-policies/packet-filtering/create-rate-limiting-policies", + "/cloudflare-one/traffic-policies/packet-filtering/enable-managed-rulesets", + "/cloudflare-one/traffic-policies/packet-filtering/form-expressions", + "/cloudflare-one/traffic-policies/packet-filtering/network-firewall-overview", + "/cloudflare-one/traffic-policies/packet-filtering/protocol-validation-rules", + "/cloudflare-one/traffic-policies/packet-filtering/ruleset-logic", + "/cloudflare-one/traffic-policies/packet-filtering/traffic-types", + "/cloudflare-one/traffic-policies/proxy", + "/cloudflare-one/traffic-policies/resolver-policies", + "/cloudflare-one/traffic-policies/tiered-policies", + "/cloudflare-one/traffic-policies/tiered-policies/organizations", + "/cloudflare-one/traffic-policies/tiered-policies/tenant-api", + "/cloudflare-one/traffic-policies/troubleshoot-gateway", + "/cloudflare-one/traffic-policies/troubleshooting", + "/cloudflare-one/troubleshooting", + "/cloudflare-one/troubleshooting/access", + "/cloudflare-one/troubleshooting/browser-isolation", + "/cloudflare-one/troubleshooting/casb", + "/cloudflare-one/troubleshooting/contact-support", + "/cloudflare-one/troubleshooting/dex", + "/cloudflare-one/troubleshooting/dlp", + "/cloudflare-one/troubleshooting/email-security", + "/cloudflare-one/troubleshooting/gateway", + "/cloudflare-one/troubleshooting/tunnel", + "/cloudflare-one/troubleshooting/wan", + "/cloudflare-one/troubleshooting/wan/connectivity", + "/cloudflare-one/troubleshooting/wan/ipsec", + "/cloudflare-one/troubleshooting/wan/routing-bgp", + "/cloudflare-one/troubleshooting/wan/tunnel-health", + "/cloudflare-one/troubleshooting/warp-client", + "/cloudflare-one/tutorials", + "/cloudflare-one/tutorials/access-workers", + "/cloudflare-one/tutorials/ai-wrapper-tenant-control", + "/cloudflare-one/tutorials/cli", + "/cloudflare-one/tutorials/clientless-access-private-dns", + "/cloudflare-one/tutorials/deploy-client-headless-linux", + "/cloudflare-one/tutorials/detect-mcp-traffic-gateway-logs", + "/cloudflare-one/tutorials/entra-id-conditional-access", + "/cloudflare-one/tutorials/entra-id-risky-users", + "/cloudflare-one/tutorials/extend-sso-with-workers", + "/cloudflare-one/tutorials/fastapi", + "/cloudflare-one/tutorials/gitlab", + "/cloudflare-one/tutorials/grafana", + "/cloudflare-one/tutorials/graphql-analytics", + "/cloudflare-one/tutorials/integrate-microsoft-mcas-teams", + "/cloudflare-one/tutorials/kubectl", + "/cloudflare-one/tutorials/m365-dedicated-egress-ips", + "/cloudflare-one/tutorials/mongodb-tunnel", + "/cloudflare-one/tutorials/mysql-network-policy", + "/cloudflare-one/tutorials/okta-u2f", + "/cloudflare-one/tutorials/r2-logs", + "/cloudflare-one/tutorials/regional-private-dns-resolver-policies", + "/cloudflare-one/tutorials/s3-buckets", + "/cloudflare-one/tutorials/tunnel-kubectl", + "/cloudflare-one/tutorials/user-selectable-egress-ips", + "/cloudflare-one/video-tutorials", + "/cloudflare-wan", + "/cloudflare-wan/analytics", + "/cloudflare-wan/analytics/netflow-analytics", + "/cloudflare-wan/analytics/network-analytics", + "/cloudflare-wan/analytics/packet-captures", + "/cloudflare-wan/analytics/query-bandwidth", + "/cloudflare-wan/analytics/query-tunnel-health", + "/cloudflare-wan/analytics/site-analytics", + "/cloudflare-wan/analytics/traceroutes", + "/cloudflare-wan/changelog", + "/cloudflare-wan/configuration", + "/cloudflare-wan/configuration/appliance", + "/cloudflare-wan/configuration/appliance/configure-hardware-appliance", + "/cloudflare-wan/configuration/appliance/configure-hardware-appliance/sfp-port-information", + "/cloudflare-wan/configuration/appliance/configure-virtual-appliance", + "/cloudflare-wan/configuration/appliance/device-metrics", + "/cloudflare-wan/configuration/appliance/maintenance", + "/cloudflare-wan/configuration/appliance/maintenance/activate-appliance", + "/cloudflare-wan/configuration/appliance/maintenance/deactivate-appliance", + "/cloudflare-wan/configuration/appliance/maintenance/default-password", + "/cloudflare-wan/configuration/appliance/maintenance/edit-basic-info", + "/cloudflare-wan/configuration/appliance/maintenance/edit-network-settings", + "/cloudflare-wan/configuration/appliance/maintenance/edit-sites", + "/cloudflare-wan/configuration/appliance/maintenance/edit-traffic-steering-settings", + "/cloudflare-wan/configuration/appliance/maintenance/heartbeat", + "/cloudflare-wan/configuration/appliance/maintenance/interrupt-service-window", + "/cloudflare-wan/configuration/appliance/maintenance/register-appliance", + "/cloudflare-wan/configuration/appliance/maintenance/remove-appliances", + "/cloudflare-wan/configuration/appliance/network-options", + "/cloudflare-wan/configuration/appliance/network-options/application-based-policies", + "/cloudflare-wan/configuration/appliance/network-options/application-based-policies/breakout-traffic", + "/cloudflare-wan/configuration/appliance/network-options/application-based-policies/prioritized-traffic", + "/cloudflare-wan/configuration/appliance/network-options/dhcp", + "/cloudflare-wan/configuration/appliance/network-options/dhcp/dhcp-options", + "/cloudflare-wan/configuration/appliance/network-options/dhcp/dhcp-relay", + "/cloudflare-wan/configuration/appliance/network-options/dhcp/dhcp-server", + "/cloudflare-wan/configuration/appliance/network-options/dhcp/dhcp-static-address-reservation", + "/cloudflare-wan/configuration/appliance/network-options/link-aggregation", + "/cloudflare-wan/configuration/appliance/network-options/nat-subnet", + "/cloudflare-wan/configuration/appliance/network-options/network-segmentation", + "/cloudflare-wan/configuration/appliance/network-options/routed-subnets", + "/cloudflare-wan/configuration/appliance/reference", + "/cloudflare-wan/configuration/appliance/troubleshooting", + "/cloudflare-wan/configuration/common-settings", + "/cloudflare-wan/configuration/common-settings/check-tunnel-health-dashboard", + "/cloudflare-wan/configuration/common-settings/configure-tunnel-health-alerts", + "/cloudflare-wan/configuration/common-settings/custom-ike-id-ipsec", + "/cloudflare-wan/configuration/common-settings/enable-roles", + "/cloudflare-wan/configuration/common-settings/sites", + "/cloudflare-wan/configuration/common-settings/update-tunnel-health-checks-frequency", + "/cloudflare-wan/configuration/how-to", + "/cloudflare-wan/configuration/how-to/configure-cloudflare-source-ips", + "/cloudflare-wan/configuration/how-to/configure-routes", + "/cloudflare-wan/configuration/how-to/configure-tunnel-endpoints", + "/cloudflare-wan/configuration/how-to/traceroute", + "/cloudflare-wan/configuration/multi-cloud-networking", + "/cloudflare-wan/configuration/third-party", + "/cloudflare-wan/configuration/third-party/alibaba-cloud", + "/cloudflare-wan/configuration/third-party/aruba-edgeconnect", + "/cloudflare-wan/configuration/third-party/aws", + "/cloudflare-wan/configuration/third-party/azure", + "/cloudflare-wan/configuration/third-party/azure/azure-virtual-wan", + "/cloudflare-wan/configuration/third-party/azure/azure-vpn-gateway", + "/cloudflare-wan/configuration/third-party/cisco-ios-xe", + "/cloudflare-wan/configuration/third-party/cisco-meraki-static", + "/cloudflare-wan/configuration/third-party/fitelnet", + "/cloudflare-wan/configuration/third-party/fortinet", + "/cloudflare-wan/configuration/third-party/google", + "/cloudflare-wan/configuration/third-party/juniper", + "/cloudflare-wan/configuration/third-party/oracle", + "/cloudflare-wan/configuration/third-party/palo-alto", + "/cloudflare-wan/configuration/third-party/pfsense", + "/cloudflare-wan/configuration/third-party/sonicwall", + "/cloudflare-wan/configuration/third-party/sophos-firewall", + "/cloudflare-wan/configuration/third-party/strongswan", + "/cloudflare-wan/configuration/third-party/ubiquiti", + "/cloudflare-wan/configuration/third-party/velocloud", + "/cloudflare-wan/configuration/third-party/viptela", + "/cloudflare-wan/configuration/third-party/vyos", + "/cloudflare-wan/configuration/third-party/yamaha", + "/cloudflare-wan/get-started", + "/cloudflare-wan/glossary", + "/cloudflare-wan/legal", + "/cloudflare-wan/legal/3rdparty", + "/cloudflare-wan/load-balancing", + "/cloudflare-wan/network-interconnect", + "/cloudflare-wan/on-ramps", + "/cloudflare-wan/private-origin-via-cloudflare-wan", + "/cloudflare-wan/reference", + "/cloudflare-wan/reference/anti-replay-protection", + "/cloudflare-wan/reference/bandwidth-measurement", + "/cloudflare-wan/reference/device-compatibility", + "/cloudflare-wan/reference/gre-ipsec-tunnels", + "/cloudflare-wan/reference/how-cloudflare-calculates-tunnel-health-alerts", + "/cloudflare-wan/reference/mtu-mss", + "/cloudflare-wan/reference/traffic-steering", + "/cloudflare-wan/reference/tunnel-health-checks", + "/cloudflare-wan/reference/virtual-networks", + "/cloudflare-wan/security", + "/cloudflare-wan/troubleshooting", + "/cloudflare-wan/troubleshooting/connectivity", + "/cloudflare-wan/troubleshooting/ipsec-troubleshoot", + "/cloudflare-wan/troubleshooting/routing-and-bgp", + "/cloudflare-wan/troubleshooting/tunnel-health", + "/cloudflare-wan/wan-transformation", + "/cloudflare-wan/zero-trust", + "/cloudflare-wan/zero-trust/cloudflare-gateway", + "/cloudflare-wan/zero-trust/cloudflare-one-client", + "/cloudflare-wan/zero-trust/cloudflare-tunnel", + "/cloudflare-wan/zero-trust/connectivity-options", + "/cloudflare-wan/zero-trust/security-services", + "/constellation", + "/constellation/platform", + "/constellation/platform/client-api", + "/containers", + "/containers/container-class", + "/containers/examples", + "/containers/examples/container-backend", + "/containers/examples/cron", + "/containers/examples/durable-object-interface", + "/containers/examples/env-vars-and-secrets", + "/containers/examples/r2-fuse-mount", + "/containers/examples/stateless", + "/containers/examples/status-hooks", + "/containers/examples/websocket", + "/containers/faq", + "/containers/get-started", + "/containers/local-dev", + "/containers/platform-details", + "/containers/platform-details/architecture", + "/containers/platform-details/durable-object-methods", + "/containers/platform-details/environment-variables", + "/containers/platform-details/image-management", + "/containers/platform-details/limits", + "/containers/platform-details/outbound-traffic", + "/containers/platform-details/placement", + "/containers/platform-details/rollouts", + "/containers/platform-details/scaling-and-routing", + "/containers/platform-details/workers-connections", + "/containers/pricing", + "/containers/ssh", + "/containers/wrangler-commands", + "/containers/wrangler-configuration", + "/d1", + "/d1/best-practices", + "/d1/best-practices/import-export-data", + "/d1/best-practices/local-development", + "/d1/best-practices/query-d1", + "/d1/best-practices/read-replication", + "/d1/best-practices/remote-development", + "/d1/best-practices/retry-queries", + "/d1/best-practices/use-d1-from-pages", + "/d1/best-practices/use-indexes", + "/d1/configuration", + "/d1/configuration/data-location", + "/d1/configuration/environments", + "/d1/d1-api", + "/d1/demos", + "/d1/examples", + "/d1/examples/d1-and-hono", + "/d1/examples/d1-and-remix", + "/d1/examples/d1-and-sveltekit", + "/d1/examples/export-d1-into-r2", + "/d1/examples/query-d1-from-python-workers", + "/d1/get-started", + "/d1/observability", + "/d1/observability/audit-logs", + "/d1/observability/billing", + "/d1/observability/debug-d1", + "/d1/observability/metrics-analytics", + "/d1/platform", + "/d1/platform/alpha-migration", + "/d1/platform/limits", + "/d1/platform/pricing", + "/d1/platform/release-notes", + "/d1/platform/storage-options", + "/d1/reference", + "/d1/reference/backups", + "/d1/reference/community-projects", + "/d1/reference/data-security", + "/d1/reference/faq", + "/d1/reference/generated-columns", + "/d1/reference/glossary", + "/d1/reference/migrations", + "/d1/reference/time-travel", + "/d1/sql-api", + "/d1/sql-api/foreign-keys", + "/d1/sql-api/query-json", + "/d1/sql-api/sql-statements", + "/d1/tutorials", + "/d1/tutorials/build-a-comments-api", + "/d1/tutorials/build-a-staff-directory-app", + "/d1/tutorials/build-an-api-to-access-d1", + "/d1/tutorials/d1-and-prisma-orm", + "/d1/tutorials/import-to-d1-with-rest-api", + "/d1/tutorials/using-read-replication-for-e-com", + "/d1/worker-api", + "/d1/worker-api/d1-database", + "/d1/worker-api/prepared-statements", + "/d1/worker-api/return-object", + "/d1/wrangler-commands", + "/data-localization", + "/data-localization/changelog", + "/data-localization/compatibility", + "/data-localization/faq", + "/data-localization/geo-key-manager", + "/data-localization/how-to", + "/data-localization/how-to/cache", + "/data-localization/how-to/cloudflare-for-saas", + "/data-localization/how-to/durable-objects", + "/data-localization/how-to/load-balancing", + "/data-localization/how-to/pages", + "/data-localization/how-to/r2", + "/data-localization/how-to/workers", + "/data-localization/how-to/zero-trust", + "/data-localization/limitations", + "/data-localization/metadata-boundary", + "/data-localization/metadata-boundary/faq", + "/data-localization/metadata-boundary/get-started", + "/data-localization/metadata-boundary/graphql-datasets", + "/data-localization/metadata-boundary/logpush-datasets", + "/data-localization/metadata-boundary/out-of-region-access", + "/data-localization/region-support", + "/data-localization/regional-services", + "/data-localization/regional-services/get-started", + "/data-localization/regional-services/http-requests", + "/ddos-protection", + "/ddos-protection/about", + "/ddos-protection/about/attack-coverage", + "/ddos-protection/about/components", + "/ddos-protection/about/how-ddos-protection-works", + "/ddos-protection/advanced-ddos-systems", + "/ddos-protection/advanced-ddos-systems/api", + "/ddos-protection/advanced-ddos-systems/api/dns-protection", + "/ddos-protection/advanced-ddos-systems/api/dns-protection/examples", + "/ddos-protection/advanced-ddos-systems/api/dns-protection/json-objects", + "/ddos-protection/advanced-ddos-systems/api/programmable-flow-protection", + "/ddos-protection/advanced-ddos-systems/api/programmable-flow-protection/examples", + "/ddos-protection/advanced-ddos-systems/api/programmable-flow-protection/json-objects", + "/ddos-protection/advanced-ddos-systems/api/tcp-protection", + "/ddos-protection/advanced-ddos-systems/api/tcp-protection/examples", + "/ddos-protection/advanced-ddos-systems/api/tcp-protection/json-objects", + "/ddos-protection/advanced-ddos-systems/concepts", + "/ddos-protection/advanced-ddos-systems/how-to", + "/ddos-protection/advanced-ddos-systems/how-to/add-prefix", + "/ddos-protection/advanced-ddos-systems/how-to/add-prefix-allowlist", + "/ddos-protection/advanced-ddos-systems/how-to/create-filter", + "/ddos-protection/advanced-ddos-systems/how-to/create-rule", + "/ddos-protection/advanced-ddos-systems/how-to/exclude-prefix", + "/ddos-protection/advanced-ddos-systems/overview", + "/ddos-protection/advanced-ddos-systems/overview/advanced-dns-protection", + "/ddos-protection/advanced-ddos-systems/overview/advanced-tcp-protection", + "/ddos-protection/advanced-ddos-systems/overview/programmable-flow-protection", + "/ddos-protection/advanced-ddos-systems/troubleshooting", + "/ddos-protection/best-practices", + "/ddos-protection/best-practices/prevent-ddos-attacks-external", + "/ddos-protection/best-practices/proactive-defense", + "/ddos-protection/best-practices/third-party", + "/ddos-protection/botnet-threat-feed", + "/ddos-protection/change-log", + "/ddos-protection/change-log/general-updates", + "/ddos-protection/change-log/http", + "/ddos-protection/change-log/http/2022-04-07", + "/ddos-protection/change-log/http/2022-04-12", + "/ddos-protection/change-log/http/2022-04-21", + "/ddos-protection/change-log/http/2022-05-03", + "/ddos-protection/change-log/http/2022-05-12", + "/ddos-protection/change-log/http/2022-06-01", + "/ddos-protection/change-log/http/2022-06-08", + "/ddos-protection/change-log/http/2022-07-06", + "/ddos-protection/change-log/http/2022-07-08", + "/ddos-protection/change-log/http/2022-07-18", + "/ddos-protection/change-log/http/2022-08-02", + "/ddos-protection/change-log/http/2022-08-10", + "/ddos-protection/change-log/http/2022-08-16", + "/ddos-protection/change-log/http/2022-09-13", + "/ddos-protection/change-log/http/2022-09-14", + "/ddos-protection/change-log/http/2022-09-19-emergency", + "/ddos-protection/change-log/http/2022-10-06-emergency", + "/ddos-protection/change-log/http/2022-10-14", + "/ddos-protection/change-log/http/2022-11-02-emergency", + "/ddos-protection/change-log/http/2022-12-07-emergency", + "/ddos-protection/change-log/http/2023-01-30", + "/ddos-protection/change-log/http/2023-02-20", + "/ddos-protection/change-log/http/2023-02-28-emergency", + "/ddos-protection/change-log/http/2023-03-10", + "/ddos-protection/change-log/http/2023-03-22", + "/ddos-protection/change-log/http/2023-04-03", + "/ddos-protection/change-log/http/2023-04-17", + "/ddos-protection/change-log/http/2023-04-21-emergency", + "/ddos-protection/change-log/http/2023-04-27-emergency", + "/ddos-protection/change-log/http/2023-05-02-emergency", + "/ddos-protection/change-log/http/2023-05-15-emergency", + "/ddos-protection/change-log/http/2023-05-16-emergency", + "/ddos-protection/change-log/http/2023-05-22", + "/ddos-protection/change-log/http/2023-05-26", + "/ddos-protection/change-log/http/2023-06-05-emergency", + "/ddos-protection/change-log/http/2023-06-06", + "/ddos-protection/change-log/http/2023-06-14-emergency", + "/ddos-protection/change-log/http/2023-06-16", + "/ddos-protection/change-log/http/2023-06-19", + "/ddos-protection/change-log/http/2023-06-28", + "/ddos-protection/change-log/http/2023-07-06", + "/ddos-protection/change-log/http/2023-07-07", + "/ddos-protection/change-log/http/2023-07-12-emergency", + "/ddos-protection/change-log/http/2023-07-17", + "/ddos-protection/change-log/http/2023-07-31", + "/ddos-protection/change-log/http/2023-08-11-emergency", + "/ddos-protection/change-log/http/2023-08-14", + "/ddos-protection/change-log/http/2023-08-16-emergency", + "/ddos-protection/change-log/http/2023-08-25-emergency", + "/ddos-protection/change-log/http/2023-08-29-emergency", + "/ddos-protection/change-log/http/2023-08-30-emergency", + "/ddos-protection/change-log/http/2023-09-05-emergency", + "/ddos-protection/change-log/http/2023-09-21-emergency", + "/ddos-protection/change-log/http/2023-09-24-emergency", + "/ddos-protection/change-log/http/2023-10-09-emergency", + "/ddos-protection/change-log/http/2023-10-11", + "/ddos-protection/change-log/http/2023-10-19", + "/ddos-protection/change-log/http/2023-11-10-emergency", + "/ddos-protection/change-log/http/2023-11-13-emergency", + "/ddos-protection/change-log/http/2023-11-22", + "/ddos-protection/change-log/http/2023-11-29", + "/ddos-protection/change-log/http/2023-12-08-emergency", + "/ddos-protection/change-log/http/2023-12-14-emergency", + "/ddos-protection/change-log/http/2023-12-19-emergency", + "/ddos-protection/change-log/http/2024-01-05", + "/ddos-protection/change-log/http/2024-01-23", + "/ddos-protection/change-log/http/2024-01-25", + "/ddos-protection/change-log/http/2024-01-26-emergency", + "/ddos-protection/change-log/http/2024-02-05-emergency", + "/ddos-protection/change-log/http/2024-02-06-emergency", + "/ddos-protection/change-log/http/2024-02-08-emergency", + "/ddos-protection/change-log/http/2024-02-12", + "/ddos-protection/change-log/http/2024-02-19", + "/ddos-protection/change-log/http/2024-02-26-emergency", + "/ddos-protection/change-log/http/2024-02-27", + "/ddos-protection/change-log/http/2024-04-02", + "/ddos-protection/change-log/http/2024-04-04-emergency", + "/ddos-protection/change-log/http/2024-04-16-emergency", + "/ddos-protection/change-log/http/2024-04-19", + "/ddos-protection/change-log/http/scheduled-changes", + "/ddos-protection/change-log/network", + "/ddos-protection/change-log/network/2022-04-12", + "/ddos-protection/change-log/network/2022-09-16", + "/ddos-protection/change-log/network/2022-09-21", + "/ddos-protection/change-log/network/2022-10-06", + "/ddos-protection/change-log/network/2022-10-24", + "/ddos-protection/change-log/network/2022-12-02", + "/ddos-protection/change-log/network/2023-04-17", + "/ddos-protection/change-log/network/2023-07-31", + "/ddos-protection/change-log/network/2024-03-12", + "/ddos-protection/change-log/network/scheduled-changes", + "/ddos-protection/frequently-asked-questions", + "/ddos-protection/get-started", + "/ddos-protection/managed-rulesets", + "/ddos-protection/managed-rulesets/adaptive-protection", + "/ddos-protection/managed-rulesets/http", + "/ddos-protection/managed-rulesets/http/http-overrides", + "/ddos-protection/managed-rulesets/http/http-overrides/configure-api", + "/ddos-protection/managed-rulesets/http/http-overrides/configure-dashboard", + "/ddos-protection/managed-rulesets/http/http-overrides/link-configure-terraform", + "/ddos-protection/managed-rulesets/http/http-overrides/override-examples", + "/ddos-protection/managed-rulesets/http/http-overrides/override-expressions", + "/ddos-protection/managed-rulesets/http/override-parameters", + "/ddos-protection/managed-rulesets/http/rule-categories", + "/ddos-protection/managed-rulesets/network", + "/ddos-protection/managed-rulesets/network/network-overrides", + "/ddos-protection/managed-rulesets/network/network-overrides/configure-api", + "/ddos-protection/managed-rulesets/network/network-overrides/configure-dashboard", + "/ddos-protection/managed-rulesets/network/network-overrides/link-configure-terraform", + "/ddos-protection/managed-rulesets/network/network-overrides/override-examples", + "/ddos-protection/managed-rulesets/network/network-overrides/override-expressions", + "/ddos-protection/managed-rulesets/network/override-parameters", + "/ddos-protection/managed-rulesets/network/rule-categories", + "/ddos-protection/reference", + "/ddos-protection/reference/alerts", + "/ddos-protection/reference/analytics", + "/ddos-protection/reference/logs", + "/ddos-protection/reference/reports", + "/ddos-protection/reference/simulate-ddos-attack", + "/directory", + "/dmarc-management", + "/dmarc-management/dns-lookup-limits", + "/dmarc-management/enable", + "/dmarc-management/security-records", + "/dmarc-management/statistics", + "/dns", + "/dns/additional-options", + "/dns/additional-options/analytics", + "/dns/additional-options/dns-zone-defaults", + "/dns/additional-options/reverse-zones", + "/dns/changelog", + "/dns/cname-flattening", + "/dns/cname-flattening/cname-flattening-diagram", + "/dns/cname-flattening/set-up-cname-flattening", + "/dns/concepts", + "/dns/dns-firewall", + "/dns/dns-firewall/analytics", + "/dns/dns-firewall/faq", + "/dns/dns-firewall/random-prefix-attacks", + "/dns/dns-firewall/random-prefix-attacks/about", + "/dns/dns-firewall/random-prefix-attacks/setup", + "/dns/dns-firewall/setup", + "/dns/dnssec", + "/dns/dnssec/dnssec-active-migration", + "/dns/dnssec/dnssec-states", + "/dns/dnssec/enable-nsec3", + "/dns/dnssec/multi-signer-dnssec", + "/dns/dnssec/multi-signer-dnssec/about", + "/dns/dnssec/multi-signer-dnssec/setup", + "/dns/dnssec/troubleshooting", + "/dns/dnssec/validation-and-key-management", + "/dns/faq", + "/dns/foundation-dns", + "/dns/foundation-dns/advanced-nameservers", + "/dns/foundation-dns/dnssec-keys", + "/dns/foundation-dns/setup", + "/dns/get-started", + "/dns/glossary", + "/dns/internal-dns", + "/dns/internal-dns/analytics", + "/dns/internal-dns/connectivity", + "/dns/internal-dns/dns-views", + "/dns/internal-dns/get-started", + "/dns/internal-dns/internal-zones", + "/dns/internal-dns/internal-zones/internal-dns-records", + "/dns/internal-dns/internal-zones/reference-zones", + "/dns/internal-dns/internal-zones/setup", + "/dns/manage-dns-records", + "/dns/manage-dns-records/how-to", + "/dns/manage-dns-records/how-to/batch-record-changes", + "/dns/manage-dns-records/how-to/create-dns-records", + "/dns/manage-dns-records/how-to/create-subdomain", + "/dns/manage-dns-records/how-to/create-zone-apex", + "/dns/manage-dns-records/how-to/email-records", + "/dns/manage-dns-records/how-to/import-and-export", + "/dns/manage-dns-records/how-to/managing-dynamic-ip-addresses", + "/dns/manage-dns-records/how-to/round-robin-dns", + "/dns/manage-dns-records/how-to/subdomains-outside-cloudflare", + "/dns/manage-dns-records/reference", + "/dns/manage-dns-records/reference/dns-record-types", + "/dns/manage-dns-records/reference/record-attributes", + "/dns/manage-dns-records/reference/ttl", + "/dns/manage-dns-records/reference/vendor-specific-records", + "/dns/manage-dns-records/reference/wildcard-dns-records", + "/dns/manage-dns-records/troubleshooting", + "/dns/manage-dns-records/troubleshooting/cname-domain-verification", + "/dns/manage-dns-records/troubleshooting/existing-ns-record", + "/dns/manage-dns-records/troubleshooting/exposed-ip-address", + "/dns/manage-dns-records/troubleshooting/records-with-same-name", + "/dns/manage-dns-records/troubleshooting/stale-response", + "/dns/manage-dns-records/troubleshooting/unexpected-dns-records", + "/dns/nameservers", + "/dns/nameservers/advanced-nameservers", + "/dns/nameservers/custom-nameservers", + "/dns/nameservers/custom-nameservers/account-custom-nameservers", + "/dns/nameservers/custom-nameservers/tenant-custom-nameservers", + "/dns/nameservers/custom-nameservers/zone-custom-nameservers", + "/dns/nameservers/nameserver-options", + "/dns/nameservers/update-nameservers", + "/dns/private-origins", + "/dns/private-origins/private-network-routing", + "/dns/private-origins/set-up-via-cloudflare-wan", + "/dns/private-origins/troubleshooting", + "/dns/proxy-status", + "/dns/proxy-status/enforce-dns-only", + "/dns/proxy-status/limitations", + "/dns/proxy-status/use-cases", + "/dns/reference", + "/dns/reference/all-features", + "/dns/reference/analytics-api-properties", + "/dns/reference/analytics-mcp-server", + "/dns/reference/best-practices", + "/dns/reference/domain-connect", + "/dns/reference/recommended-third-party-tools", + "/dns/troubleshooting", + "/dns/troubleshooting/dns-debug-endpoints", + "/dns/troubleshooting/dns-issues", + "/dns/troubleshooting/dns-probe-finished-nxdomain", + "/dns/troubleshooting/dns-probe-possible", + "/dns/troubleshooting/email-issues", + "/dns/zone-setups", + "/dns/zone-setups/conversions", + "/dns/zone-setups/conversions/convert-full-to-partial", + "/dns/zone-setups/conversions/convert-full-to-secondary", + "/dns/zone-setups/conversions/convert-partial-to-full", + "/dns/zone-setups/conversions/convert-partial-to-secondary", + "/dns/zone-setups/conversions/convert-secondary-to-full", + "/dns/zone-setups/conversions/convert-secondary-to-partial", + "/dns/zone-setups/full-setup", + "/dns/zone-setups/full-setup/setup", + "/dns/zone-setups/full-setup/troubleshooting", + "/dns/zone-setups/partial-setup", + "/dns/zone-setups/partial-setup/dns-resolution", + "/dns/zone-setups/partial-setup/setup", + "/dns/zone-setups/reference", + "/dns/zone-setups/reference/dns-quick-scan", + "/dns/zone-setups/reference/domain-status", + "/dns/zone-setups/removal", + "/dns/zone-setups/subdomain-setup", + "/dns/zone-setups/subdomain-setup/dnssec", + "/dns/zone-setups/subdomain-setup/move-to-new-account", + "/dns/zone-setups/subdomain-setup/rollback", + "/dns/zone-setups/subdomain-setup/setup", + "/dns/zone-setups/subdomain-setup/setup/parent-on-full", + "/dns/zone-setups/subdomain-setup/setup/parent-on-partial", + "/dns/zone-setups/troubleshooting", + "/dns/zone-setups/troubleshooting/cannot-add-domain", + "/dns/zone-setups/troubleshooting/delete-all-records", + "/dns/zone-setups/troubleshooting/domain-deleted", + "/dns/zone-setups/troubleshooting/pending-nameservers", + "/dns/zone-setups/zone-transfers", + "/dns/zone-setups/zone-transfers/access-control-lists", + "/dns/zone-setups/zone-transfers/access-control-lists/cloudflare-ip-addresses", + "/dns/zone-setups/zone-transfers/access-control-lists/create-new-list", + "/dns/zone-setups/zone-transfers/cloudflare-as-primary", + "/dns/zone-setups/zone-transfers/cloudflare-as-primary/dnssec-for-primary", + "/dns/zone-setups/zone-transfers/cloudflare-as-primary/setup", + "/dns/zone-setups/zone-transfers/cloudflare-as-primary/transfer-criteria", + "/dns/zone-setups/zone-transfers/cloudflare-as-secondary", + "/dns/zone-setups/zone-transfers/cloudflare-as-secondary/alerts", + "/dns/zone-setups/zone-transfers/cloudflare-as-secondary/dnssec-for-secondary", + "/dns/zone-setups/zone-transfers/cloudflare-as-secondary/proxy-traffic", + "/dns/zone-setups/zone-transfers/cloudflare-as-secondary/setup", + "/dns/zone-setups/zone-transfers/troubleshooting", + "/docs-for-agents", + "/durable-objects", + "/durable-objects/api", + "/durable-objects/api/alarms", + "/durable-objects/api/base", + "/durable-objects/api/container", + "/durable-objects/api/id", + "/durable-objects/api/legacy-kv-storage-api", + "/durable-objects/api/namespace", + "/durable-objects/api/sqlite-storage-api", + "/durable-objects/api/state", + "/durable-objects/api/stub", + "/durable-objects/api/webgpu", + "/durable-objects/api/workers-rs", + "/durable-objects/best-practices", + "/durable-objects/best-practices/access-durable-objects-storage", + "/durable-objects/best-practices/create-durable-object-stubs-and-send-requests", + "/durable-objects/best-practices/error-handling", + "/durable-objects/best-practices/rules-of-durable-objects", + "/durable-objects/best-practices/websockets", + "/durable-objects/concepts", + "/durable-objects/concepts/durable-object-lifecycle", + "/durable-objects/concepts/what-are-durable-objects", + "/durable-objects/demos", + "/durable-objects/durable-objects-rest-api", + "/durable-objects/examples", + "/durable-objects/examples/agents", + "/durable-objects/examples/alarms-api", + "/durable-objects/examples/build-a-counter", + "/durable-objects/examples/durable-object-in-memory-state", + "/durable-objects/examples/durable-object-ttl", + "/durable-objects/examples/readable-stream", + "/durable-objects/examples/reference-do-name-using-init", + "/durable-objects/examples/testing-with-durable-objects", + "/durable-objects/examples/use-kv-from-durable-objects", + "/durable-objects/examples/websocket-hibernation-server", + "/durable-objects/examples/websocket-server", + "/durable-objects/get-started", + "/durable-objects/observability", + "/durable-objects/observability/data-studio", + "/durable-objects/observability/metrics-and-analytics", + "/durable-objects/observability/troubleshooting", + "/durable-objects/platform", + "/durable-objects/platform/known-issues", + "/durable-objects/platform/limits", + "/durable-objects/platform/pricing", + "/durable-objects/platform/storage-options", + "/durable-objects/reference", + "/durable-objects/reference/data-location", + "/durable-objects/reference/data-security", + "/durable-objects/reference/durable-object-gradual-deployments", + "/durable-objects/reference/durable-objects-migrations", + "/durable-objects/reference/environments", + "/durable-objects/reference/faq", + "/durable-objects/reference/glossary", + "/durable-objects/reference/in-memory-state", + "/durable-objects/release-notes", + "/durable-objects/tutorials", + "/durable-objects/tutorials/build-a-seat-booking-app", + "/durable-objects/video-tutorials", + "/dynamic-workers", + "/dynamic-workers/api-reference", + "/dynamic-workers/examples", + "/dynamic-workers/examples/codemode", + "/dynamic-workers/examples/dynamic-workers-playground", + "/dynamic-workers/examples/dynamic-workers-starter", + "/dynamic-workers/examples/dynamic-workflows-playground", + "/dynamic-workers/getting-started", + "/dynamic-workers/pricing", + "/dynamic-workers/usage", + "/dynamic-workers/usage/bindings", + "/dynamic-workers/usage/durable-object-facets", + "/dynamic-workers/usage/dynamic-workflows", + "/dynamic-workers/usage/egress-control", + "/dynamic-workers/usage/limits", + "/dynamic-workers/usage/observability", + "/dynamic-workers/usage/static-assets", + "/email-security", + "/email-security/account-setup", + "/email-security/account-setup/escalation-contacts", + "/email-security/account-setup/manage-account-members", + "/email-security/account-setup/manage-parent-permissions", + "/email-security/account-setup/permissions", + "/email-security/account-setup/sso", + "/email-security/account-setup/sso/access", + "/email-security/account-setup/sso/azure", + "/email-security/account-setup/sso/generic-sso", + "/email-security/account-setup/sso/okta", + "/email-security/api", + "/email-security/api/service-accounts", + "/email-security/deployment", + "/email-security/deployment/api", + "/email-security/deployment/api/setup", + "/email-security/deployment/api/setup/email-retro-scan", + "/email-security/deployment/api/setup/exchange-bcc-setup", + "/email-security/deployment/api/setup/gsuite-bcc-setup", + "/email-security/deployment/api/setup/gsuite-bcc-setup/add-domain", + "/email-security/deployment/api/setup/gsuite-bcc-setup/add-retraction", + "/email-security/deployment/api/setup/gsuite-bcc-setup/bcc-rules-to-area1", + "/email-security/deployment/api/setup/gsuite-bcc-setup/create-project-gcp", + "/email-security/deployment/api/setup/gsuite-bcc-setup/create-service-account", + "/email-security/deployment/api/setup/gsuite-bcc-setup/geographic-locations", + "/email-security/deployment/api/setup/office365-graph-api", + "/email-security/deployment/api/setup/office365-journaling", + "/email-security/deployment/inline", + "/email-security/deployment/inline/reference", + "/email-security/deployment/inline/reference/egress-ips", + "/email-security/deployment/inline/setup", + "/email-security/deployment/inline/setup/cisco-area1-mx", + "/email-security/deployment/inline/setup/cisco-cisco-mx", + "/email-security/deployment/inline/setup/gsuite-area1-mx", + "/email-security/deployment/inline/setup/office-365-area1-mx", + "/email-security/deployment/inline/setup/office-365-area1-mx/use-cases", + "/email-security/deployment/inline/setup/office-365-area1-mx/use-cases/five-junk-admin-quarantine", + "/email-security/deployment/inline/setup/office-365-area1-mx/use-cases/four-user-quarantine-admin-quarantine", + "/email-security/deployment/inline/setup/office-365-area1-mx/use-cases/one-junk-admin-quarantine", + "/email-security/deployment/inline/setup/office-365-area1-mx/use-cases/three-junk-admin-quarantine", + "/email-security/deployment/inline/setup/office-365-area1-mx/use-cases/two-junk-user-quarantine", + "/email-security/email-configuration", + "/email-security/email-configuration/admin-quarantine", + "/email-security/email-configuration/domains-and-routing", + "/email-security/email-configuration/domains-and-routing/alert-webhooks", + "/email-security/email-configuration/domains-and-routing/domains", + "/email-security/email-configuration/domains-and-routing/partner-domains-tls", + "/email-security/email-configuration/email-policies", + "/email-security/email-configuration/email-policies/link-actions", + "/email-security/email-configuration/email-policies/text-addons", + "/email-security/email-configuration/enhanced-detections", + "/email-security/email-configuration/enhanced-detections/added-detections", + "/email-security/email-configuration/enhanced-detections/business-email-compromise", + "/email-security/email-configuration/enhanced-detections/business-email-compromise/gworkspaces-directory-guide", + "/email-security/email-configuration/enhanced-detections/business-email-compromise/o365-directory-guide", + "/email-security/email-configuration/lists", + "/email-security/email-configuration/lists/allowed-patterns", + "/email-security/email-configuration/lists/block-list", + "/email-security/email-configuration/lists/trusted-domains", + "/email-security/email-configuration/phish-submissions", + "/email-security/email-configuration/phish-submissions/knowbe4", + "/email-security/email-configuration/phish-submissions/microsoft-report-message", + "/email-security/email-configuration/phish-submissions/phishnet-gworkspace", + "/email-security/email-configuration/phish-submissions/phishnet-o365", + "/email-security/email-configuration/retract-settings", + "/email-security/email-configuration/retract-settings/office365-retraction", + "/email-security/glossary", + "/email-security/migrate-to-email-security", + "/email-security/partners", + "/email-security/reference", + "/email-security/reference/cloudflare-sso", + "/email-security/reference/dispositions-and-attributes", + "/email-security/reference/how-we-detect-phish", + "/email-security/reference/language-support", + "/email-security/reference/office365-gcc", + "/email-security/reference/timestamps", + "/email-security/reporting", + "/email-security/reporting/audit-logs", + "/email-security/reporting/phish-reports", + "/email-security/reporting/search", + "/email-security/reporting/search/available-parameters", + "/email-security/reporting/siem-integration", + "/email-security/reporting/siem-integration/knowbe4-integration-guide", + "/email-security/reporting/siem-integration/logscale-integration-guide", + "/email-security/reporting/siem-integration/splunk-integration-guide", + "/email-security/reporting/siem-integration/sumo-logic-integration-guide", + "/email-security/reporting/statistics-overview", + "/email-security/reporting/types-malicious-detections", + "/email-service", + "/email-service/api", + "/email-service/api/route-emails", + "/email-service/api/route-emails/email-handler", + "/email-service/api/send-emails", + "/email-service/api/send-emails/rest-api", + "/email-service/api/send-emails/smtp", + "/email-service/api/send-emails/workers-api", + "/email-service/concepts", + "/email-service/concepts/deliverability", + "/email-service/concepts/email-authentication", + "/email-service/concepts/email-lifecycle", + "/email-service/concepts/suppressions", + "/email-service/configuration", + "/email-service/configuration/domains", + "/email-service/configuration/email-routing-addresses", + "/email-service/configuration/mta-sts", + "/email-service/configuration/send-bindings", + "/email-service/configuration/subdomains", + "/email-service/examples", + "/email-service/examples/email-routing", + "/email-service/examples/email-routing/email-storage", + "/email-service/examples/email-routing/hard-bounce-handling", + "/email-service/examples/email-routing/spam-filtering", + "/email-service/examples/email-sending", + "/email-service/examples/email-sending/email-attachments", + "/email-service/examples/email-sending/magic-link", + "/email-service/examples/email-sending/recipients", + "/email-service/examples/email-sending/signup-flow", + "/email-service/examples/email-sending/smtp", + "/email-service/get-started", + "/email-service/get-started/route-emails", + "/email-service/get-started/send-emails", + "/email-service/local-development", + "/email-service/local-development/routing", + "/email-service/local-development/sending", + "/email-service/observability", + "/email-service/observability/audit-logs", + "/email-service/observability/logs", + "/email-service/observability/metrics-analytics", + "/email-service/platform", + "/email-service/platform/email-routing-rest-api", + "/email-service/platform/email-sending-rest-api", + "/email-service/platform/limits", + "/email-service/platform/pricing", + "/email-service/reference", + "/email-service/reference/faq", + "/email-service/reference/headers", + "/email-service/reference/postmaster", + "/email-service/reference/troubleshooting", + "/firewall", + "/firewall/api", + "/firewall/api/call-sequence", + "/firewall/api/cf-filters", + "/firewall/api/cf-filters/delete", + "/firewall/api/cf-filters/endpoints", + "/firewall/api/cf-filters/get", + "/firewall/api/cf-filters/json-object", + "/firewall/api/cf-filters/post", + "/firewall/api/cf-filters/put", + "/firewall/api/cf-filters/validation", + "/firewall/api/cf-filters/what-is-a-filter", + "/firewall/api/cf-firewall-rules", + "/firewall/api/cf-firewall-rules/delete", + "/firewall/api/cf-firewall-rules/endpoints", + "/firewall/api/cf-firewall-rules/get", + "/firewall/api/cf-firewall-rules/json-object", + "/firewall/api/cf-firewall-rules/post", + "/firewall/api/cf-firewall-rules/put", + "/firewall/cf-dashboard", + "/firewall/cf-dashboard/create-edit-delete-rules", + "/firewall/cf-dashboard/create-mtls-rule", + "/firewall/cf-dashboard/rule-preview", + "/firewall/cf-firewall-rules", + "/firewall/cf-firewall-rules/actions", + "/firewall/cf-firewall-rules/order-priority", + "/firewall/troubleshooting", + "/firewall/troubleshooting/required-changes-to-enable-url-normalization", + "/flagship", + "/flagship/best-practices", + "/flagship/binding", + "/flagship/binding/methods", + "/flagship/binding/types", + "/flagship/concepts", + "/flagship/configuration", + "/flagship/get-started", + "/flagship/reference", + "/flagship/reference/api-reference", + "/flagship/reference/evaluation-reasons", + "/flagship/reference/limits", + "/flagship/sdk", + "/flagship/sdk/client-provider", + "/flagship/sdk/python", + "/flagship/sdk/server-provider", + "/flagship/targeting", + "/flagship/targeting/operators", + "/flagship/targeting/percentage-rollouts", + "/fundamentals", + "/fundamentals/account", + "/fundamentals/account/account-security", + "/fundamentals/account/account-security/abuse-contact", + "/fundamentals/account/account-security/audit-logs", + "/fundamentals/account/account-security/cloudflare-access", + "/fundamentals/account/account-security/dashboard-sso", + "/fundamentals/account/account-security/leaked-password-notifications", + "/fundamentals/account/account-security/manage-active-sessions", + "/fundamentals/account/account-security/review-audit-logs", + "/fundamentals/account/account-security/scim-setup", + "/fundamentals/account/account-security/scim-setup/authentik", + "/fundamentals/account/account-security/scim-setup/entra", + "/fundamentals/account/account-security/scim-setup/okta", + "/fundamentals/account/account-security/scim-setup/troubleshooting", + "/fundamentals/account/account-security/secure-a-compromised-account", + "/fundamentals/account/account-security/zone-holds", + "/fundamentals/account/change-super-admin", + "/fundamentals/account/create-account", + "/fundamentals/account/find-account-and-zone-ids", + "/fundamentals/api", + "/fundamentals/api/get-started", + "/fundamentals/api/get-started/account-owned-tokens", + "/fundamentals/api/get-started/ca-keys", + "/fundamentals/api/get-started/create-token", + "/fundamentals/api/get-started/keys", + "/fundamentals/api/get-started/token-formats", + "/fundamentals/api/how-to", + "/fundamentals/api/how-to/account-owned-token-template", + "/fundamentals/api/how-to/control-api-access", + "/fundamentals/api/how-to/create-via-api", + "/fundamentals/api/how-to/make-api-calls", + "/fundamentals/api/how-to/restrict-tokens", + "/fundamentals/api/how-to/roll-token", + "/fundamentals/api/reference", + "/fundamentals/api/reference/deprecations", + "/fundamentals/api/reference/graphql-api", + "/fundamentals/api/reference/limits", + "/fundamentals/api/reference/permissions", + "/fundamentals/api/reference/rest-api", + "/fundamentals/api/reference/sdks", + "/fundamentals/api/reference/template", + "/fundamentals/api/reference/wrangler-api", + "/fundamentals/api/troubleshooting", + "/fundamentals/concepts", + "/fundamentals/concepts/accounts-and-zones", + "/fundamentals/concepts/cloudflare-ip-addresses", + "/fundamentals/concepts/how-cloudflare-works", + "/fundamentals/concepts/traffic-flow-cloudflare", + "/fundamentals/get-started", + "/fundamentals/manage-domains", + "/fundamentals/manage-domains/add-multiple-sites-automation", + "/fundamentals/manage-domains/add-site", + "/fundamentals/manage-domains/domain-version", + "/fundamentals/manage-domains/manage-subdomains", + "/fundamentals/manage-domains/move-domain", + "/fundamentals/manage-domains/pause-cloudflare", + "/fundamentals/manage-domains/redirect-domain", + "/fundamentals/manage-domains/remove-domain", + "/fundamentals/manage-domains/star-zones", + "/fundamentals/manage-members", + "/fundamentals/manage-members/dashboard-sso", + "/fundamentals/manage-members/manage", + "/fundamentals/manage-members/policies", + "/fundamentals/manage-members/roles", + "/fundamentals/manage-members/scope", + "/fundamentals/manage-members/user-groups", + "/fundamentals/new-features", + "/fundamentals/new-features/available-rss-feeds", + "/fundamentals/new-features/consuming-rss-feeds", + "/fundamentals/oauth", + "/fundamentals/oauth/authorizing-an-application", + "/fundamentals/oauth/create-an-oauth-client", + "/fundamentals/oauth/integrate-with-cloudflare", + "/fundamentals/organizations", + "/fundamentals/organizations/limitations", + "/fundamentals/organizations/manage-members", + "/fundamentals/organizations/manage-organization", + "/fundamentals/organizations/setup", + "/fundamentals/performance", + "/fundamentals/performance/improve-seo", + "/fundamentals/performance/maintenance-mode", + "/fundamentals/performance/minimize-downtime", + "/fundamentals/performance/optimize-speed-external-link", + "/fundamentals/performance/preparing-for-surges-or-spikes-in-web-traffic", + "/fundamentals/performance/test-speed", + "/fundamentals/reference", + "/fundamentals/reference/best-practices", + "/fundamentals/reference/cdn-cgi-endpoint", + "/fundamentals/reference/cloudflare-ray-id", + "/fundamentals/reference/cloudflare-site-crawling", + "/fundamentals/reference/cloudy-ai-agent", + "/fundamentals/reference/connection-limits", + "/fundamentals/reference/cryptographic-personhood", + "/fundamentals/reference/error-responses", + "/fundamentals/reference/glossary", + "/fundamentals/reference/google-analytics", + "/fundamentals/reference/http-headers", + "/fundamentals/reference/markdown-for-agents", + "/fundamentals/reference/migration-guides", + "/fundamentals/reference/migration-guides/scim-virtual-groups-migration", + "/fundamentals/reference/network-layers", + "/fundamentals/reference/network-ports", + "/fundamentals/reference/partners", + "/fundamentals/reference/policies-compliances", + "/fundamentals/reference/policies-compliances/cloudflare-cookies", + "/fundamentals/reference/policies-compliances/compliance-docs", + "/fundamentals/reference/policies-compliances/content-security-policies", + "/fundamentals/reference/policies-compliances/cybersafe", + "/fundamentals/reference/policies-compliances/delivering-videos-with-cloudflare", + "/fundamentals/reference/policies-compliances/licenses", + "/fundamentals/reference/redirects", + "/fundamentals/reference/report-abuse", + "/fundamentals/reference/report-abuse/abuse-report-obligations", + "/fundamentals/reference/report-abuse/blocked-content", + "/fundamentals/reference/report-abuse/complaint-types", + "/fundamentals/reference/report-abuse/provide-specific-urls", + "/fundamentals/reference/report-abuse/review-policies", + "/fundamentals/reference/report-abuse/submit-report", + "/fundamentals/reference/scans-penetration", + "/fundamentals/reference/sdk-ecosystem-support-policy", + "/fundamentals/reference/tcp-connections", + "/fundamentals/reference/troubleshooting", + "/fundamentals/reference/under-attack-mode", + "/fundamentals/security", + "/fundamentals/security/pci-scans", + "/fundamentals/security/prevent-ddos-attacks-external", + "/fundamentals/security/protect-your-origin-server", + "/fundamentals/security/recovering-from-hacked-site", + "/fundamentals/security/secure-your-website", + "/fundamentals/security/under-ddos-attack", + "/fundamentals/user-profiles", + "/fundamentals/user-profiles/2fa", + "/fundamentals/user-profiles/account-recovery", + "/fundamentals/user-profiles/change-password-or-email", + "/fundamentals/user-profiles/customize-account", + "/fundamentals/user-profiles/delete-account", + "/fundamentals/user-profiles/login", + "/fundamentals/user-profiles/multi-factor-email-authentication", + "/fundamentals/user-profiles/verify-email-address", + "/glossary", + "/google-tag-gateway", + "/health-checks", + "/health-checks/concepts", + "/health-checks/concepts/health-checks-regions", + "/health-checks/get-started", + "/health-checks/health-checks-analytics", + "/health-checks/how-to", + "/health-checks/how-to/health-checks-notifications", + "/health-checks/how-to/zone-lockdown", + "/hyperdrive", + "/hyperdrive/concepts", + "/hyperdrive/concepts/connection-lifecycle", + "/hyperdrive/concepts/connection-pooling", + "/hyperdrive/concepts/how-hyperdrive-works", + "/hyperdrive/concepts/query-caching", + "/hyperdrive/configuration", + "/hyperdrive/configuration/connect-to-private-database", + "/hyperdrive/configuration/connect-to-private-database-vpc", + "/hyperdrive/configuration/firewall-and-networking-configuration", + "/hyperdrive/configuration/local-development", + "/hyperdrive/configuration/rotate-credentials", + "/hyperdrive/configuration/tls-ssl-certificates-for-hyperdrive", + "/hyperdrive/configuration/tune-connection-pool", + "/hyperdrive/demos", + "/hyperdrive/examples", + "/hyperdrive/examples/connect-to-mysql", + "/hyperdrive/examples/connect-to-mysql/mysql-database-providers", + "/hyperdrive/examples/connect-to-mysql/mysql-database-providers/aws-rds-aurora", + "/hyperdrive/examples/connect-to-mysql/mysql-database-providers/azure", + "/hyperdrive/examples/connect-to-mysql/mysql-database-providers/google-cloud-sql", + "/hyperdrive/examples/connect-to-mysql/mysql-database-providers/planetscale", + "/hyperdrive/examples/connect-to-mysql/mysql-drivers-and-libraries", + "/hyperdrive/examples/connect-to-mysql/mysql-drivers-and-libraries/drizzle-orm", + "/hyperdrive/examples/connect-to-mysql/mysql-drivers-and-libraries/mysql", + "/hyperdrive/examples/connect-to-mysql/mysql-drivers-and-libraries/mysql2", + "/hyperdrive/examples/connect-to-postgres", + "/hyperdrive/examples/connect-to-postgres/postgres-database-providers", + "/hyperdrive/examples/connect-to-postgres/postgres-database-providers/aws-rds-aurora", + "/hyperdrive/examples/connect-to-postgres/postgres-database-providers/azure", + "/hyperdrive/examples/connect-to-postgres/postgres-database-providers/cockroachdb", + "/hyperdrive/examples/connect-to-postgres/postgres-database-providers/digital-ocean", + "/hyperdrive/examples/connect-to-postgres/postgres-database-providers/fly", + "/hyperdrive/examples/connect-to-postgres/postgres-database-providers/google-cloud-sql", + "/hyperdrive/examples/connect-to-postgres/postgres-database-providers/materialize", + "/hyperdrive/examples/connect-to-postgres/postgres-database-providers/neon", + "/hyperdrive/examples/connect-to-postgres/postgres-database-providers/nile", + "/hyperdrive/examples/connect-to-postgres/postgres-database-providers/pgedge", + "/hyperdrive/examples/connect-to-postgres/postgres-database-providers/planetscale-postgres", + "/hyperdrive/examples/connect-to-postgres/postgres-database-providers/prisma-postgres", + "/hyperdrive/examples/connect-to-postgres/postgres-database-providers/supabase", + "/hyperdrive/examples/connect-to-postgres/postgres-database-providers/timescale", + "/hyperdrive/examples/connect-to-postgres/postgres-database-providers/xata", + "/hyperdrive/examples/connect-to-postgres/postgres-drivers-and-libraries", + "/hyperdrive/examples/connect-to-postgres/postgres-drivers-and-libraries/drizzle-orm", + "/hyperdrive/examples/connect-to-postgres/postgres-drivers-and-libraries/node-postgres", + "/hyperdrive/examples/connect-to-postgres/postgres-drivers-and-libraries/postgres-js", + "/hyperdrive/examples/connect-to-postgres/postgres-drivers-and-libraries/prisma-orm", + "/hyperdrive/get-started", + "/hyperdrive/hyperdrive-rest-api", + "/hyperdrive/observability", + "/hyperdrive/observability/metrics", + "/hyperdrive/observability/troubleshooting", + "/hyperdrive/planetscale", + "/hyperdrive/platform", + "/hyperdrive/platform/limits", + "/hyperdrive/platform/pricing", + "/hyperdrive/platform/release-notes", + "/hyperdrive/platform/storage-options", + "/hyperdrive/reference", + "/hyperdrive/reference/faq", + "/hyperdrive/reference/supported-databases-and-features", + "/hyperdrive/reference/wrangler-commands", + "/hyperdrive/tutorials", + "/hyperdrive/tutorials/serverless-timeseries-api-with-timescale", + "/images", + "/images/demos", + "/images/examples", + "/images/examples/transcode-from-workers-ai", + "/images/examples/watermark-from-kv", + "/images/get-started", + "/images/get-started/introduction", + "/images/get-started/key-concepts", + "/images/get-started/limits", + "/images/images-api", + "/images/optimization", + "/images/optimization/binding", + "/images/optimization/draw-overlays", + "/images/optimization/features", + "/images/optimization/hosted-images", + "/images/optimization/hosted-images/blur-variants", + "/images/optimization/hosted-images/browser-ttl", + "/images/optimization/hosted-images/create-variants", + "/images/optimization/hosted-images/delete-variants", + "/images/optimization/hosted-images/enable-flexible-variants", + "/images/optimization/hosted-images/serve-from-custom-domains", + "/images/optimization/hosted-images/serve-private-images", + "/images/optimization/hosted-images/serve-uploaded-images", + "/images/optimization/make-responsive-images", + "/images/optimization/transformations", + "/images/optimization/transformations/control-origin-access", + "/images/optimization/transformations/draw-overlays", + "/images/optimization/transformations/flows", + "/images/optimization/transformations/integrate-with-frameworks", + "/images/optimization/transformations/overview", + "/images/optimization/transformations/preserve-content-credentials", + "/images/optimization/transformations/rewrite-rules", + "/images/optimization/transformations/sources", + "/images/optimization/transformations/transform-via-workers", + "/images/platform", + "/images/platform/changelog", + "/images/polish", + "/images/polish/activate-polish", + "/images/polish/cf-polished-statuses", + "/images/polish/compression", + "/images/polish/no-webp", + "/images/pricing", + "/images/reference", + "/images/reference/security", + "/images/reference/troubleshooting", + "/images/storage", + "/images/storage/binding", + "/images/storage/manage-images", + "/images/storage/manage-images/delete-images", + "/images/storage/manage-images/edit-images", + "/images/storage/manage-images/export-images", + "/images/storage/upload-images", + "/images/storage/upload-images/configure-webhooks", + "/images/storage/upload-images/direct-creator-upload", + "/images/storage/upload-images/images-batch", + "/images/storage/upload-images/methods", + "/images/storage/upload-images/sourcing-kit", + "/images/storage/upload-images/sourcing-kit/credentials", + "/images/storage/upload-images/sourcing-kit/edit", + "/images/storage/upload-images/sourcing-kit/enable", + "/images/storage/upload-images/upload-custom-path", + "/images/storage/upload-images/upload-file-worker", + "/images/storage/upload-images/upload-url", + "/images/tutorials", + "/images/tutorials/optimize-mobile-viewing", + "/images/tutorials/optimize-user-uploaded-image", + "/key-transparency", + "/key-transparency/api", + "/key-transparency/api/auditor-information", + "/key-transparency/api/epochs", + "/key-transparency/api/namespaces", + "/key-transparency/monitor-the-auditor", + "/kv", + "/kv/api", + "/kv/api/delete-key-value-pairs", + "/kv/api/list-keys", + "/kv/api/read-key-value-pairs", + "/kv/api/write-key-value-pairs", + "/kv/concepts", + "/kv/concepts/how-kv-works", + "/kv/concepts/kv-bindings", + "/kv/concepts/kv-namespaces", + "/kv/demos", + "/kv/examples", + "/kv/examples/cache-data-with-workers-kv", + "/kv/examples/distributed-configuration-with-workers-kv", + "/kv/examples/implement-ab-testing-with-workers-kv", + "/kv/examples/routing-with-workers-kv", + "/kv/examples/workers-kv-to-serve-assets", + "/kv/get-started", + "/kv/glossary", + "/kv/observability", + "/kv/observability/metrics-analytics", + "/kv/platform", + "/kv/platform/event-subscriptions", + "/kv/platform/limits", + "/kv/platform/pricing", + "/kv/platform/release-notes", + "/kv/platform/storage-options", + "/kv/reference", + "/kv/reference/data-security", + "/kv/reference/environments", + "/kv/reference/faq", + "/kv/reference/kv-commands", + "/kv/tutorials", + "/kv/workers-kv-api", + "/learning-paths/application-security/account-security", + "/learning-paths/application-security/account-security/add-other-members", + "/learning-paths/application-security/account-security/audit-logs", + "/learning-paths/application-security/account-security/review-active-sessions", + "/learning-paths/application-security/account-security/review-audit-logs", + "/learning-paths/application-security/account-security/set-up-2fa", + "/learning-paths/application-security/default-traffic-security", + "/learning-paths/application-security/default-traffic-security/browser-integrity", + "/learning-paths/application-security/default-traffic-security/ddos", + "/learning-paths/application-security/default-traffic-security/dnssec", + "/learning-paths/application-security/default-traffic-security/mtls", + "/learning-paths/application-security/default-traffic-security/ssl", + "/learning-paths/application-security/firewall", + "/learning-paths/application-security/firewall/custom-rules", + "/learning-paths/application-security/firewall/managed-rules", + "/learning-paths/application-security/lists", + "/learning-paths/application-security/lists/configuration", + "/learning-paths/application-security/lists/features", + "/learning-paths/application-security/lists/use-cases", + "/learning-paths/application-security/rate-limiting", + "/learning-paths/application-security/rate-limiting/configurations", + "/learning-paths/application-security/rate-limiting/features", + "/learning-paths/application-security/rate-limiting/use-cases", + "/learning-paths/application-security/security-center", + "/learning-paths/application-security/security-center/brand-protection", + "/learning-paths/application-security/security-center/insights", + "/learning-paths/china-network-overview/series", + "/learning-paths/china-network-overview/series/china-express-overview-2", + "/learning-paths/china-network-overview/series/china-network-main-features-1", + "/learning-paths/clientless-access/access-application", + "/learning-paths/clientless-access/access-application/best-practices", + "/learning-paths/clientless-access/access-application/create-access-app", + "/learning-paths/clientless-access/advanced-workflows", + "/learning-paths/clientless-access/advanced-workflows/external-evaluation", + "/learning-paths/clientless-access/advanced-workflows/isolate-application", + "/learning-paths/clientless-access/alternative-onramps", + "/learning-paths/clientless-access/alternative-onramps/clientless-rbi", + "/learning-paths/clientless-access/concepts", + "/learning-paths/clientless-access/concepts/what-is-clientless-access", + "/learning-paths/clientless-access/connect-private-applications", + "/learning-paths/clientless-access/connect-private-applications/best-practices", + "/learning-paths/clientless-access/connect-private-applications/create-tunnel", + "/learning-paths/clientless-access/customize-ux", + "/learning-paths/clientless-access/customize-ux/app-launcher", + "/learning-paths/clientless-access/customize-ux/block-page", + "/learning-paths/clientless-access/customize-ux/bookmarks", + "/learning-paths/clientless-access/customize-ux/login-page", + "/learning-paths/clientless-access/customize-ux/tags", + "/learning-paths/clientless-access/initial-setup", + "/learning-paths/clientless-access/initial-setup/add-site", + "/learning-paths/clientless-access/initial-setup/configure-idp", + "/learning-paths/clientless-access/initial-setup/create-cloudflare-account", + "/learning-paths/clientless-access/initial-setup/create-zero-trust-org", + "/learning-paths/clientless-access/migrate-applications", + "/learning-paths/clientless-access/migrate-applications/best-practices", + "/learning-paths/clientless-access/migrate-applications/consume-jwt", + "/learning-paths/clientless-access/migrate-applications/integrated-sso", + "/learning-paths/clientless-access/terraform", + "/learning-paths/clientless-access/terraform/publish-apps-with-terraform", + "/learning-paths/cybersafe/account-creation", + "/learning-paths/cybersafe/account-creation/create-cloudflare-account", + "/learning-paths/cybersafe/account-creation/create-email-security-account", + "/learning-paths/cybersafe/account-creation/create-zero-trust-org", + "/learning-paths/cybersafe/concepts", + "/learning-paths/cybersafe/concepts/cipa-overview", + "/learning-paths/cybersafe/concepts/what-is-dns", + "/learning-paths/cybersafe/concepts/what-is-dns-filtering", + "/learning-paths/cybersafe/concepts/what-is-email-security", + "/learning-paths/cybersafe/email-security-onboarding", + "/learning-paths/cybersafe/email-security-onboarding/api-deployment", + "/learning-paths/cybersafe/email-security-onboarding/email-security-next-steps", + "/learning-paths/cybersafe/gateway-onboarding", + "/learning-paths/cybersafe/gateway-onboarding/gateway-block-pages", + "/learning-paths/cybersafe/gateway-onboarding/gateway-connection-methods", + "/learning-paths/cybersafe/gateway-onboarding/gateway-create-cipa-policy", + "/learning-paths/cybersafe/gateway-onboarding/gateway-create-test-policy", + "/learning-paths/cybersafe/gateway-onboarding/gateway-locations", + "/learning-paths/cybersafe/gateway-onboarding/gateway-update-local-resolver", + "/learning-paths/cybersafe/gateway-onboarding/gateway-verify-local-connectivity", + "/learning-paths/data-center-protection/advertise-prefixes", + "/learning-paths/data-center-protection/concepts", + "/learning-paths/data-center-protection/concepts/benefits-magic-transit", + "/learning-paths/data-center-protection/concepts/what-is-magic-transit", + "/learning-paths/data-center-protection/configure-ddos", + "/learning-paths/data-center-protection/configure-tunnels-routes", + "/learning-paths/data-center-protection/configure-tunnels-routes/configure-routes", + "/learning-paths/data-center-protection/configure-tunnels-routes/configure-tunnels", + "/learning-paths/data-center-protection/enable-network-firewall", + "/learning-paths/data-center-protection/enable-notifications", + "/learning-paths/data-center-protection/get-started", + "/learning-paths/data-center-protection/post-prefix-fine-tuning", + "/learning-paths/data-center-protection/run-pre-flight-checks", + "/learning-paths/data-center-protection/troubleshooting", + "/learning-paths/dns-best-practices/concepts", + "/learning-paths/dns-best-practices/concepts/phase-1", + "/learning-paths/dns-best-practices/concepts/phase-2", + "/learning-paths/dns-best-practices/concepts/phase-3", + "/learning-paths/dns-best-practices/concepts/phase-4", + "/learning-paths/dns-best-practices/concepts/summary-considerations", + "/learning-paths/durable-objects-course/series", + "/learning-paths/durable-objects-course/series/build-the-app-frontend-5", + "/learning-paths/durable-objects-course/series/deploy-your-video-call-app-7", + "/learning-paths/durable-objects-course/series/introduction-to-series-1", + "/learning-paths/durable-objects-course/series/make-answer-webrtc-calls-6", + "/learning-paths/durable-objects-course/series/real-time-messaging-with-websockets-4", + "/learning-paths/durable-objects-course/series/serverless-websocket-backend-3", + "/learning-paths/durable-objects-course/series/what-are-durable-objects-2", + "/learning-paths/holistic-ai-security/appendix", + "/learning-paths/holistic-ai-security/appendix/workers-create-custom-coaching-pages", + "/learning-paths/holistic-ai-security/build-security-policies", + "/learning-paths/holistic-ai-security/build-security-policies/set-policy-approval", + "/learning-paths/holistic-ai-security/concepts", + "/learning-paths/holistic-ai-security/concepts/mcp", + "/learning-paths/holistic-ai-security/concepts/shadow-ai", + "/learning-paths/holistic-ai-security/concepts/shadow-it", + "/learning-paths/holistic-ai-security/get-started", + "/learning-paths/holistic-ai-security/get-started/additional-setup", + "/learning-paths/holistic-ai-security/get-started/define-ai-risk-tolerance", + "/learning-paths/holistic-ai-security/monitor-ai-use", + "/learning-paths/holistic-ai-security/monitor-ai-use/monitor-prompts-responses", + "/learning-paths/holistic-ai-security/monitor-ai-use/review-inline-ai-use", + "/learning-paths/holistic-ai-security/monitor-ai-use/review-out-of-band-ai", + "/learning-paths/holistic-ai-security/secure-approved-ai-models-tools", + "/learning-paths/load-balancing/concepts", + "/learning-paths/load-balancing/concepts/health-checks", + "/learning-paths/load-balancing/concepts/load-balancer-components", + "/learning-paths/load-balancing/concepts/load-balancing", + "/learning-paths/load-balancing/concepts/routing", + "/learning-paths/load-balancing/planning", + "/learning-paths/load-balancing/planning/custom-rules", + "/learning-paths/load-balancing/planning/multiple-zones", + "/learning-paths/load-balancing/planning/origin-steering-policies", + "/learning-paths/load-balancing/planning/server-pool-health", + "/learning-paths/load-balancing/planning/session-affinity", + "/learning-paths/load-balancing/planning/traffic-steering", + "/learning-paths/load-balancing/planning/types-load-balancers", + "/learning-paths/load-balancing/setup", + "/learning-paths/load-balancing/setup/check-pool-health", + "/learning-paths/load-balancing/setup/create-monitor", + "/learning-paths/load-balancing/setup/create-pools", + "/learning-paths/load-balancing/setup/hostname-preparation", + "/learning-paths/load-balancing/setup/next-steps", + "/learning-paths/load-balancing/setup/production-traffic", + "/learning-paths/load-balancing/setup/test-load-balancer", + "/learning-paths/load-balancing/setup/traffic-analytics", + "/learning-paths/mtls/concepts", + "/learning-paths/mtls/concepts/benefits", + "/learning-paths/mtls/concepts/mtls-cloudflare", + "/learning-paths/mtls/mtls-app-security", + "/learning-paths/mtls/mtls-app-security/related-features", + "/learning-paths/mtls/mtls-cloudflare-access", + "/learning-paths/mtls/mtls-implementation", + "/learning-paths/mtls/mtls-workers", + "/learning-paths/prevent-ddos-attacks/advanced", + "/learning-paths/prevent-ddos-attacks/advanced/customize-security", + "/learning-paths/prevent-ddos-attacks/advanced/improve-analytics", + "/learning-paths/prevent-ddos-attacks/advanced/optimize-caching", + "/learning-paths/prevent-ddos-attacks/advanced/prevent-external-connections", + "/learning-paths/prevent-ddos-attacks/advanced/protect-origin-ip", + "/learning-paths/prevent-ddos-attacks/baseline", + "/learning-paths/prevent-ddos-attacks/baseline/enable-waf", + "/learning-paths/prevent-ddos-attacks/baseline/proxy-dns-records", + "/learning-paths/prevent-ddos-attacks/baseline/set-up-alerts", + "/learning-paths/prevent-ddos-attacks/baseline/tls-versions", + "/learning-paths/prevent-ddos-attacks/concepts", + "/learning-paths/prevent-ddos-attacks/concepts/ddos-attacks", + "/learning-paths/prevent-ddos-attacks/concepts/ddos-prevention", + "/learning-paths/r2-intro/series", + "/learning-paths/r2-intro/series/r2-1", + "/learning-paths/r2-intro/series/r2-2", + "/learning-paths/r2-intro/series/r2-3", + "/learning-paths/r2-intro/series/r2-4", + "/learning-paths/r2-intro/series/r2-5", + "/learning-paths/replace-vpn/build-policies", + "/learning-paths/replace-vpn/build-policies/block-page", + "/learning-paths/replace-vpn/build-policies/create-list", + "/learning-paths/replace-vpn/build-policies/create-policy", + "/learning-paths/replace-vpn/build-policies/policy-design", + "/learning-paths/replace-vpn/build-policies/session-timeouts", + "/learning-paths/replace-vpn/build-policies/shadow-it", + "/learning-paths/replace-vpn/build-policies/test-your-first-application", + "/learning-paths/replace-vpn/concepts", + "/learning-paths/replace-vpn/concepts/vpn-overview", + "/learning-paths/replace-vpn/concepts/what-is-cloudflare", + "/learning-paths/replace-vpn/concepts/why-vpn", + "/learning-paths/replace-vpn/configure-device-agent", + "/learning-paths/replace-vpn/configure-device-agent/device-enrollment-permissions", + "/learning-paths/replace-vpn/configure-device-agent/device-profiles", + "/learning-paths/replace-vpn/configure-device-agent/enable-proxy", + "/learning-paths/replace-vpn/configure-device-agent/enable-tls-decryption", + "/learning-paths/replace-vpn/configure-device-agent/private-dns", + "/learning-paths/replace-vpn/configure-device-agent/split-tunnel-settings", + "/learning-paths/replace-vpn/connect-devices", + "/learning-paths/replace-vpn/connect-devices/install-agent", + "/learning-paths/replace-vpn/connect-devices/mdm", + "/learning-paths/replace-vpn/connect-devices/validate-traffic-in-gateway", + "/learning-paths/replace-vpn/connect-private-network", + "/learning-paths/replace-vpn/connect-private-network/cloudflare-mesh", + "/learning-paths/replace-vpn/connect-private-network/cloudflared", + "/learning-paths/replace-vpn/connect-private-network/connection-methods", + "/learning-paths/replace-vpn/connect-private-network/overlapping-ips", + "/learning-paths/replace-vpn/connect-private-network/tunnel-capacity", + "/learning-paths/replace-vpn/get-started", + "/learning-paths/replace-vpn/get-started/configure-idp", + "/learning-paths/replace-vpn/get-started/create-cloudflare-account", + "/learning-paths/replace-vpn/get-started/create-zero-trust-org", + "/learning-paths/replace-vpn/get-started/prerequisites", + "/learning-paths/replace-vpn/troubleshooting", + "/learning-paths/replace-vpn/troubleshooting/troubleshoot-private-networks", + "/learning-paths/sase-overview-course/series", + "/learning-paths/sase-overview-course/series/connect-secure-from-any-network-to-anywhere-4", + "/learning-paths/sase-overview-course/series/evolution-corporate-networks-1", + "/learning-paths/sase-overview-course/series/protect-users-from-internet-risks-5", + "/learning-paths/sase-overview-course/series/secure-remote-access-to-critical-infrastructure-3", + "/learning-paths/sase-overview-course/series/stop-hosting-own-vpn-service-2", + "/learning-paths/secure-internet-traffic/build-dns-policies", + "/learning-paths/secure-internet-traffic/build-dns-policies/create-list", + "/learning-paths/secure-internet-traffic/build-dns-policies/create-policy", + "/learning-paths/secure-internet-traffic/build-dns-policies/onboard-dns", + "/learning-paths/secure-internet-traffic/build-dns-policies/recommended-dns-policies", + "/learning-paths/secure-internet-traffic/build-dns-policies/test-policy", + "/learning-paths/secure-internet-traffic/build-egress-policies", + "/learning-paths/secure-internet-traffic/build-egress-policies/deploy-egress-ips", + "/learning-paths/secure-internet-traffic/build-egress-policies/egress-policies", + "/learning-paths/secure-internet-traffic/build-egress-policies/source-ip-anchoring", + "/learning-paths/secure-internet-traffic/build-http-policies", + "/learning-paths/secure-internet-traffic/build-http-policies/browser-isolation", + "/learning-paths/secure-internet-traffic/build-http-policies/create-policy", + "/learning-paths/secure-internet-traffic/build-http-policies/data-loss-prevention", + "/learning-paths/secure-internet-traffic/build-http-policies/recommended-http-policies", + "/learning-paths/secure-internet-traffic/build-http-policies/tls-inspection", + "/learning-paths/secure-internet-traffic/build-network-policies", + "/learning-paths/secure-internet-traffic/build-network-policies/create-policy", + "/learning-paths/secure-internet-traffic/build-network-policies/recommended-network-policies", + "/learning-paths/secure-internet-traffic/concepts", + "/learning-paths/secure-internet-traffic/concepts/security-concepts", + "/learning-paths/secure-internet-traffic/configure-device-agent", + "/learning-paths/secure-internet-traffic/configure-device-agent/device-enrollment-permissions", + "/learning-paths/secure-internet-traffic/configure-device-agent/device-profiles", + "/learning-paths/secure-internet-traffic/configure-device-agent/enable-proxy", + "/learning-paths/secure-internet-traffic/configure-device-agent/pac-files", + "/learning-paths/secure-internet-traffic/configure-device-agent/private-dns", + "/learning-paths/secure-internet-traffic/configure-device-agent/split-tunnel-settings", + "/learning-paths/secure-internet-traffic/connect-devices-networks", + "/learning-paths/secure-internet-traffic/connect-devices-networks/choose-on-ramp", + "/learning-paths/secure-internet-traffic/connect-devices-networks/install-agent", + "/learning-paths/secure-internet-traffic/connect-devices-networks/mdm", + "/learning-paths/secure-internet-traffic/connect-devices-networks/validate-traffic-in-gateway", + "/learning-paths/secure-internet-traffic/initial-setup", + "/learning-paths/secure-internet-traffic/initial-setup/configure-idp", + "/learning-paths/secure-internet-traffic/initial-setup/create-cloudflare-account", + "/learning-paths/secure-internet-traffic/initial-setup/create-zero-trust-org", + "/learning-paths/secure-internet-traffic/initial-setup/prerequisites", + "/learning-paths/secure-internet-traffic/secure-saas-applications", + "/learning-paths/secure-internet-traffic/secure-saas-applications/configure-casb", + "/learning-paths/secure-internet-traffic/secure-saas-applications/layer-security", + "/learning-paths/secure-internet-traffic/secure-saas-applications/saas-security-overview", + "/learning-paths/secure-internet-traffic/secure-saas-applications/sso-front-door", + "/learning-paths/secure-internet-traffic/understand-policies", + "/learning-paths/secure-internet-traffic/understand-policies/create-list", + "/learning-paths/secure-internet-traffic/understand-policies/indicator-feeds", + "/learning-paths/secure-internet-traffic/understand-policies/order-of-enforcement", + "/learning-paths/secure-your-email/concepts", + "/learning-paths/secure-your-email/concepts/prevent-phishing-attack", + "/learning-paths/secure-your-email/concepts/protect-from-phishing-attacks", + "/learning-paths/secure-your-email/concepts/what-is-cloudflare", + "/learning-paths/secure-your-email/concepts/what-is-email-security", + "/learning-paths/secure-your-email/concepts/what-is-phishing-attack", + "/learning-paths/secure-your-email/configure-email-security", + "/learning-paths/secure-your-email/configure-email-security/active-directory-sync", + "/learning-paths/secure-your-email/configure-email-security/audit-logs", + "/learning-paths/secure-your-email/configure-email-security/create-allow-policies", + "/learning-paths/secure-your-email/configure-email-security/impersonation-registry", + "/learning-paths/secure-your-email/configure-email-security/report-phish", + "/learning-paths/secure-your-email/configure-email-security/set-additional-detections", + "/learning-paths/secure-your-email/enable-auto-moves", + "/learning-paths/secure-your-email/enable-auto-moves/configure-auto-moves", + "/learning-paths/secure-your-email/enable-auto-moves/email-dispositions", + "/learning-paths/secure-your-email/get-started", + "/learning-paths/secure-your-email/get-started/create-email-security-account", + "/learning-paths/secure-your-email/get-started/deployment-models", + "/learning-paths/secure-your-email/get-started/prerequisites", + "/learning-paths/secure-your-email/get-started/recommended-deployment-model", + "/learning-paths/secure-your-email/get-started/setup-google-workspace", + "/learning-paths/secure-your-email/get-started/setup-ms-graph-api", + "/learning-paths/secure-your-email/monitor-your-inbox", + "/learning-paths/secure-your-email/monitor-your-inbox/monitor-detections", + "/learning-paths/secure-your-email/monitor-your-inbox/phish-submissions", + "/learning-paths/secure-your-email/monitor-your-inbox/phishguard", + "/learning-paths/surge-readiness/concepts", + "/learning-paths/surge-readiness/concepts/custom-pages", + "/learning-paths/surge-readiness/performance", + "/learning-paths/surge-readiness/performance/analytics", + "/learning-paths/surge-readiness/performance/caching", + "/learning-paths/surge-readiness/performance/logs", + "/learning-paths/surge-readiness/security", + "/learning-paths/surge-readiness/security/block-agents-lock-zones", + "/learning-paths/surge-readiness/security/confirm-account-security", + "/learning-paths/surge-readiness/security/control-domain-access", + "/learning-paths/surge-readiness/security/control-incoming-requests", + "/learning-paths/surge-readiness/security/defend-content", + "/learning-paths/surge-readiness/security/enable-iaum", + "/learning-paths/surge-readiness/security/prepare-for-surges", + "/learning-paths/surge-readiness/security/secure-against-attacks", + "/learning-paths/surge-readiness/support", + "/learning-paths/surge-readiness/support/resources", + "/learning-paths/warp-overview-course/series", + "/learning-paths/warp-overview-course/series/warp-basics-1", + "/learning-paths/warp-overview-course/series/warp-basics-2", + "/learning-paths/workers/concepts", + "/learning-paths/workers/concepts/cloudflare-intro", + "/learning-paths/workers/concepts/serverless-computing", + "/learning-paths/workers/concepts/workers-concepts", + "/learning-paths/workers/devplat", + "/learning-paths/workers/devplat/intro-to-devplat", + "/learning-paths/workers/get-started", + "/learning-paths/workers/get-started/c3-and-wrangler", + "/learning-paths/workers/get-started/first-application", + "/learning-paths/workers/get-started/first-worker", + "/learning-paths/workflows-course/series", + "/learning-paths/workflows-course/series/workflows-1", + "/learning-paths/workflows-course/series/workflows-2", + "/learning-paths/workflows-course/series/workflows-3", + "/load-balancing", + "/load-balancing/additional-options", + "/load-balancing/additional-options/additional-dns-records", + "/load-balancing/additional-options/cloudflare-tunnel", + "/load-balancing/additional-options/dns-persistence", + "/load-balancing/additional-options/load-balancing-china", + "/load-balancing/additional-options/load-balancing-rules", + "/load-balancing/additional-options/load-balancing-rules/actions", + "/load-balancing/additional-options/load-balancing-rules/create-rules", + "/load-balancing/additional-options/load-balancing-rules/expressions", + "/load-balancing/additional-options/load-balancing-rules/reference", + "/load-balancing/additional-options/load-shedding", + "/load-balancing/additional-options/override-http-host-headers", + "/load-balancing/additional-options/pagerduty-integration", + "/load-balancing/additional-options/planned-maintenance", + "/load-balancing/additional-options/spectrum", + "/load-balancing/api-reference", + "/load-balancing/changelog", + "/load-balancing/get-started", + "/load-balancing/get-started/enable-load-balancing", + "/load-balancing/get-started/learning-path", + "/load-balancing/get-started/quickstart", + "/load-balancing/load-balancers", + "/load-balancing/load-balancers/common-configurations", + "/load-balancing/load-balancers/create-load-balancer", + "/load-balancing/load-balancers/dns-records", + "/load-balancing/monitors", + "/load-balancing/monitors/create-monitor", + "/load-balancing/monitors/monitor-groups", + "/load-balancing/pools", + "/load-balancing/pools/cloudflare-pages-origin", + "/load-balancing/pools/create-pool", + "/load-balancing/private-network", + "/load-balancing/private-network/cloudflare-wan", + "/load-balancing/private-network/public-to-tunnel", + "/load-balancing/private-network/warp-to-tunnel", + "/load-balancing/reference", + "/load-balancing/reference-architecture-external-link", + "/load-balancing/reference/limitations", + "/load-balancing/reference/load-balancing-analytics", + "/load-balancing/reference/migration-guides", + "/load-balancing/reference/migration-guides/health-monitor-notifications", + "/load-balancing/reference/migration-guides/load-balancing-graphql-nodes", + "/load-balancing/reference/region-mapping-api", + "/load-balancing/troubleshooting", + "/load-balancing/troubleshooting/common-error-codes", + "/load-balancing/troubleshooting/load-balancing-faq", + "/load-balancing/understand-basics", + "/load-balancing/understand-basics/adaptive-routing", + "/load-balancing/understand-basics/health-details", + "/load-balancing/understand-basics/load-balancing-components", + "/load-balancing/understand-basics/proxy-modes", + "/load-balancing/understand-basics/session-affinity", + "/load-balancing/understand-basics/traffic-steering", + "/load-balancing/understand-basics/traffic-steering/origin-level-steering", + "/load-balancing/understand-basics/traffic-steering/origin-level-steering/hash-origin-steering", + "/load-balancing/understand-basics/traffic-steering/origin-level-steering/least-outstanding-requests-pools", + "/load-balancing/understand-basics/traffic-steering/origin-level-steering/random-origin-steering", + "/load-balancing/understand-basics/traffic-steering/steering-policies", + "/load-balancing/understand-basics/traffic-steering/steering-policies/dynamic-steering", + "/load-balancing/understand-basics/traffic-steering/steering-policies/geo-steering", + "/load-balancing/understand-basics/traffic-steering/steering-policies/least-outstanding-requests", + "/load-balancing/understand-basics/traffic-steering/steering-policies/proximity-steering", + "/load-balancing/understand-basics/traffic-steering/steering-policies/standard-options", + "/log-explorer", + "/log-explorer/api", + "/log-explorer/changelog", + "/log-explorer/example-queries", + "/log-explorer/faq", + "/log-explorer/log-search", + "/log-explorer/manage-datasets", + "/log-explorer/pricing", + "/log-explorer/sql-queries", + "/logs", + "/logs/auditlogs-mcp-server", + "/logs/changelog", + "/logs/changelog/audit-logs", + "/logs/changelog/logs", + "/logs/faq", + "/logs/faq/504-origin-status-0", + "/logs/faq/common-calculations", + "/logs/faq/general-faq", + "/logs/faq/instant-logs", + "/logs/faq/logpull-api", + "/logs/faq/logpush", + "/logs/faq/random-hostnames-partial-zones", + "/logs/faq/worker-subrequests", + "/logs/glossary", + "/logs/instant-logs", + "/logs/logpull", + "/logs/logpull/additional-details", + "/logs/logpull/enabling-log-retention", + "/logs/logpull/requesting-logs", + "/logs/logpull/understanding-the-basics", + "/logs/logpush", + "/logs/logpush-mcp-server", + "/logs/logpush/alerts-and-analytics", + "/logs/logpush/examples", + "/logs/logpush/examples/example-logpush-curl", + "/logs/logpush/examples/example-logpush-python", + "/logs/logpush/logpush-health", + "/logs/logpush/logpush-job", + "/logs/logpush/logpush-job/api-configuration", + "/logs/logpush/logpush-job/custom-fields", + "/logs/logpush/logpush-job/datasets", + "/logs/logpush/logpush-job/datasets/account", + "/logs/logpush/logpush-job/datasets/account/access_requests", + "/logs/logpush/logpush-job/datasets/account/audit_logs", + "/logs/logpush/logpush-job/datasets/account/audit_logs_v2", + "/logs/logpush/logpush-job/datasets/account/biso_user_actions", + "/logs/logpush/logpush-job/datasets/account/casb_findings", + "/logs/logpush/logpush-job/datasets/account/device_posture_results", + "/logs/logpush/logpush-job/datasets/account/dex_application_tests", + "/logs/logpush/logpush-job/datasets/account/dex_device_state_events", + "/logs/logpush/logpush-job/datasets/account/dlp_forensic_copies", + "/logs/logpush/logpush-job/datasets/account/dns_firewall_logs", + "/logs/logpush/logpush-job/datasets/account/email_security_alerts", + "/logs/logpush/logpush-job/datasets/account/email_security_post_delivery_events", + "/logs/logpush/logpush-job/datasets/account/gateway_dns", + "/logs/logpush/logpush-job/datasets/account/gateway_http", + "/logs/logpush/logpush-job/datasets/account/gateway_network", + "/logs/logpush/logpush-job/datasets/account/ipsec_logs", + "/logs/logpush/logpush-job/datasets/account/magic_ids_detections", + "/logs/logpush/logpush-job/datasets/account/mcp_portal_logs", + "/logs/logpush/logpush-job/datasets/account/mnm_flow_logs", + "/logs/logpush/logpush-job/datasets/account/network_analytics_logs", + "/logs/logpush/logpush-job/datasets/account/sinkhole_http_logs", + "/logs/logpush/logpush-job/datasets/account/ssh_logs", + "/logs/logpush/logpush-job/datasets/account/turnstile_events", + "/logs/logpush/logpush-job/datasets/account/warp_config_changes", + "/logs/logpush/logpush-job/datasets/account/warp_toggle_changes", + "/logs/logpush/logpush-job/datasets/account/workers_trace_events", + "/logs/logpush/logpush-job/datasets/account/zero_trust_network_sessions", + "/logs/logpush/logpush-job/datasets/cmb", + "/logs/logpush/logpush-job/datasets/zone", + "/logs/logpush/logpush-job/datasets/zone/dns_logs", + "/logs/logpush/logpush-job/datasets/zone/firewall_events", + "/logs/logpush/logpush-job/datasets/zone/http_requests", + "/logs/logpush/logpush-job/datasets/zone/nel_reports", + "/logs/logpush/logpush-job/datasets/zone/page_shield_events", + "/logs/logpush/logpush-job/datasets/zone/spectrum_events", + "/logs/logpush/logpush-job/datasets/zone/zaraz_events", + "/logs/logpush/logpush-job/edge-log-delivery", + "/logs/logpush/logpush-job/enable-destinations", + "/logs/logpush/logpush-job/enable-destinations/aws-s3", + "/logs/logpush/logpush-job/enable-destinations/azure", + "/logs/logpush/logpush-job/enable-destinations/bigquery", + "/logs/logpush/logpush-job/enable-destinations/datadog", + "/logs/logpush/logpush-job/enable-destinations/egress-ip", + "/logs/logpush/logpush-job/enable-destinations/elastic", + "/logs/logpush/logpush-job/enable-destinations/google-cloud-storage", + "/logs/logpush/logpush-job/enable-destinations/http", + "/logs/logpush/logpush-job/enable-destinations/ibm-cloud-logs", + "/logs/logpush/logpush-job/enable-destinations/ibm-qradar", + "/logs/logpush/logpush-job/enable-destinations/kinesis", + "/logs/logpush/logpush-job/enable-destinations/new-relic", + "/logs/logpush/logpush-job/enable-destinations/other-providers", + "/logs/logpush/logpush-job/enable-destinations/pipelines", + "/logs/logpush/logpush-job/enable-destinations/r2", + "/logs/logpush/logpush-job/enable-destinations/s3-compatible-endpoints", + "/logs/logpush/logpush-job/enable-destinations/sentinelone", + "/logs/logpush/logpush-job/enable-destinations/splunk", + "/logs/logpush/logpush-job/enable-destinations/sumo-logic", + "/logs/logpush/logpush-job/enable-destinations/third-party", + "/logs/logpush/logpush-job/enable-destinations/third-party/axiom", + "/logs/logpush/logpush-job/enable-destinations/third-party/dynatrace", + "/logs/logpush/logpush-job/enable-destinations/third-party/exabeam", + "/logs/logpush/logpush-job/enable-destinations/third-party/sekoia", + "/logs/logpush/logpush-job/enable-destinations/third-party/taegis", + "/logs/logpush/logpush-job/filters", + "/logs/logpush/logpush-job/log-output-options", + "/logs/logpush/logpush-job/logpush-ownership-challenge", + "/logs/logpush/logpush-job/subrequests", + "/logs/logpush/parsing-json-log-data", + "/logs/logpush/permissions", + "/logs/r2-log-retrieval", + "/logs/reference", + "/logs/reference/change-notices", + "/logs/reference/change-notices/2023-02-01-security-fields-updates", + "/logs/reference/clientrequestsource", + "/logs/reference/pathing-status", + "/logs/reference/security-fields", + "/logs/reference/waf-fields", + "/magic-transit", + "/magic-transit/about", + "/magic-transit/alerts", + "/magic-transit/analytics", + "/magic-transit/analytics/network-analytics", + "/magic-transit/analytics/packet-captures", + "/magic-transit/analytics/query-bandwidth", + "/magic-transit/analytics/query-tunnel-health", + "/magic-transit/analytics/traceroutes", + "/magic-transit/changelog", + "/magic-transit/cloudflare-ips", + "/magic-transit/ddos", + "/magic-transit/get-started", + "/magic-transit/glossary", + "/magic-transit/how-to", + "/magic-transit/how-to/advertise-prefixes", + "/magic-transit/how-to/configure-routes", + "/magic-transit/how-to/configure-tunnel-endpoints", + "/magic-transit/how-to/enable-magic-roles", + "/magic-transit/how-to/ipv6", + "/magic-transit/how-to/safely-withdraw-byoip-prefix", + "/magic-transit/how-to/verify-ddos-protection", + "/magic-transit/network-flow", + "/magic-transit/network-health", + "/magic-transit/network-health/check-tunnel-health-dashboard", + "/magic-transit/network-health/configure-tunnel-health-alerts", + "/magic-transit/network-health/magic-tunnel-health-check-calculation", + "/magic-transit/network-health/run-endpoint-health-checks", + "/magic-transit/network-health/update-tunnel-health-checks-frequency", + "/magic-transit/network-interconnect", + "/magic-transit/on-demand", + "/magic-transit/partners", + "/magic-transit/partners/kentik", + "/magic-transit/reference", + "/magic-transit/reference/anti-replay-protection", + "/magic-transit/reference/bandwidth-measurement", + "/magic-transit/reference/egress", + "/magic-transit/reference/gre-ipsec-tunnels", + "/magic-transit/reference/how-cloudflare-calculates-tunnel-health-alerts", + "/magic-transit/reference/mtu-mss", + "/magic-transit/reference/reference-architecture", + "/magic-transit/reference/traffic-steering", + "/magic-transit/reference/tunnel-health-checks", + "/magic-transit/troubleshooting", + "/magic-transit/troubleshooting/connectivity", + "/magic-transit/troubleshooting/ipsec-troubleshoot", + "/magic-transit/troubleshooting/routing-and-bgp", + "/magic-transit/troubleshooting/tunnel-health", + "/migration-guides", + "/moq", + "/moq/about", + "/moq/feature-matrix", + "/multi-cloud-networking", + "/multi-cloud-networking/changelog", + "/multi-cloud-networking/cloud-on-ramps", + "/multi-cloud-networking/get-started", + "/multi-cloud-networking/manage-resources", + "/multi-cloud-networking/reference", + "/network", + "/network-error-logging", + "/network-error-logging/get-started", + "/network-error-logging/how-to", + "/network-error-logging/reference", + "/network-flow", + "/network-flow/api", + "/network-flow/changelog", + "/network-flow/cloud-flow-logs", + "/network-flow/faq", + "/network-flow/get-started", + "/network-flow/glossary", + "/network-flow/magic-transit-integration", + "/network-flow/network-flow-free", + "/network-flow/routers", + "/network-flow/routers/netflow-ipfix-config", + "/network-flow/routers/recommended-sampling-rate", + "/network-flow/routers/sflow-config", + "/network-flow/routers/supported-routers", + "/network-flow/rules", + "/network-flow/rules/dynamic-threshold", + "/network-flow/rules/rule-notifications", + "/network-flow/rules/s-flow-ddos-attack", + "/network-flow/rules/static-threshold", + "/network-flow/tutorials", + "/network-flow/tutorials/ddos-testing-guide", + "/network-flow/tutorials/encrypt-network-flow-data", + "/network-flow/tutorials/graphql-analytics", + "/network-interconnect", + "/network-interconnect/changelog", + "/network-interconnect/get-started", + "/network-interconnect/monitoring-and-alerts", + "/network-interconnect/operational-guidance", + "/network/grpc-connections", + "/network/ip-geolocation", + "/network/ipv6-compatibility", + "/network/maximum-upload-size", + "/network/onion-routing", + "/network/pseudo-ipv4", + "/network/true-client-ip-header", + "/network/websockets", + "/notifications", + "/notifications/api-reference", + "/notifications/get-started", + "/notifications/get-started/configure-pagerduty", + "/notifications/get-started/configure-webhooks", + "/notifications/notification-available", + "/notifications/notification-history", + "/notifications/reference", + "/notifications/reference/common-errors", + "/notifications/reference/traffic-alerts", + "/notifications/reference/webhook-payload-schema", + "/pages", + "/pages/configuration", + "/pages/configuration/api", + "/pages/configuration/branch-build-controls", + "/pages/configuration/build-caching", + "/pages/configuration/build-configuration", + "/pages/configuration/build-image", + "/pages/configuration/build-watch-paths", + "/pages/configuration/custom-domains", + "/pages/configuration/debugging-pages", + "/pages/configuration/deploy-hooks", + "/pages/configuration/early-hints", + "/pages/configuration/git-integration", + "/pages/configuration/git-integration/github-integration", + "/pages/configuration/git-integration/gitlab-integration", + "/pages/configuration/git-integration/troubleshooting", + "/pages/configuration/headers", + "/pages/configuration/monorepos", + "/pages/configuration/preview-deployments", + "/pages/configuration/redirects", + "/pages/configuration/rollbacks", + "/pages/configuration/serving-pages", + "/pages/demos", + "/pages/framework-guides", + "/pages/framework-guides/deploy-a-blazor-site", + "/pages/framework-guides/deploy-a-brunch-site", + "/pages/framework-guides/deploy-a-docusaurus-site", + "/pages/framework-guides/deploy-a-gatsby-site", + "/pages/framework-guides/deploy-a-gridsome-site", + "/pages/framework-guides/deploy-a-hexo-site", + "/pages/framework-guides/deploy-a-hono-site", + "/pages/framework-guides/deploy-a-hugo-site", + "/pages/framework-guides/deploy-a-jekyll-site", + "/pages/framework-guides/deploy-a-nuxt-site", + "/pages/framework-guides/deploy-a-pelican-site", + "/pages/framework-guides/deploy-a-preact-site", + "/pages/framework-guides/deploy-a-qwik-site", + "/pages/framework-guides/deploy-a-react-site", + "/pages/framework-guides/deploy-a-remix-site", + "/pages/framework-guides/deploy-a-solid-start-site", + "/pages/framework-guides/deploy-a-sphinx-site", + "/pages/framework-guides/deploy-a-svelte-kit-site", + "/pages/framework-guides/deploy-a-vite3-project", + "/pages/framework-guides/deploy-a-vitepress-site", + "/pages/framework-guides/deploy-a-vue-site", + "/pages/framework-guides/deploy-a-zola-site", + "/pages/framework-guides/deploy-an-analog-site", + "/pages/framework-guides/deploy-an-angular-site", + "/pages/framework-guides/deploy-an-astro-site", + "/pages/framework-guides/deploy-an-elderjs-site", + "/pages/framework-guides/deploy-an-eleventy-site", + "/pages/framework-guides/deploy-an-emberjs-site", + "/pages/framework-guides/deploy-an-mkdocs-site", + "/pages/framework-guides/deploy-anything", + "/pages/framework-guides/nextjs", + "/pages/framework-guides/nextjs/deploy-a-static-nextjs-site", + "/pages/functions", + "/pages/functions/advanced-mode", + "/pages/functions/api-reference", + "/pages/functions/bindings", + "/pages/functions/debugging-and-logging", + "/pages/functions/examples", + "/pages/functions/examples/ab-testing", + "/pages/functions/examples/cors-headers", + "/pages/functions/get-started", + "/pages/functions/local-development", + "/pages/functions/metrics", + "/pages/functions/middleware", + "/pages/functions/module-support", + "/pages/functions/plugins", + "/pages/functions/plugins/cloudflare-access", + "/pages/functions/plugins/community-plugins", + "/pages/functions/plugins/google-chat", + "/pages/functions/plugins/graphql", + "/pages/functions/plugins/hcaptcha", + "/pages/functions/plugins/honeycomb", + "/pages/functions/plugins/sentry", + "/pages/functions/plugins/static-forms", + "/pages/functions/plugins/stytch", + "/pages/functions/plugins/turnstile", + "/pages/functions/plugins/vercel-og", + "/pages/functions/pricing", + "/pages/functions/routing", + "/pages/functions/smart-placement", + "/pages/functions/source-maps", + "/pages/functions/typescript", + "/pages/functions/wrangler-configuration", + "/pages/get-started", + "/pages/get-started/c3", + "/pages/get-started/direct-upload", + "/pages/get-started/git-integration", + "/pages/how-to", + "/pages/how-to/add-custom-http-headers", + "/pages/how-to/build-commands-branches", + "/pages/how-to/custom-branch-aliases", + "/pages/how-to/deploy-a-wordpress-site", + "/pages/how-to/enable-zaraz", + "/pages/how-to/npm-private-registry", + "/pages/how-to/preview-with-cloudflare-tunnel", + "/pages/how-to/redirect-to-custom-domain", + "/pages/how-to/refactor-a-worker-to-pages-functions", + "/pages/how-to/use-direct-upload-with-continuous-integration", + "/pages/how-to/use-worker-for-ab-testing-in-pages", + "/pages/how-to/web-analytics", + "/pages/how-to/www-redirect", + "/pages/migrate-to-workers", + "/pages/migrations", + "/pages/migrations/migrating-from-firebase", + "/pages/migrations/migrating-from-netlify", + "/pages/migrations/migrating-from-vercel", + "/pages/migrations/migrating-from-workers", + "/pages/migrations/migrating-jekyll-from-github-pages", + "/pages/platform", + "/pages/platform/changelog", + "/pages/platform/known-issues", + "/pages/platform/limits", + "/pages/platform/storage-options", + "/pages/tutorials", + "/pages/tutorials/add-a-react-form-with-formspree", + "/pages/tutorials/add-an-html-form-with-formspree", + "/pages/tutorials/build-a-blog-using-nuxt-and-sanity", + "/pages/tutorials/build-an-api-with-pages-functions", + "/pages/tutorials/forms", + "/pages/tutorials/localize-a-website", + "/pages/tutorials/use-r2-as-static-asset-storage-for-pages", + "/pipelines", + "/pipelines/getting-started", + "/pipelines/observability", + "/pipelines/observability/metrics", + "/pipelines/pipelines", + "/pipelines/pipelines/manage-pipelines", + "/pipelines/platform", + "/pipelines/platform/limits", + "/pipelines/platform/pricing", + "/pipelines/reference", + "/pipelines/reference/legacy-pipelines", + "/pipelines/reference/rest-api", + "/pipelines/reference/terraform", + "/pipelines/reference/wrangler-commands", + "/pipelines/sinks", + "/pipelines/sinks/available-sinks", + "/pipelines/sinks/available-sinks/r2", + "/pipelines/sinks/available-sinks/r2-data-catalog", + "/pipelines/sinks/manage-sinks", + "/pipelines/sql-reference", + "/pipelines/sql-reference/scalar-functions", + "/pipelines/sql-reference/scalar-functions/array", + "/pipelines/sql-reference/scalar-functions/binary-string", + "/pipelines/sql-reference/scalar-functions/conditional", + "/pipelines/sql-reference/scalar-functions/hashing", + "/pipelines/sql-reference/scalar-functions/json", + "/pipelines/sql-reference/scalar-functions/math", + "/pipelines/sql-reference/scalar-functions/other", + "/pipelines/sql-reference/scalar-functions/regex", + "/pipelines/sql-reference/scalar-functions/string", + "/pipelines/sql-reference/scalar-functions/struct", + "/pipelines/sql-reference/scalar-functions/time-and-date", + "/pipelines/sql-reference/select-statements", + "/pipelines/sql-reference/sql-data-types", + "/pipelines/streams", + "/pipelines/streams/logpush", + "/pipelines/streams/manage-streams", + "/pipelines/streams/writing-to-streams", + "/plans", + "/privacy-gateway", + "/privacy-gateway/get-started", + "/privacy-gateway/reference", + "/privacy-gateway/reference/legal", + "/privacy-gateway/reference/limitations", + "/privacy-gateway/reference/metrics", + "/privacy-gateway/reference/product-compatibility", + "/privacy-proxy", + "/privacy-proxy/concepts", + "/privacy-proxy/concepts/authentication", + "/privacy-proxy/concepts/deployment-models", + "/privacy-proxy/concepts/geolocation", + "/privacy-proxy/concepts/how-it-works", + "/privacy-proxy/get-started", + "/privacy-proxy/reference", + "/privacy-proxy/reference/client-libraries", + "/privacy-proxy/reference/http-headers", + "/privacy-proxy/reference/metrics", + "/privacy-proxy/reference/metrics/graphql", + "/privacy-proxy/reference/metrics/opentelemetry", + "/privacy-proxy/reference/proxy-status", + "/pulumi", + "/pulumi/installing", + "/pulumi/tutorial", + "/pulumi/tutorial/add-site", + "/pulumi/tutorial/dynamic-provider-and-wrangler", + "/pulumi/tutorial/hello-world", + "/pulumi/tutorial/manage-secrets", + "/queues", + "/queues/configuration", + "/queues/configuration/batching-retries", + "/queues/configuration/configure-queues", + "/queues/configuration/consumer-concurrency", + "/queues/configuration/dead-letter-queues", + "/queues/configuration/event-notifications", + "/queues/configuration/javascript-apis", + "/queues/configuration/local-development", + "/queues/configuration/pause-purge", + "/queues/configuration/pull-consumers", + "/queues/demos", + "/queues/event-subscriptions", + "/queues/event-subscriptions/events-schemas", + "/queues/event-subscriptions/manage-event-subscriptions", + "/queues/examples", + "/queues/examples/list-messages-from-dash", + "/queues/examples/publish-to-a-queue-via-http", + "/queues/examples/publish-to-a-queue-via-workers", + "/queues/examples/send-errors-to-r2", + "/queues/examples/send-messages-from-dash", + "/queues/examples/serverless-etl", + "/queues/examples/use-queues-with-durable-objects", + "/queues/get-started", + "/queues/glossary", + "/queues/observability", + "/queues/observability/metrics", + "/queues/platform", + "/queues/platform/audit-logs", + "/queues/platform/changelog", + "/queues/platform/limits", + "/queues/platform/pricing", + "/queues/platform/storage-options", + "/queues/queues-api", + "/queues/reference", + "/queues/reference/delivery-guarantees", + "/queues/reference/error-codes", + "/queues/reference/how-queues-works", + "/queues/reference/wrangler-commands", + "/queues/tutorials", + "/queues/tutorials/handle-rate-limits", + "/queues/tutorials/web-crawler-with-browser-run", + "/r2", + "/r2-sql", + "/r2-sql/get-started", + "/r2-sql/platform", + "/r2-sql/platform/pricing", + "/r2-sql/query-data", + "/r2-sql/reference", + "/r2-sql/reference/limitations-best-practices", + "/r2-sql/reference/wrangler-commands", + "/r2-sql/sql-reference", + "/r2-sql/sql-reference/aggregate-functions", + "/r2-sql/sql-reference/complex-types", + "/r2-sql/sql-reference/scalar-functions", + "/r2-sql/troubleshooting", + "/r2-sql/tutorials", + "/r2-sql/tutorials/end-to-end-pipeline", + "/r2/api", + "/r2/api/error-codes", + "/r2/api/s3", + "/r2/api/s3/api", + "/r2/api/s3/extensions", + "/r2/api/s3/presigned-urls", + "/r2/api/s3/temporary-credentials", + "/r2/api/tokens", + "/r2/api/workers", + "/r2/api/workers/workers-api-reference", + "/r2/api/workers/workers-api-usage", + "/r2/api/workers/workers-multipart-usage", + "/r2/buckets", + "/r2/buckets/bucket-locks", + "/r2/buckets/cors", + "/r2/buckets/create-buckets", + "/r2/buckets/delete-buckets", + "/r2/buckets/event-notifications", + "/r2/buckets/local-uploads", + "/r2/buckets/object-lifecycles", + "/r2/buckets/public-buckets", + "/r2/buckets/storage-classes", + "/r2/data-catalog", + "/r2/data-catalog/config-examples", + "/r2/data-catalog/config-examples/duckdb", + "/r2/data-catalog/config-examples/pyiceberg", + "/r2/data-catalog/config-examples/snowflake", + "/r2/data-catalog/config-examples/spark-python", + "/r2/data-catalog/config-examples/spark-scala", + "/r2/data-catalog/config-examples/starrocks", + "/r2/data-catalog/config-examples/trino", + "/r2/data-catalog/deleting-data", + "/r2/data-catalog/get-started", + "/r2/data-catalog/manage-catalogs", + "/r2/data-catalog/observability", + "/r2/data-catalog/observability/metrics", + "/r2/data-catalog/platform", + "/r2/data-catalog/platform/pricing", + "/r2/data-catalog/table-maintenance", + "/r2/data-migration", + "/r2/data-migration/migration-strategies", + "/r2/data-migration/sippy", + "/r2/data-migration/super-slurper", + "/r2/demos", + "/r2/examples", + "/r2/examples/authenticate-r2-auth-tokens", + "/r2/examples/authenticate-r2-temp-credentials", + "/r2/examples/aws", + "/r2/examples/aws/aws-cli", + "/r2/examples/aws/aws-sdk-go", + "/r2/examples/aws/aws-sdk-java", + "/r2/examples/aws/aws-sdk-js", + "/r2/examples/aws/aws-sdk-js-v3", + "/r2/examples/aws/aws-sdk-kotlin", + "/r2/examples/aws/aws-sdk-net", + "/r2/examples/aws/aws-sdk-php", + "/r2/examples/aws/aws-sdk-ruby", + "/r2/examples/aws/aws-sdk-rust", + "/r2/examples/aws/aws4fetch", + "/r2/examples/aws/boto3", + "/r2/examples/aws/custom-header", + "/r2/examples/aws/s3mini", + "/r2/examples/cache-api", + "/r2/examples/multi-cloud", + "/r2/examples/rclone", + "/r2/examples/ssec", + "/r2/examples/terraform", + "/r2/examples/terraform-aws", + "/r2/get-started", + "/r2/get-started/cli", + "/r2/get-started/s3", + "/r2/get-started/workers-api", + "/r2/how-r2-works", + "/r2/objects", + "/r2/objects/delete-objects", + "/r2/objects/download-objects", + "/r2/objects/upload-objects", + "/r2/platform", + "/r2/platform/audit-logs", + "/r2/platform/event-subscriptions", + "/r2/platform/limits", + "/r2/platform/metrics-analytics", + "/r2/platform/release-notes", + "/r2/platform/storage-options", + "/r2/platform/troubleshooting", + "/r2/pricing", + "/r2/r2-sql", + "/r2/reference", + "/r2/reference/consistency", + "/r2/reference/data-location", + "/r2/reference/data-security", + "/r2/reference/durability", + "/r2/reference/partners", + "/r2/reference/partners/snowflake-regions", + "/r2/reference/unicode-interoperability", + "/r2/reference/wrangler-commands", + "/r2/tutorials", + "/r2/tutorials/cloudflare-access", + "/r2/tutorials/mastodon", + "/r2/tutorials/postman", + "/r2/tutorials/summarize-pdf", + "/r2/tutorials/upload-logs-event-notifications", + "/r2/video-tutorials", + "/radar", + "/radar/api-reference", + "/radar/concepts", + "/radar/concepts/aggregation-intervals", + "/radar/concepts/bot-classes", + "/radar/concepts/confidence-levels", + "/radar/concepts/normalization", + "/radar/get-started", + "/radar/get-started/configure-alerts", + "/radar/get-started/embed", + "/radar/get-started/error-codes", + "/radar/get-started/first-request", + "/radar/get-started/making-comparisons", + "/radar/glossary", + "/radar/investigate", + "/radar/investigate/application-layer-attacks", + "/radar/investigate/bgp-anomalies", + "/radar/investigate/dns", + "/radar/investigate/domain-ranking-datasets", + "/radar/investigate/http-requests", + "/radar/investigate/netflows", + "/radar/investigate/network-layer-attacks", + "/radar/investigate/outages", + "/radar/investigate/url-scanner", + "/radar/mcp-server", + "/radar/reference", + "/radar/reference/quarterly-ddos-reports", + "/radar/release-notes", + "/randomness-beacon", + "/randomness-beacon/about", + "/randomness-beacon/about/background", + "/randomness-beacon/about/future", + "/randomness-beacon/cryptographic-background", + "/randomness-beacon/cryptographic-background/randomness-generation", + "/randomness-beacon/cryptographic-background/setup-phase", + "/randomness-beacon/operator-guide", + "/randomness-beacon/user-guide", + "/realtime", + "/realtime/realtimekit", + "/realtime/realtimekit/ai", + "/realtime/realtimekit/ai/summary", + "/realtime/realtimekit/ai/transcription", + "/realtime/realtimekit/audio-calls", + "/realtime/realtimekit/best-practices", + "/realtime/realtimekit/best-practices/video-and-simulcast", + "/realtime/realtimekit/broadcast-apis", + "/realtime/realtimekit/collaborative-stores", + "/realtime/realtimekit/concepts", + "/realtime/realtimekit/concepts/meeting", + "/realtime/realtimekit/concepts/participant", + "/realtime/realtimekit/concepts/preset", + "/realtime/realtimekit/concepts/session-lifecycle", + "/realtime/realtimekit/core", + "/realtime/realtimekit/core/api-reference", + "/realtime/realtimekit/core/api-reference/realtimekitclient", + "/realtime/realtimekit/core/api-reference/rtkai", + "/realtime/realtimekit/core/api-reference/rtkchat", + "/realtime/realtimekit/core/api-reference/rtkconnectedmeetings", + "/realtime/realtimekit/core/api-reference/rtklivestream", + "/realtime/realtimekit/core/api-reference/rtkmeta", + "/realtime/realtimekit/core/api-reference/rtkparticipant", + "/realtime/realtimekit/core/api-reference/rtkparticipantmap", + "/realtime/realtimekit/core/api-reference/rtkparticipants", + "/realtime/realtimekit/core/api-reference/rtkpermissionspreset", + "/realtime/realtimekit/core/api-reference/rtkpip", + "/realtime/realtimekit/core/api-reference/rtkplugin", + "/realtime/realtimekit/core/api-reference/rtkplugins", + "/realtime/realtimekit/core/api-reference/rtkpolls", + "/realtime/realtimekit/core/api-reference/rtkrecording", + "/realtime/realtimekit/core/api-reference/rtkself", + "/realtime/realtimekit/core/api-reference/rtkselfmedia", + "/realtime/realtimekit/core/api-reference/rtkstage", + "/realtime/realtimekit/core/api-reference/rtkstore", + "/realtime/realtimekit/core/api-reference/rtkthemepreset", + "/realtime/realtimekit/core/breakout-rooms", + "/realtime/realtimekit/core/chat", + "/realtime/realtimekit/core/display-active-speakers", + "/realtime/realtimekit/core/end-a-session", + "/realtime/realtimekit/core/error-codes", + "/realtime/realtimekit/core/local-participant", + "/realtime/realtimekit/core/manage-participants-in-a-session", + "/realtime/realtimekit/core/media-acquisition-approaches", + "/realtime/realtimekit/core/meeting-metadata", + "/realtime/realtimekit/core/meeting-object-explained", + "/realtime/realtimekit/core/plugins", + "/realtime/realtimekit/core/polls", + "/realtime/realtimekit/core/remote-participants", + "/realtime/realtimekit/core/remote-participants/events", + "/realtime/realtimekit/core/remote-participants/pip", + "/realtime/realtimekit/core/stage-management", + "/realtime/realtimekit/core/video-effects", + "/realtime/realtimekit/core/waiting-room", + "/realtime/realtimekit/custom-plugins", + "/realtime/realtimekit/custom-plugins/build-your-own-plugins", + "/realtime/realtimekit/faq", + "/realtime/realtimekit/legal", + "/realtime/realtimekit/legal/3rdparty", + "/realtime/realtimekit/pricing", + "/realtime/realtimekit/quickstart", + "/realtime/realtimekit/recording-guide", + "/realtime/realtimekit/recording-guide/add-watermark", + "/realtime/realtimekit/recording-guide/configure-audio-codec", + "/realtime/realtimekit/recording-guide/configure-codecs", + "/realtime/realtimekit/recording-guide/configure-realtimekit-bucket-config", + "/realtime/realtimekit/recording-guide/create-record-app-using-sdks", + "/realtime/realtimekit/recording-guide/custom-cloud-storage", + "/realtime/realtimekit/recording-guide/interactive-recording", + "/realtime/realtimekit/recording-guide/manage-recording-config-hierarchy", + "/realtime/realtimekit/recording-guide/monitor-status", + "/realtime/realtimekit/recording-guide/start-recording", + "/realtime/realtimekit/recording-guide/stop-recording", + "/realtime/realtimekit/recording-guide/track-recording", + "/realtime/realtimekit/release-notes", + "/realtime/realtimekit/release-notes/android-core", + "/realtime/realtimekit/release-notes/android-ui-kit", + "/realtime/realtimekit/release-notes/flutter-core", + "/realtime/realtimekit/release-notes/flutter-ui-kit", + "/realtime/realtimekit/release-notes/ios-core", + "/realtime/realtimekit/release-notes/ios-ui-kit", + "/realtime/realtimekit/release-notes/notice-board", + "/realtime/realtimekit/release-notes/react-native-core", + "/realtime/realtimekit/release-notes/react-native-ui-kit", + "/realtime/realtimekit/release-notes/web-ui-kit", + "/realtime/realtimekit/rest-api-reference", + "/realtime/realtimekit/sdk-selection", + "/realtime/realtimekit/ui-kit", + "/realtime/realtimekit/ui-kit/addons", + "/realtime/realtimekit/ui-kit/api-reference", + "/realtime/realtimekit/ui-kit/api-reference/android", + "/realtime/realtimekit/ui-kit/api-reference/android/audio-device-selector", + "/realtime/realtimekit/ui-kit/api-reference/android/avatar-view", + "/realtime/realtimekit/ui-kit/api-reference/android/button", + "/realtime/realtimekit/ui-kit/api-reference/android/camera-toggle", + "/realtime/realtimekit/ui-kit/api-reference/android/chat", + "/realtime/realtimekit/ui-kit/api-reference/android/clock-view", + "/realtime/realtimekit/ui-kit/api-reference/android/control-bar-button", + "/realtime/realtimekit/ui-kit/api-reference/android/control-bar-view", + "/realtime/realtimekit/ui-kit/api-reference/android/create-poll", + "/realtime/realtimekit/ui-kit/api-reference/android/design-tokens", + "/realtime/realtimekit/ui-kit/api-reference/android/error-view", + "/realtime/realtimekit/ui-kit/api-reference/android/grid-paginator", + "/realtime/realtimekit/ui-kit/api-reference/android/grid-view", + "/realtime/realtimekit/ui-kit/api-reference/android/header-view", + "/realtime/realtimekit/ui-kit/api-reference/android/join-button", + "/realtime/realtimekit/ui-kit/api-reference/android/join-livestream-button", + "/realtime/realtimekit/ui-kit/api-reference/android/join-stage-dialog", + "/realtime/realtimekit/ui-kit/api-reference/android/leave-button", + "/realtime/realtimekit/ui-kit/api-reference/android/leave-meeting-dialog", + "/realtime/realtimekit/ui-kit/api-reference/android/livestream-control-bar", + "/realtime/realtimekit/ui-kit/api-reference/android/livestream-header-view", + "/realtime/realtimekit/ui-kit/api-reference/android/livestream-indicator", + "/realtime/realtimekit/ui-kit/api-reference/android/livestream-toggle-button", + "/realtime/realtimekit/ui-kit/api-reference/android/livestream-viewer-count", + "/realtime/realtimekit/ui-kit/api-reference/android/loader-view", + "/realtime/realtimekit/ui-kit/api-reference/android/meeting-activity", + "/realtime/realtimekit/ui-kit/api-reference/android/meeting-control-bar", + "/realtime/realtimekit/ui-kit/api-reference/android/meeting-header", + "/realtime/realtimekit/ui-kit/api-reference/android/meeting-option-bottomsheet", + "/realtime/realtimekit/ui-kit/api-reference/android/meeting-title-view", + "/realtime/realtimekit/ui-kit/api-reference/android/mic-toggle", + "/realtime/realtimekit/ui-kit/api-reference/android/more-toggle-button", + "/realtime/realtimekit/ui-kit/api-reference/android/name-tag-view", + "/realtime/realtimekit/ui-kit/api-reference/android/participant-audio-indicator", + "/realtime/realtimekit/ui-kit/api-reference/android/participant-count-view", + "/realtime/realtimekit/ui-kit/api-reference/android/participant-tile-view", + "/realtime/realtimekit/ui-kit/api-reference/android/participant-video-indicator", + "/realtime/realtimekit/ui-kit/api-reference/android/participants", + "/realtime/realtimekit/ui-kit/api-reference/android/plugins", + "/realtime/realtimekit/ui-kit/api-reference/android/polls", + "/realtime/realtimekit/ui-kit/api-reference/android/recording-indicator", + "/realtime/realtimekit/ui-kit/api-reference/android/settings-bottomsheet", + "/realtime/realtimekit/ui-kit/api-reference/android/settings-fragment", + "/realtime/realtimekit/ui-kit/api-reference/android/setup-screen", + "/realtime/realtimekit/ui-kit/api-reference/android/tab-sync-toggle-button", + "/realtime/realtimekit/ui-kit/api-reference/android/video-device-selector", + "/realtime/realtimekit/ui-kit/api-reference/android/video-peer", + "/realtime/realtimekit/ui-kit/api-reference/android/webinar-control-bar", + "/realtime/realtimekit/ui-kit/api-reference/android/webinar-stage-toggle", + "/realtime/realtimekit/ui-kit/api-reference/angular", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-ai", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-ai-toggle", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-ai-transcriptions", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-audio-grid", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-audio-tile", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-audio-visualizer", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-avatar", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-breakout-room-manager", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-breakout-room-participants", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-breakout-rooms-manager", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-breakout-rooms-toggle", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-broadcast-message-modal", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-button", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-camera-selector", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-camera-toggle", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-caption-toggle", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-chat", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-chat-composer-ui", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-chat-composer-view", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-chat-header", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-chat-message", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-chat-messages-ui", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-chat-messages-ui-paginated", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-chat-search-results", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-chat-selector", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-chat-selector-ui", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-chat-toggle", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-clock", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-confirmation-modal", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-controlbar", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-controlbar-button", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-counter", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-debugger", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-debugger-audio", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-debugger-screenshare", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-debugger-system", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-debugger-toggle", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-debugger-video", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-dialog", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-dialog-manager", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-draft-attachment-view", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-emoji-picker", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-emoji-picker-button", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-ended-screen", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-file-dropzone", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-file-message", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-file-message-view", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-file-picker-button", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-fullscreen-toggle", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-grid", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-grid-pagination", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-header", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-icon", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-idle-screen", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-image-message", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-image-message-view", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-image-viewer", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-information-tooltip", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-join-stage", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-leave-button", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-leave-meeting", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-livestream-indicator", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-livestream-player", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-livestream-toggle", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-logo", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-markdown-view", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-meeting", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-meeting-title", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-menu", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-menu-item", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-menu-list", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-message-list-view", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-message-view", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-mic-toggle", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-microphone-selector", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-mixed-grid", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-more-toggle", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-mute-all-button", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-mute-all-confirmation", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-name-tag", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-network-indicator", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-notification", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-notifications", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-overlay-modal", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-paginated-list", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-participant", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-participant-count", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-participant-setup", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-participant-tile", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-participants", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-participants-audio", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-participants-stage-list", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-participants-stage-queue", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-participants-toggle", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-participants-viewer-list", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-participants-waiting-list", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-permissions-message", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-pinned-message-selector", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-pip-toggle", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-plugin-main", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-plugins", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-plugins-toggle", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-poll", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-poll-form", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-polls", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-polls-toggle", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-recording-indicator", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-recording-toggle", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-screen-share-toggle", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-screenshare-view", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-settings", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-settings-audio", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-settings-toggle", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-settings-video", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-setup-screen", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-sidebar", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-sidebar-ui", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-simple-grid", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-speaker-selector", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-spinner", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-spotlight-grid", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-spotlight-indicator", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-stage", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-stage-toggle", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-switch", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-tab-bar", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-text-composer-view", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-text-message", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-text-message-view", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-tooltip", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-transcript", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-transcripts", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-ui-provider", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-viewer-count", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-virtualized-participant-list", + "/realtime/realtimekit/ui-kit/api-reference/angular/rtk-waiting-screen", + "/realtime/realtimekit/ui-kit/api-reference/core", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-ai", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-ai-toggle", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-ai-transcriptions", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-audio-grid", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-audio-tile", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-audio-visualizer", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-avatar", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-breakout-room-manager", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-breakout-room-participants", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-breakout-rooms-manager", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-breakout-rooms-toggle", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-broadcast-message-modal", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-button", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-camera-selector", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-camera-toggle", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-caption-toggle", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-chat", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-chat-composer-ui", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-chat-composer-view", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-chat-header", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-chat-message", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-chat-messages-ui", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-chat-messages-ui-paginated", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-chat-search-results", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-chat-selector", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-chat-selector-ui", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-chat-toggle", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-clock", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-confirmation-modal", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-controlbar", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-controlbar-button", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-counter", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-debugger", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-debugger-audio", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-debugger-screenshare", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-debugger-system", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-debugger-toggle", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-debugger-video", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-dialog", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-dialog-manager", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-draft-attachment-view", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-emoji-picker", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-emoji-picker-button", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-ended-screen", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-file-dropzone", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-file-message", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-file-message-view", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-file-picker-button", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-fullscreen-toggle", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-grid", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-grid-pagination", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-header", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-icon", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-idle-screen", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-image-message", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-image-message-view", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-image-viewer", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-information-tooltip", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-join-stage", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-leave-button", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-leave-meeting", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-livestream-indicator", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-livestream-player", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-livestream-toggle", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-logo", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-markdown-view", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-meeting", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-meeting-title", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-menu", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-menu-item", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-menu-list", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-message-list-view", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-message-view", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-mic-toggle", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-microphone-selector", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-mixed-grid", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-more-toggle", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-mute-all-button", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-mute-all-confirmation", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-name-tag", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-network-indicator", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-notification", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-notifications", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-overlay-modal", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-paginated-list", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-participant", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-participant-count", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-participant-setup", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-participant-tile", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-participants", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-participants-audio", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-participants-stage-list", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-participants-stage-queue", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-participants-toggle", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-participants-viewer-list", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-participants-waiting-list", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-permissions-message", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-pinned-message-selector", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-pip-toggle", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-plugin-main", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-plugins", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-plugins-toggle", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-poll", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-poll-form", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-polls", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-polls-toggle", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-recording-indicator", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-recording-toggle", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-screen-share-toggle", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-screenshare-view", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-settings", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-settings-audio", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-settings-toggle", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-settings-video", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-setup-screen", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-sidebar", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-sidebar-ui", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-simple-grid", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-speaker-selector", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-spinner", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-spotlight-grid", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-spotlight-indicator", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-stage", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-stage-toggle", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-switch", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-tab-bar", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-text-composer-view", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-text-message", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-text-message-view", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-tooltip", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-transcript", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-transcripts", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-ui-provider", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-viewer-count", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-virtualized-participant-list", + "/realtime/realtimekit/ui-kit/api-reference/core/rtk-waiting-screen", + "/realtime/realtimekit/ui-kit/api-reference/flutter", + "/realtime/realtimekit/ui-kit/api-reference/flutter/rtk-audio-indicator-icon-widget", + "/realtime/realtimekit/ui-kit/api-reference/flutter/rtk-chat-icon-widget", + "/realtime/realtimekit/ui-kit/api-reference/flutter/rtk-join-button", + "/realtime/realtimekit/ui-kit/api-reference/flutter/rtk-leave-button", + "/realtime/realtimekit/ui-kit/api-reference/flutter/rtk-leave-meeting-dialog", + "/realtime/realtimekit/ui-kit/api-reference/flutter/rtk-meeting-title", + "/realtime/realtimekit/ui-kit/api-reference/flutter/rtk-name-tag-widget", + "/realtime/realtimekit/ui-kit/api-reference/flutter/rtk-participant-tile", + "/realtime/realtimekit/ui-kit/api-reference/flutter/rtk-participants-icon-widget", + "/realtime/realtimekit/ui-kit/api-reference/flutter/rtk-plugin-icon-widget", + "/realtime/realtimekit/ui-kit/api-reference/flutter/rtk-plugins-screen", + "/realtime/realtimekit/ui-kit/api-reference/flutter/rtk-polls-icon-widget", + "/realtime/realtimekit/ui-kit/api-reference/flutter/rtk-polls-screen", + "/realtime/realtimekit/ui-kit/api-reference/flutter/rtk-provider", + "/realtime/realtimekit/ui-kit/api-reference/flutter/rtk-self-audio-toggle", + "/realtime/realtimekit/ui-kit/api-reference/flutter/rtk-self-video-toggle", + "/realtime/realtimekit/ui-kit/api-reference/flutter/rtk-setup-screen-component", + "/realtime/realtimekit/ui-kit/api-reference/ios", + "/realtime/realtimekit/ui-kit/api-reference/ios/app-theme", + "/realtime/realtimekit/ui-kit/api-reference/ios/design-library", + "/realtime/realtimekit/ui-kit/api-reference/ios/grid-view", + "/realtime/realtimekit/ui-kit/api-reference/ios/meeting-view-controller", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-active-tab-selector-view", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-audio-button-control-bar", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-avatar-view", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-button", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-clock-view", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-control-bar", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-control-bar-button", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-end-meeting-control-bar-button", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-event-self-listener", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-image", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-join-button", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-label", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-leave-dialog", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-meeting-control-bar", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-meeting-header-view", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-meeting-name-tag", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-meeting-title-label", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-more-button-control-bar", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-more-menu", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-name-tag", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-navigation-bar", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-notification-badge-view", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-notification-config", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-participant-count-view", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-participant-tile-view", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-plugin-screen-share-tab-button", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-plugins-view", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-recording-view", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-setup-view-controller", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-stage-action-button-control-bar", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-switch-camera-button-control-bar", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-video-button-control-bar", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-video-view", + "/realtime/realtimekit/ui-kit/api-reference/ios/rtk-wait-list-participant-update-event-listener", + "/realtime/realtimekit/ui-kit/api-reference/react", + "/realtime/realtimekit/ui-kit/api-reference/react-native", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkaudiovisualizer", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkavatar", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkbutton", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkcameratoggle", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkchat", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkchattoggle", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkclock", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkcontrolbar", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkcontrolbarbutton", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkdialog", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkdialogmanager", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkendedscreen", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkfilemessage", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkgrid", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkgridpagination", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkheader", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkicon", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkidlescreen", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkimagemessage", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkimageviewer", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkjoinstage", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkleavebutton", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkleavemeeting", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtklivestreamindicator", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtklivestreamplayer", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtklivestreamstagetoggle", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtklivestreamtoggle", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtklivestreamviewercount", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtklogo", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkmeeting", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkmeetingtitle", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkmenu", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkmenuitem", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkmenulist", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkmictoggle", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkmixedgrid", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkmoretoggle", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkmutetoggle", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtknametag", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtknotification", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtknotifications", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkparticipant", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkparticipantcount", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkparticipants", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkparticipanttile", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkparticipanttoggle", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkparticipantwaiting", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkpermissionsmessage", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkpluginmain", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkplugins", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkpluginstoggle", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkpoll", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkpollform", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkpolls", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkpollstoggle", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkrecordingindicator", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkrecordingtoggle", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkscreensharetoggle", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkscreenshareview", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtksettings", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtksettingsaudio", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtksettingstoggle", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtksettingsvideo", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtksetupscreen", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtksidebar", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtksimplegrid", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkspinner", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkspotlightgrid", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtktext", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtktextfield", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkuiprovider", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkwaitingscreen", + "/realtime/realtimekit/ui-kit/api-reference/react-native/rtkwebinarstagetoggle", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkai", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkaitoggle", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkaitranscriptions", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkaudiogrid", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkaudiotile", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkaudiovisualizer", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkavatar", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkbreakoutroommanager", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkbreakoutroomparticipants", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkbreakoutroomsmanager", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkbreakoutroomstoggle", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkbroadcastmessagemodal", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkbutton", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkcameraselector", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkcameratoggle", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkcaptiontoggle", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkchat", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkchatcomposerui", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkchatcomposerview", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkchatheader", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkchatmessage", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkchatmessagesui", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkchatmessagesuipaginated", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkchatsearchresults", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkchatselector", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkchatselectorui", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkchattoggle", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkclock", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkconfirmationmodal", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkcontrolbar", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkcontrolbarbutton", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkcounter", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkdebugger", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkdebuggeraudio", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkdebuggerscreenshare", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkdebuggersystem", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkdebuggertoggle", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkdebuggervideo", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkdialog", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkdialogmanager", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkdraftattachmentview", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkemojipicker", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkemojipickerbutton", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkendedscreen", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkfiledropzone", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkfilemessage", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkfilemessageview", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkfilepickerbutton", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkfullscreentoggle", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkgrid", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkgridpagination", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkheader", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkicon", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkidlescreen", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkimagemessage", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkimagemessageview", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkimageviewer", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkinformationtooltip", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkjoinstage", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkleavebutton", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkleavemeeting", + "/realtime/realtimekit/ui-kit/api-reference/react/rtklivestreamindicator", + "/realtime/realtimekit/ui-kit/api-reference/react/rtklivestreamplayer", + "/realtime/realtimekit/ui-kit/api-reference/react/rtklivestreamtoggle", + "/realtime/realtimekit/ui-kit/api-reference/react/rtklogo", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkmarkdownview", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkmeeting", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkmeetingtitle", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkmenu", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkmenuitem", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkmenulist", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkmessagelistview", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkmessageview", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkmicrophoneselector", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkmictoggle", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkmixedgrid", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkmoretoggle", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkmuteallbutton", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkmuteallconfirmation", + "/realtime/realtimekit/ui-kit/api-reference/react/rtknametag", + "/realtime/realtimekit/ui-kit/api-reference/react/rtknetworkindicator", + "/realtime/realtimekit/ui-kit/api-reference/react/rtknotification", + "/realtime/realtimekit/ui-kit/api-reference/react/rtknotifications", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkoverlaymodal", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkpaginatedlist", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkparticipant", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkparticipantcount", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkparticipants", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkparticipantsaudio", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkparticipantsetup", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkparticipantsstagelist", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkparticipantsstagequeue", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkparticipantstoggle", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkparticipantsviewerlist", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkparticipantswaitinglist", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkparticipanttile", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkpermissionsmessage", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkpinnedmessageselector", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkpiptoggle", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkpluginmain", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkplugins", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkpluginstoggle", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkpoll", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkpollform", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkpolls", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkpollstoggle", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkrecordingindicator", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkrecordingtoggle", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkscreensharetoggle", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkscreenshareview", + "/realtime/realtimekit/ui-kit/api-reference/react/rtksettings", + "/realtime/realtimekit/ui-kit/api-reference/react/rtksettingsaudio", + "/realtime/realtimekit/ui-kit/api-reference/react/rtksettingstoggle", + "/realtime/realtimekit/ui-kit/api-reference/react/rtksettingsvideo", + "/realtime/realtimekit/ui-kit/api-reference/react/rtksetupscreen", + "/realtime/realtimekit/ui-kit/api-reference/react/rtksidebar", + "/realtime/realtimekit/ui-kit/api-reference/react/rtksidebarui", + "/realtime/realtimekit/ui-kit/api-reference/react/rtksimplegrid", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkspeakerselector", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkspinner", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkspotlightgrid", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkspotlightindicator", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkstage", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkstagetoggle", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkswitch", + "/realtime/realtimekit/ui-kit/api-reference/react/rtktabbar", + "/realtime/realtimekit/ui-kit/api-reference/react/rtktextcomposerview", + "/realtime/realtimekit/ui-kit/api-reference/react/rtktextmessage", + "/realtime/realtimekit/ui-kit/api-reference/react/rtktextmessageview", + "/realtime/realtimekit/ui-kit/api-reference/react/rtktooltip", + "/realtime/realtimekit/ui-kit/api-reference/react/rtktranscript", + "/realtime/realtimekit/ui-kit/api-reference/react/rtktranscripts", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkuiprovider", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkviewercount", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkvirtualizedparticipantlist", + "/realtime/realtimekit/ui-kit/api-reference/react/rtkwaitingscreen", + "/realtime/realtimekit/ui-kit/branding", + "/realtime/realtimekit/ui-kit/branding/design-system", + "/realtime/realtimekit/ui-kit/breakout-rooms", + "/realtime/realtimekit/ui-kit/build-your-own-ui", + "/realtime/realtimekit/ui-kit/component-library", + "/realtime/realtimekit/ui-kit/custom-controlbar", + "/realtime/realtimekit/ui-kit/custom-header", + "/realtime/realtimekit/ui-kit/meeting-locale", + "/realtime/realtimekit/ui-kit/state-management", + "/realtime/realtimekit/webhooks", + "/realtime/sfu", + "/realtime/sfu/calls-vs-sfus", + "/realtime/sfu/changelog", + "/realtime/sfu/datachannels", + "/realtime/sfu/demos", + "/realtime/sfu/example-architecture", + "/realtime/sfu/get-started", + "/realtime/sfu/https-api", + "/realtime/sfu/introduction", + "/realtime/sfu/limits", + "/realtime/sfu/media-transport-adapters", + "/realtime/sfu/media-transport-adapters/websocket-adapter", + "/realtime/sfu/pricing", + "/realtime/sfu/sessions-tracks", + "/realtime/sfu/simulcast", + "/realtime/turn", + "/realtime/turn/analytics", + "/realtime/turn/custom-domains", + "/realtime/turn/faq", + "/realtime/turn/generate-credentials", + "/realtime/turn/replacing-existing", + "/realtime/turn/rfc-matrix", + "/realtime/turn/what-is-turn", + "/reference-architecture", + "/reference-architecture/architectures", + "/reference-architecture/architectures/ai-security-for-apps", + "/reference-architecture/architectures/cdn", + "/reference-architecture/architectures/cloudflare-sase-with-crowdstrike", + "/reference-architecture/architectures/cloudflare-sase-with-microsoft", + "/reference-architecture/architectures/cloudflare-sase-with-sentinelone", + "/reference-architecture/architectures/email-security-deployments", + "/reference-architecture/architectures/load-balancing", + "/reference-architecture/architectures/magic-transit", + "/reference-architecture/architectures/multi-vendor", + "/reference-architecture/architectures/sase", + "/reference-architecture/architectures/security", + "/reference-architecture/by-solution", + "/reference-architecture/design-guides", + "/reference-architecture/design-guides/designing-ztna-access-policies", + "/reference-architecture/design-guides/extending-cloudflares-benefits-to-saas-providers-end-customers", + "/reference-architecture/design-guides/leveraging-cloudflare-for-your-saas-applications", + "/reference-architecture/design-guides/network-vpn-migration", + "/reference-architecture/design-guides/secure-application-delivery", + "/reference-architecture/design-guides/securing-guest-wireless-networks", + "/reference-architecture/design-guides/streamlined-waf-deployment-across-zones-and-applications", + "/reference-architecture/design-guides/zero-trust-for-saas", + "/reference-architecture/design-guides/zero-trust-for-startups", + "/reference-architecture/diagrams", + "/reference-architecture/diagrams/ai", + "/reference-architecture/diagrams/ai/ai-asset-creation", + "/reference-architecture/diagrams/ai/ai-composable", + "/reference-architecture/diagrams/ai/ai-multivendor-observability-control", + "/reference-architecture/diagrams/ai/ai-rag", + "/reference-architecture/diagrams/ai/ai-vibe-coding-platform", + "/reference-architecture/diagrams/ai/ai-video-caption", + "/reference-architecture/diagrams/ai/bigquery-workers-ai", + "/reference-architecture/diagrams/bots", + "/reference-architecture/diagrams/bots/bot-management", + "/reference-architecture/diagrams/content-delivery", + "/reference-architecture/diagrams/content-delivery/distributed-web-performance-architecture", + "/reference-architecture/diagrams/content-delivery/optimizing-image-delivery-with-cloudflare-image-resizing-and-r2", + "/reference-architecture/diagrams/iot", + "/reference-architecture/diagrams/iot/optimizing-and-securing-connected-transportation-systems", + "/reference-architecture/diagrams/network", + "/reference-architecture/diagrams/network/bring-your-own-ip-space-to-cloudflare", + "/reference-architecture/diagrams/network/optimizing-roaming-experience-with-geolocated-ips", + "/reference-architecture/diagrams/network/protect-data-center-networks", + "/reference-architecture/diagrams/network/protect-hybrid-cloud-networks-with-cloudflare-magic-transit", + "/reference-architecture/diagrams/network/protect-public-networks-with-cloudflare", + "/reference-architecture/diagrams/network/protecting-sp-networks-from-ddos", + "/reference-architecture/diagrams/sase", + "/reference-architecture/diagrams/sase/augment-access-with-serverless", + "/reference-architecture/diagrams/sase/cloudflare-one-appliance-deployment", + "/reference-architecture/diagrams/sase/deploying-self-hosted-voip-services-for-hybrid-users", + "/reference-architecture/diagrams/sase/gateway-dns-for-isp", + "/reference-architecture/diagrams/sase/gateway-for-protective-dns", + "/reference-architecture/diagrams/sase/sase-clientless-access-private-dns", + "/reference-architecture/diagrams/sase/secure-access-to-saas-applications-with-sase", + "/reference-architecture/diagrams/sase/zero-trust-and-virtual-desktop-infrastructure", + "/reference-architecture/diagrams/security", + "/reference-architecture/diagrams/security/fips-140-3", + "/reference-architecture/diagrams/security/securing-data-at-rest", + "/reference-architecture/diagrams/security/securing-data-in-transit", + "/reference-architecture/diagrams/security/securing-data-in-use", + "/reference-architecture/diagrams/serverless", + "/reference-architecture/diagrams/serverless/a-b-testing-using-workers", + "/reference-architecture/diagrams/serverless/fullstack-application", + "/reference-architecture/diagrams/serverless/programmable-platforms", + "/reference-architecture/diagrams/serverless/serverless-etl", + "/reference-architecture/diagrams/serverless/serverless-global-apis", + "/reference-architecture/diagrams/serverless/serverless-image-content-management", + "/reference-architecture/diagrams/storage", + "/reference-architecture/diagrams/storage/durable-object-control-data-plane-pattern", + "/reference-architecture/diagrams/storage/egress-free-storage-multi-cloud", + "/reference-architecture/diagrams/storage/event-notifications-for-storage", + "/reference-architecture/diagrams/storage/on-demand-object-storage-migration", + "/reference-architecture/diagrams/storage/storing-user-generated-content", + "/reference-architecture/how-to-use", + "/reference-architecture/implementation-guides", + "/reference-architecture/implementation-guides/application-security", + "/reference-architecture/implementation-guides/application-security/mtls", + "/reference-architecture/implementation-guides/zero-trust", + "/reference-architecture/implementation-guides/zero-trust/holistic-ai-security", + "/reference-architecture/implementation-guides/zero-trust/replace-vpn", + "/reference-architecture/implementation-guides/zero-trust/secure-internet-traffic", + "/reference-architecture/implementation-guides/zero-trust/secure-your-email", + "/reference-architecture/implementation-guides/zero-trust/ztna-web-access", + "/registrar", + "/registrar/about", + "/registrar/account-options", + "/registrar/account-options/domain-contact-updates", + "/registrar/account-options/domain-management", + "/registrar/account-options/domain-ownership-certificate", + "/registrar/account-options/icloud-domains", + "/registrar/account-options/inter-account-transfer", + "/registrar/account-options/renew-domains", + "/registrar/account-options/transfer-out-from-cloudflare", + "/registrar/account-options/whois-redaction", + "/registrar/api-reference", + "/registrar/custom-domain-protection", + "/registrar/faq", + "/registrar/get-started", + "/registrar/get-started/enable-dnssec", + "/registrar/get-started/register-domain", + "/registrar/get-started/transfer-domain-to-cloudflare", + "/registrar/registrar-api", + "/registrar/top-level-domains", + "/registrar/top-level-domains/uk-domains", + "/registrar/top-level-domains/us-domains", + "/registrar/troubleshooting", + "/registrar/whoisrequests", + "/resource-tagging", + "/resource-tagging/get-started", + "/resource-tagging/how-to", + "/resource-tagging/how-to/filter-resources", + "/resource-tagging/how-to/manage-tags", + "/resource-tagging/reference", + "/resource-tagging/reference/error-codes", + "/resource-tagging/reference/limits", + "/resource-tagging/reference/resource-types", + "/resources", + "/rules", + "/rules/changelog", + "/rules/cloud-connector", + "/rules/cloud-connector/create-api", + "/rules/cloud-connector/create-dashboard", + "/rules/cloud-connector/create-terraform", + "/rules/cloud-connector/examples", + "/rules/cloud-connector/examples/route-images-to-aws-s3-using-terraform", + "/rules/cloud-connector/examples/route-images-to-s3", + "/rules/cloud-connector/examples/send-eu-visitors-to-gcs", + "/rules/cloud-connector/examples/serve-static-assets-from-azure", + "/rules/cloud-connector/providers", + "/rules/compression-rules", + "/rules/compression-rules/create-api", + "/rules/compression-rules/create-dashboard", + "/rules/compression-rules/examples", + "/rules/compression-rules/examples/disable-all-brotli", + "/rules/compression-rules/examples/disable-compression-avif", + "/rules/compression-rules/examples/enable-zstandard", + "/rules/compression-rules/examples/gzip-for-csv", + "/rules/compression-rules/examples/only-brotli-url-path", + "/rules/compression-rules/settings", + "/rules/configuration-rules", + "/rules/configuration-rules/create-api", + "/rules/configuration-rules/create-dashboard", + "/rules/configuration-rules/examples", + "/rules/configuration-rules/examples/define-single-configuration-terraform", + "/rules/configuration-rules/settings", + "/rules/custom-errors", + "/rules/custom-errors/api-calls", + "/rules/custom-errors/create-rules", + "/rules/custom-errors/edit-error-pages", + "/rules/custom-errors/example-rules", + "/rules/custom-errors/reference", + "/rules/custom-errors/reference/error-page-types", + "/rules/custom-errors/reference/error-tokens", + "/rules/custom-errors/reference/parameters", + "/rules/custom-errors/troubleshooting", + "/rules/examples", + "/rules/link-cache-rules", + "/rules/normalization", + "/rules/normalization/examples", + "/rules/normalization/how-it-works", + "/rules/normalization/manage", + "/rules/normalization/settings", + "/rules/origin-rules", + "/rules/origin-rules/create-api", + "/rules/origin-rules/create-dashboard", + "/rules/origin-rules/examples", + "/rules/origin-rules/examples/change-http-host-header", + "/rules/origin-rules/examples/change-port", + "/rules/origin-rules/examples/define-single-origin-terraform", + "/rules/origin-rules/faq", + "/rules/origin-rules/features", + "/rules/origin-rules/parameters", + "/rules/origin-rules/tutorials", + "/rules/origin-rules/tutorials/change-uri-path-and-host-header", + "/rules/origin-rules/tutorials/point-to-pages-with-custom-domain", + "/rules/origin-rules/tutorials/point-to-r2-bucket-with-custom-domain", + "/rules/page-rules", + "/rules/page-rules/how-to", + "/rules/page-rules/how-to/url-forwarding", + "/rules/page-rules/manage", + "/rules/page-rules/reference", + "/rules/page-rules/reference/additional-reference", + "/rules/page-rules/reference/recommended-rules", + "/rules/page-rules/reference/settings", + "/rules/page-rules/reference/wildcard-matching", + "/rules/page-rules/troubleshooting", + "/rules/page-rules/troubleshooting/billing-and-subscription", + "/rules/page-rules/troubleshooting/general", + "/rules/reference", + "/rules/reference/link-rule-language", + "/rules/reference/page-rules-migration", + "/rules/reference/troubleshooting", + "/rules/snippets", + "/rules/snippets/create-api", + "/rules/snippets/create-dashboard", + "/rules/snippets/create-terraform", + "/rules/snippets/errors", + "/rules/snippets/examples", + "/rules/snippets/examples/ab-testing-same-url", + "/rules/snippets/examples/append-dates-to-cookies", + "/rules/snippets/examples/auth-with-headers", + "/rules/snippets/examples/bot-data-to-origin", + "/rules/snippets/examples/bots-to-honeypot", + "/rules/snippets/examples/bulk-redirect-map", + "/rules/snippets/examples/country-code-redirect", + "/rules/snippets/examples/custom-cache", + "/rules/snippets/examples/debugging-logs", + "/rules/snippets/examples/define-cors-headers", + "/rules/snippets/examples/follow-redirects", + "/rules/snippets/examples/hex-timestamp", + "/rules/snippets/examples/jwt-validation", + "/rules/snippets/examples/maintenance", + "/rules/snippets/examples/override-set-cookies-value", + "/rules/snippets/examples/redirect-forbidden-status", + "/rules/snippets/examples/redirect-replaced-domain", + "/rules/snippets/examples/remove-fields-api-response", + "/rules/snippets/examples/remove-query-strings", + "/rules/snippets/examples/remove-response-headers", + "/rules/snippets/examples/return-incoming-request-properties", + "/rules/snippets/examples/rewrite-site-links", + "/rules/snippets/examples/route-and-rewrite", + "/rules/snippets/examples/security-headers", + "/rules/snippets/examples/send-timestamp-to-origin", + "/rules/snippets/examples/serve-different-origin", + "/rules/snippets/examples/signing-requests", + "/rules/snippets/examples/slow-suspicious-requests", + "/rules/snippets/how-it-works", + "/rules/snippets/when-to-use", + "/rules/trace-request", + "/rules/trace-request/changelog", + "/rules/trace-request/how-to", + "/rules/trace-request/limitations", + "/rules/transform", + "/rules/transform/examples", + "/rules/transform/examples/add-cors-header", + "/rules/transform/examples/add-request-header-bot-score", + "/rules/transform/examples/add-request-header-static-value", + "/rules/transform/examples/add-request-header-subrequest-other-zone", + "/rules/transform/examples/add-response-header-static-value", + "/rules/transform/examples/normalize-encoded-slash", + "/rules/transform/examples/remove-request-header", + "/rules/transform/examples/remove-response-header", + "/rules/transform/examples/rewrite-archive-urls-new-format", + "/rules/transform/examples/rewrite-moved-section", + "/rules/transform/examples/rewrite-path-archived-posts", + "/rules/transform/examples/rewrite-path-object-storage", + "/rules/transform/examples/rewrite-several-url-different-url", + "/rules/transform/examples/rewrite-url-string-visitors", + "/rules/transform/examples/rewrite-welcome-for-countries", + "/rules/transform/examples/set-response-header-bot-score", + "/rules/transform/examples/set-response-header-static-value", + "/rules/transform/managed-transforms", + "/rules/transform/managed-transforms/configure", + "/rules/transform/managed-transforms/reference", + "/rules/transform/request-header-modification", + "/rules/transform/request-header-modification/create-api", + "/rules/transform/request-header-modification/create-dashboard", + "/rules/transform/request-header-modification/link-create-terraform", + "/rules/transform/request-header-modification/reference", + "/rules/transform/request-header-modification/reference/fields-functions", + "/rules/transform/request-header-modification/reference/header-format", + "/rules/transform/request-header-modification/reference/parameters", + "/rules/transform/response-header-modification", + "/rules/transform/response-header-modification/create-api", + "/rules/transform/response-header-modification/create-dashboard", + "/rules/transform/response-header-modification/link-create-terraform", + "/rules/transform/response-header-modification/reference", + "/rules/transform/response-header-modification/reference/fields-functions", + "/rules/transform/response-header-modification/reference/header-format", + "/rules/transform/response-header-modification/reference/parameters", + "/rules/transform/troubleshooting", + "/rules/transform/url-rewrite", + "/rules/transform/url-rewrite/create-api", + "/rules/transform/url-rewrite/create-dashboard", + "/rules/transform/url-rewrite/link-create-terraform", + "/rules/transform/url-rewrite/reference", + "/rules/transform/url-rewrite/reference/fields-functions", + "/rules/transform/url-rewrite/reference/parameters", + "/rules/url-forwarding", + "/rules/url-forwarding/bulk-redirects", + "/rules/url-forwarding/bulk-redirects/concepts", + "/rules/url-forwarding/bulk-redirects/create-api", + "/rules/url-forwarding/bulk-redirects/create-dashboard", + "/rules/url-forwarding/bulk-redirects/faq", + "/rules/url-forwarding/bulk-redirects/how-it-works", + "/rules/url-forwarding/bulk-redirects/reference", + "/rules/url-forwarding/bulk-redirects/reference/csv-file-format", + "/rules/url-forwarding/bulk-redirects/reference/fields-functions", + "/rules/url-forwarding/bulk-redirects/reference/json-objects", + "/rules/url-forwarding/bulk-redirects/reference/parameters", + "/rules/url-forwarding/bulk-redirects/reference/url-components", + "/rules/url-forwarding/bulk-redirects/terraform-example", + "/rules/url-forwarding/examples", + "/rules/url-forwarding/examples/perform-mobile-redirects", + "/rules/url-forwarding/examples/redirect-admin-https", + "/rules/url-forwarding/examples/redirect-all-another-domain", + "/rules/url-forwarding/examples/redirect-all-country", + "/rules/url-forwarding/examples/redirect-all-different-domain-root", + "/rules/url-forwarding/examples/redirect-all-different-hostname", + "/rules/url-forwarding/examples/redirect-country-subdomains", + "/rules/url-forwarding/examples/redirect-new-url", + "/rules/url-forwarding/examples/redirect-root-to-www", + "/rules/url-forwarding/examples/redirect-www-to-root", + "/rules/url-forwarding/examples/remove-locale-url", + "/rules/url-forwarding/single-redirects", + "/rules/url-forwarding/single-redirects/create-api", + "/rules/url-forwarding/single-redirects/create-dashboard", + "/rules/url-forwarding/single-redirects/settings", + "/rules/url-forwarding/single-redirects/terraform-example", + "/ruleset-engine", + "/ruleset-engine/about", + "/ruleset-engine/about/phases", + "/ruleset-engine/about/rules", + "/ruleset-engine/about/rulesets", + "/ruleset-engine/basic-operations", + "/ruleset-engine/basic-operations/add-rule-phase-rulesets", + "/ruleset-engine/basic-operations/deploy-rulesets", + "/ruleset-engine/basic-operations/view-rulesets", + "/ruleset-engine/custom-rulesets", + "/ruleset-engine/custom-rulesets/add-rules-ruleset", + "/ruleset-engine/custom-rulesets/create-custom-ruleset", + "/ruleset-engine/custom-rulesets/deploy-custom-ruleset", + "/ruleset-engine/managed-rulesets", + "/ruleset-engine/managed-rulesets/create-exception", + "/ruleset-engine/managed-rulesets/deploy-managed-ruleset", + "/ruleset-engine/managed-rulesets/override-examples", + "/ruleset-engine/managed-rulesets/override-examples/deploy-cmr-joomla-only", + "/ruleset-engine/managed-rulesets/override-examples/deploy-cmr-wordpress-block", + "/ruleset-engine/managed-rulesets/override-examples/enable-selected-rules", + "/ruleset-engine/managed-rulesets/override-examples/link-override-ddos-l34-rule-sensitivity", + "/ruleset-engine/managed-rulesets/override-examples/override-ddos-rule-sensitivity", + "/ruleset-engine/managed-rulesets/override-examples/override-ruleset-tag-rule", + "/ruleset-engine/managed-rulesets/override-managed-ruleset", + "/ruleset-engine/reference", + "/ruleset-engine/reference/phases-list", + "/ruleset-engine/rules-language", + "/ruleset-engine/rules-language/actions", + "/ruleset-engine/rules-language/expressions", + "/ruleset-engine/rules-language/expressions/edit-expressions", + "/ruleset-engine/rules-language/fields", + "/ruleset-engine/rules-language/fields/reference", + "/ruleset-engine/rules-language/fields/reference/cf.api_gateway.auth_id_present", + "/ruleset-engine/rules-language/fields/reference/cf.api_gateway.fallthrough_detected", + "/ruleset-engine/rules-language/fields/reference/cf.api_gateway.request_violates_schema", + "/ruleset-engine/rules-language/fields/reference/cf.bot_management.corporate_proxy", + "/ruleset-engine/rules-language/fields/reference/cf.bot_management.detection_ids", + "/ruleset-engine/rules-language/fields/reference/cf.bot_management.ja3_hash", + "/ruleset-engine/rules-language/fields/reference/cf.bot_management.ja4", + "/ruleset-engine/rules-language/fields/reference/cf.bot_management.js_detection.passed", + "/ruleset-engine/rules-language/fields/reference/cf.bot_management.score", + "/ruleset-engine/rules-language/fields/reference/cf.bot_management.signed_agent", + "/ruleset-engine/rules-language/fields/reference/cf.bot_management.static_resource", + "/ruleset-engine/rules-language/fields/reference/cf.bot_management.verified_bot", + "/ruleset-engine/rules-language/fields/reference/cf.client.bot", + "/ruleset-engine/rules-language/fields/reference/cf.edge.client_tcp", + "/ruleset-engine/rules-language/fields/reference/cf.edge.l4.delivery_rate", + "/ruleset-engine/rules-language/fields/reference/cf.edge.server_ip", + "/ruleset-engine/rules-language/fields/reference/cf.edge.server_port", + "/ruleset-engine/rules-language/fields/reference/cf.hostname.metadata", + "/ruleset-engine/rules-language/fields/reference/cf.llm.prompt.custom_topic_categories", + "/ruleset-engine/rules-language/fields/reference/cf.llm.prompt.detected", + "/ruleset-engine/rules-language/fields/reference/cf.llm.prompt.injection_score", + "/ruleset-engine/rules-language/fields/reference/cf.llm.prompt.pii_categories", + "/ruleset-engine/rules-language/fields/reference/cf.llm.prompt.pii_detected", + "/ruleset-engine/rules-language/fields/reference/cf.llm.prompt.token_count", + "/ruleset-engine/rules-language/fields/reference/cf.llm.prompt.unsafe_topic_categories", + "/ruleset-engine/rules-language/fields/reference/cf.llm.prompt.unsafe_topic_detected", + "/ruleset-engine/rules-language/fields/reference/cf.random_seed", + "/ruleset-engine/rules-language/fields/reference/cf.ray_id", + "/ruleset-engine/rules-language/fields/reference/cf.response.1xxx_code", + "/ruleset-engine/rules-language/fields/reference/cf.response.error_type", + "/ruleset-engine/rules-language/fields/reference/cf.threat_score", + "/ruleset-engine/rules-language/fields/reference/cf.timings.client_quic_rtt_msec", + "/ruleset-engine/rules-language/fields/reference/cf.timings.client_tcp_rtt_msec", + "/ruleset-engine/rules-language/fields/reference/cf.timings.edge_msec", + "/ruleset-engine/rules-language/fields/reference/cf.timings.origin_ttfb_msec", + "/ruleset-engine/rules-language/fields/reference/cf.timings.worker_msec", + "/ruleset-engine/rules-language/fields/reference/cf.tls_cipher", + "/ruleset-engine/rules-language/fields/reference/cf.tls_ciphers_sha1", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_auth.cert_chain_rfc9440", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_auth.cert_chain_rfc9440_too_large", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_auth.cert_fingerprint_sha1", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_auth.cert_fingerprint_sha256", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_auth.cert_issuer_dn", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_auth.cert_issuer_dn_legacy", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_auth.cert_issuer_dn_rfc2253", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_auth.cert_issuer_serial", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_auth.cert_issuer_ski", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_auth.cert_not_after", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_auth.cert_not_before", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_auth.cert_presented", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_auth.cert_revoked", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_auth.cert_rfc9440", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_auth.cert_rfc9440_too_large", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_auth.cert_serial", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_auth.cert_ski", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_auth.cert_subject_dn", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_auth.cert_subject_dn_legacy", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_auth.cert_subject_dn_rfc2253", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_auth.cert_verified", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_extensions_sha1", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_extensions_sha1_le", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_hello_length", + "/ruleset-engine/rules-language/fields/reference/cf.tls_client_random", + "/ruleset-engine/rules-language/fields/reference/cf.tls_version", + "/ruleset-engine/rules-language/fields/reference/cf.verified_bot_category", + "/ruleset-engine/rules-language/fields/reference/cf.waf.auth_detected", + "/ruleset-engine/rules-language/fields/reference/cf.waf.content_scan.has_failed", + "/ruleset-engine/rules-language/fields/reference/cf.waf.content_scan.has_malicious_obj", + "/ruleset-engine/rules-language/fields/reference/cf.waf.content_scan.has_obj", + "/ruleset-engine/rules-language/fields/reference/cf.waf.content_scan.num_malicious_obj", + "/ruleset-engine/rules-language/fields/reference/cf.waf.content_scan.num_obj", + "/ruleset-engine/rules-language/fields/reference/cf.waf.content_scan.obj_results", + "/ruleset-engine/rules-language/fields/reference/cf.waf.content_scan.obj_sizes", + "/ruleset-engine/rules-language/fields/reference/cf.waf.content_scan.obj_types", + "/ruleset-engine/rules-language/fields/reference/cf.waf.credential_check.password_leaked", + "/ruleset-engine/rules-language/fields/reference/cf.waf.credential_check.username_and_password_leaked", + "/ruleset-engine/rules-language/fields/reference/cf.waf.credential_check.username_leaked", + "/ruleset-engine/rules-language/fields/reference/cf.waf.credential_check.username_password_similar", + "/ruleset-engine/rules-language/fields/reference/cf.waf.score", + "/ruleset-engine/rules-language/fields/reference/cf.waf.score.class", + "/ruleset-engine/rules-language/fields/reference/cf.waf.score.rce", + "/ruleset-engine/rules-language/fields/reference/cf.waf.score.sqli", + "/ruleset-engine/rules-language/fields/reference/cf.waf.score.xss", + "/ruleset-engine/rules-language/fields/reference/cf.worker.upstream_zone", + "/ruleset-engine/rules-language/fields/reference/http.cookie", + "/ruleset-engine/rules-language/fields/reference/http.host", + "/ruleset-engine/rules-language/fields/reference/http.referer", + "/ruleset-engine/rules-language/fields/reference/http.request.accepted_languages", + "/ruleset-engine/rules-language/fields/reference/http.request.body.form", + "/ruleset-engine/rules-language/fields/reference/http.request.body.form.names", + "/ruleset-engine/rules-language/fields/reference/http.request.body.form.values", + "/ruleset-engine/rules-language/fields/reference/http.request.body.mime", + "/ruleset-engine/rules-language/fields/reference/http.request.body.multipart", + "/ruleset-engine/rules-language/fields/reference/http.request.body.multipart.content_dispositions", + "/ruleset-engine/rules-language/fields/reference/http.request.body.multipart.content_transfer_encodings", + "/ruleset-engine/rules-language/fields/reference/http.request.body.multipart.content_types", + "/ruleset-engine/rules-language/fields/reference/http.request.body.multipart.filenames", + "/ruleset-engine/rules-language/fields/reference/http.request.body.multipart.names", + "/ruleset-engine/rules-language/fields/reference/http.request.body.multipart.values", + "/ruleset-engine/rules-language/fields/reference/http.request.body.raw", + "/ruleset-engine/rules-language/fields/reference/http.request.body.size", + "/ruleset-engine/rules-language/fields/reference/http.request.body.truncated", + "/ruleset-engine/rules-language/fields/reference/http.request.cookies", + "/ruleset-engine/rules-language/fields/reference/http.request.full_uri", + "/ruleset-engine/rules-language/fields/reference/http.request.headers", + "/ruleset-engine/rules-language/fields/reference/http.request.headers.names", + "/ruleset-engine/rules-language/fields/reference/http.request.headers.truncated", + "/ruleset-engine/rules-language/fields/reference/http.request.headers.values", + "/ruleset-engine/rules-language/fields/reference/http.request.jwt.claims.aud", + "/ruleset-engine/rules-language/fields/reference/http.request.jwt.claims.aud.names", + "/ruleset-engine/rules-language/fields/reference/http.request.jwt.claims.aud.values", + "/ruleset-engine/rules-language/fields/reference/http.request.jwt.claims.iat.sec", + "/ruleset-engine/rules-language/fields/reference/http.request.jwt.claims.iat.sec.names", + "/ruleset-engine/rules-language/fields/reference/http.request.jwt.claims.iat.sec.values", + "/ruleset-engine/rules-language/fields/reference/http.request.jwt.claims.iss", + "/ruleset-engine/rules-language/fields/reference/http.request.jwt.claims.iss.names", + "/ruleset-engine/rules-language/fields/reference/http.request.jwt.claims.iss.values", + "/ruleset-engine/rules-language/fields/reference/http.request.jwt.claims.jti", + "/ruleset-engine/rules-language/fields/reference/http.request.jwt.claims.jti.names", + "/ruleset-engine/rules-language/fields/reference/http.request.jwt.claims.jti.values", + "/ruleset-engine/rules-language/fields/reference/http.request.jwt.claims.nbf.sec", + "/ruleset-engine/rules-language/fields/reference/http.request.jwt.claims.nbf.sec.names", + "/ruleset-engine/rules-language/fields/reference/http.request.jwt.claims.nbf.sec.values", + "/ruleset-engine/rules-language/fields/reference/http.request.jwt.claims.sub", + "/ruleset-engine/rules-language/fields/reference/http.request.jwt.claims.sub.names", + "/ruleset-engine/rules-language/fields/reference/http.request.jwt.claims.sub.values", + "/ruleset-engine/rules-language/fields/reference/http.request.method", + "/ruleset-engine/rules-language/fields/reference/http.request.timestamp.msec", + "/ruleset-engine/rules-language/fields/reference/http.request.timestamp.sec", + "/ruleset-engine/rules-language/fields/reference/http.request.uri", + "/ruleset-engine/rules-language/fields/reference/http.request.uri.args", + "/ruleset-engine/rules-language/fields/reference/http.request.uri.args.names", + "/ruleset-engine/rules-language/fields/reference/http.request.uri.args.values", + "/ruleset-engine/rules-language/fields/reference/http.request.uri.path", + "/ruleset-engine/rules-language/fields/reference/http.request.uri.path.extension", + "/ruleset-engine/rules-language/fields/reference/http.request.uri.query", + "/ruleset-engine/rules-language/fields/reference/http.request.version", + "/ruleset-engine/rules-language/fields/reference/http.response.code", + "/ruleset-engine/rules-language/fields/reference/http.response.content_type.media_type", + "/ruleset-engine/rules-language/fields/reference/http.response.headers", + "/ruleset-engine/rules-language/fields/reference/http.response.headers.names", + "/ruleset-engine/rules-language/fields/reference/http.response.headers.values", + "/ruleset-engine/rules-language/fields/reference/http.user_agent", + "/ruleset-engine/rules-language/fields/reference/http.x_forwarded_for", + "/ruleset-engine/rules-language/fields/reference/ip.src", + "/ruleset-engine/rules-language/fields/reference/ip.src.asnum", + "/ruleset-engine/rules-language/fields/reference/ip.src.city", + "/ruleset-engine/rules-language/fields/reference/ip.src.continent", + "/ruleset-engine/rules-language/fields/reference/ip.src.country", + "/ruleset-engine/rules-language/fields/reference/ip.src.is_in_european_union", + "/ruleset-engine/rules-language/fields/reference/ip.src.lat", + "/ruleset-engine/rules-language/fields/reference/ip.src.lon", + "/ruleset-engine/rules-language/fields/reference/ip.src.metro_code", + "/ruleset-engine/rules-language/fields/reference/ip.src.postal_code", + "/ruleset-engine/rules-language/fields/reference/ip.src.region", + "/ruleset-engine/rules-language/fields/reference/ip.src.region_code", + "/ruleset-engine/rules-language/fields/reference/ip.src.subdivision_1_iso_code", + "/ruleset-engine/rules-language/fields/reference/ip.src.subdivision_2_iso_code", + "/ruleset-engine/rules-language/fields/reference/ip.src.timezone.name", + "/ruleset-engine/rules-language/fields/reference/raw.http.request.full_uri", + "/ruleset-engine/rules-language/fields/reference/raw.http.request.uri", + "/ruleset-engine/rules-language/fields/reference/raw.http.request.uri.args", + "/ruleset-engine/rules-language/fields/reference/raw.http.request.uri.args.names", + "/ruleset-engine/rules-language/fields/reference/raw.http.request.uri.args.values", + "/ruleset-engine/rules-language/fields/reference/raw.http.request.uri.path", + "/ruleset-engine/rules-language/fields/reference/raw.http.request.uri.path.extension", + "/ruleset-engine/rules-language/fields/reference/raw.http.request.uri.query", + "/ruleset-engine/rules-language/fields/reference/raw.http.response.headers", + "/ruleset-engine/rules-language/fields/reference/raw.http.response.headers.names", + "/ruleset-engine/rules-language/fields/reference/raw.http.response.headers.values", + "/ruleset-engine/rules-language/fields/reference/ssl", + "/ruleset-engine/rules-language/functions", + "/ruleset-engine/rules-language/operators", + "/ruleset-engine/rules-language/values", + "/ruleset-engine/rulesets-api", + "/ruleset-engine/rulesets-api/add-rule", + "/ruleset-engine/rulesets-api/create", + "/ruleset-engine/rulesets-api/delete", + "/ruleset-engine/rulesets-api/delete-rule", + "/ruleset-engine/rulesets-api/endpoints", + "/ruleset-engine/rulesets-api/json-object", + "/ruleset-engine/rulesets-api/update", + "/ruleset-engine/rulesets-api/update-rule", + "/ruleset-engine/rulesets-api/view", + "/sandbox", + "/sandbox/api", + "/sandbox/api/backups", + "/sandbox/api/commands", + "/sandbox/api/file-watching", + "/sandbox/api/files", + "/sandbox/api/interpreter", + "/sandbox/api/lifecycle", + "/sandbox/api/ports", + "/sandbox/api/sessions", + "/sandbox/api/storage", + "/sandbox/api/terminal", + "/sandbox/api/tunnels", + "/sandbox/bridge", + "/sandbox/bridge/http-api", + "/sandbox/concepts", + "/sandbox/concepts/architecture", + "/sandbox/concepts/containers", + "/sandbox/concepts/preview-urls", + "/sandbox/concepts/sandboxes", + "/sandbox/concepts/security", + "/sandbox/concepts/sessions", + "/sandbox/concepts/terminal", + "/sandbox/configuration", + "/sandbox/configuration/dockerfile", + "/sandbox/configuration/environment-variables", + "/sandbox/configuration/sandbox-options", + "/sandbox/configuration/transport", + "/sandbox/configuration/wrangler", + "/sandbox/get-started", + "/sandbox/guides", + "/sandbox/guides/2026-deprecation", + "/sandbox/guides/background-processes", + "/sandbox/guides/backup-restore", + "/sandbox/guides/browser-terminals", + "/sandbox/guides/code-execution", + "/sandbox/guides/docker-in-docker", + "/sandbox/guides/execute-commands", + "/sandbox/guides/expose-services", + "/sandbox/guides/file-watching", + "/sandbox/guides/git-workflows", + "/sandbox/guides/manage-files", + "/sandbox/guides/mount-buckets", + "/sandbox/guides/outbound-traffic", + "/sandbox/guides/production-deployment", + "/sandbox/guides/proxy-requests", + "/sandbox/guides/streaming-output", + "/sandbox/guides/websocket-connections", + "/sandbox/guides/workers-connections", + "/sandbox/platform", + "/sandbox/platform/limits", + "/sandbox/platform/pricing", + "/sandbox/tutorials", + "/sandbox/tutorials/ai-code-executor", + "/sandbox/tutorials/analyze-data-with-ai", + "/sandbox/tutorials/automated-testing-pipeline", + "/sandbox/tutorials/claude-code", + "/sandbox/tutorials/claude-managed-agents", + "/sandbox/tutorials/code-review-bot", + "/sandbox/tutorials/openai-agents", + "/sandbox/tutorials/persistent-storage", + "/sandbox/tutorials/workers-ai-code-interpreter", + "/secrets-store", + "/secrets-store/access-control", + "/secrets-store/api", + "/secrets-store/audit-logs", + "/secrets-store/integrations", + "/secrets-store/integrations/ai-gateway", + "/secrets-store/integrations/workers", + "/secrets-store/manage-secrets", + "/secrets-store/manage-secrets/how-to", + "/security", + "/security-center", + "/security-center/brand-protection", + "/security-center/changelog", + "/security-center/cloudforce-one", + "/security-center/cloudforce-one/cloudforce-one", + "/security-center/cloudforce-one/open-port-scanning", + "/security-center/get-started", + "/security-center/indicator-feeds", + "/security-center/infrastructure", + "/security-center/infrastructure/security-file", + "/security-center/intel-apis", + "/security-center/intel-apis/limits", + "/security-center/intel-apis/manage-miscategorization-reports", + "/security-center/investigate", + "/security-center/investigate/change-categorization", + "/security-center/investigate/investigate-threats", + "/security-center/investigate/scan-limits", + "/security/analytics", + "/security/overview", + "/security/rules", + "/security/security-insights", + "/security/security-insights/how-it-works", + "/security/security-insights/review-insights", + "/security/security-insights/roles-and-permissions", + "/security/settings", + "/security/web-assets", + "/smart-shield", + "/smart-shield/concepts", + "/smart-shield/concepts/connection-reuse", + "/smart-shield/concepts/network-diagram", + "/smart-shield/configuration", + "/smart-shield/configuration/argo", + "/smart-shield/configuration/cache-reserve", + "/smart-shield/configuration/cache-reserve/analytics", + "/smart-shield/configuration/cache-reserve/operations", + "/smart-shield/configuration/dedicated-egress-ips", + "/smart-shield/configuration/dedicated-egress-ips/how-it-works", + "/smart-shield/configuration/dedicated-egress-ips/how-it-works/connection-forwarding", + "/smart-shield/configuration/dedicated-egress-ips/how-it-works/egress-ips", + "/smart-shield/configuration/dedicated-egress-ips/ips-utilization", + "/smart-shield/configuration/dedicated-egress-ips/other-products", + "/smart-shield/configuration/dedicated-egress-ips/setup", + "/smart-shield/configuration/health-checks", + "/smart-shield/configuration/health-checks/analytics", + "/smart-shield/configuration/health-checks/setup", + "/smart-shield/configuration/health-checks/zone-lockdown", + "/smart-shield/configuration/regional-tiered-cache", + "/smart-shield/configuration/smart-tiered-cache", + "/smart-shield/get-started", + "/spectrum", + "/spectrum/about", + "/spectrum/about/byoip", + "/spectrum/about/ddos-for-spectrum", + "/spectrum/about/ftp", + "/spectrum/about/load-balancer", + "/spectrum/about/static-ip", + "/spectrum/get-started", + "/spectrum/glossary", + "/spectrum/how-to", + "/spectrum/how-to/enable-proxy-protocol", + "/spectrum/protocols-per-plan", + "/spectrum/reference", + "/spectrum/reference/analytics", + "/spectrum/reference/configuration-options", + "/spectrum/reference/error-codes", + "/spectrum/reference/layer-7-analytics", + "/spectrum/reference/limitations", + "/spectrum/reference/logs", + "/spectrum/reference/settings-by-plan", + "/spectrum/reference/simple-proxy-protocol-header", + "/spectrum/reference/troubleshooting", + "/speed", + "/speed/aim", + "/speed/glossary", + "/speed/observatory", + "/speed/observatory/dashboard", + "/speed/observatory/faq", + "/speed/observatory/rum-beacon", + "/speed/observatory/run-speed-test", + "/speed/observatory/test-results", + "/speed/optimization", + "/speed/optimization/content", + "/speed/optimization/content/apo", + "/speed/optimization/content/compression", + "/speed/optimization/content/early-hints", + "/speed/optimization/content/fonts", + "/speed/optimization/content/fonts/faq", + "/speed/optimization/content/fonts/troubleshooting", + "/speed/optimization/content/prefetch-urls", + "/speed/optimization/content/rocket-loader", + "/speed/optimization/content/rocket-loader/enable", + "/speed/optimization/content/rocket-loader/ignore-javascripts", + "/speed/optimization/content/shared-dictionaries", + "/speed/optimization/content/smart-hints", + "/speed/optimization/content/speed-brain", + "/speed/optimization/content/troubleshooting", + "/speed/optimization/content/troubleshooting/content-encoding-issues", + "/speed/optimization/content/troubleshooting/disable-auto-minify", + "/speed/optimization/images", + "/speed/optimization/images/image-resizing", + "/speed/optimization/images/mirage", + "/speed/optimization/images/polish", + "/speed/optimization/images/troubleshooting", + "/speed/optimization/images/troubleshooting/multiple-optimizations", + "/speed/optimization/images/troubleshooting/polish", + "/speed/optimization/images/troubleshooting/troubleshooting-missing-images", + "/speed/optimization/measurement", + "/speed/optimization/protocol", + "/speed/optimization/protocol/0-rtt-connection-resumption", + "/speed/optimization/protocol/enhanced-http2-prioritization", + "/speed/optimization/protocol/http2", + "/speed/optimization/protocol/http2-to-origin", + "/speed/optimization/protocol/http3", + "/speed/optimization/protocol/troubleshooting", + "/speed/optimization/protocol/troubleshooting/enhanced-http2-prioritization-ios-safari", + "/speed/optimization/protocol/troubleshooting/protocol-troubleshooting", + "/speed/optimization/recommendations", + "/speed/origin-analytics", + "/speed/smart-shield", + "/speed/troubleshooting", + "/speed/troubleshooting/slow-website", + "/sponsorships", + "/ssl", + "/ssl/changelog", + "/ssl/client-certificates", + "/ssl/client-certificates/byo-ca", + "/ssl/client-certificates/client-certificate-variables", + "/ssl/client-certificates/configure-your-mobile-app-or-iot-device", + "/ssl/client-certificates/create-a-client-certificate", + "/ssl/client-certificates/enable-mtls", + "/ssl/client-certificates/forward-a-client-certificate", + "/ssl/client-certificates/label-client-certificate", + "/ssl/client-certificates/revoke-client-certificate", + "/ssl/client-certificates/troubleshooting", + "/ssl/client-certificates/zero-trust-mtls", + "/ssl/concepts", + "/ssl/edge-certificates", + "/ssl/edge-certificates/additional-options", + "/ssl/edge-certificates/additional-options/always-use-https", + "/ssl/edge-certificates/additional-options/automatic-https-rewrites", + "/ssl/edge-certificates/additional-options/certificate-signing-requests", + "/ssl/edge-certificates/additional-options/certificate-transparency-monitoring", + "/ssl/edge-certificates/additional-options/cipher-suites", + "/ssl/edge-certificates/additional-options/cipher-suites/compliance-status", + "/ssl/edge-certificates/additional-options/cipher-suites/customize-cipher-suites", + "/ssl/edge-certificates/additional-options/cipher-suites/customize-cipher-suites/api", + "/ssl/edge-certificates/additional-options/cipher-suites/customize-cipher-suites/dashboard", + "/ssl/edge-certificates/additional-options/cipher-suites/recommendations", + "/ssl/edge-certificates/additional-options/cipher-suites/supported-cipher-suites", + "/ssl/edge-certificates/additional-options/cipher-suites/troubleshooting", + "/ssl/edge-certificates/additional-options/http-strict-transport-security", + "/ssl/edge-certificates/additional-options/minimum-tls", + "/ssl/edge-certificates/additional-options/opportunistic-encryption", + "/ssl/edge-certificates/additional-options/tls-13", + "/ssl/edge-certificates/additional-options/total-tls", + "/ssl/edge-certificates/additional-options/total-tls/enable", + "/ssl/edge-certificates/additional-options/total-tls/error-messages", + "/ssl/edge-certificates/advanced-certificate-manager", + "/ssl/edge-certificates/advanced-certificate-manager/api-commands", + "/ssl/edge-certificates/advanced-certificate-manager/manage-certificates", + "/ssl/edge-certificates/backup-certificates", + "/ssl/edge-certificates/caa-records", + "/ssl/edge-certificates/changing-dcv-method", + "/ssl/edge-certificates/changing-dcv-method/dcv-flow", + "/ssl/edge-certificates/changing-dcv-method/methods", + "/ssl/edge-certificates/changing-dcv-method/methods/delegated-dcv", + "/ssl/edge-certificates/changing-dcv-method/methods/http", + "/ssl/edge-certificates/changing-dcv-method/methods/txt", + "/ssl/edge-certificates/changing-dcv-method/troubleshooting", + "/ssl/edge-certificates/changing-dcv-method/validation-backoff-schedule", + "/ssl/edge-certificates/custom-certificates", + "/ssl/edge-certificates/custom-certificates/bundling-methodologies", + "/ssl/edge-certificates/custom-certificates/remove-file-key-password", + "/ssl/edge-certificates/custom-certificates/renewing", + "/ssl/edge-certificates/custom-certificates/troubleshooting", + "/ssl/edge-certificates/custom-certificates/uploading", + "/ssl/edge-certificates/ech", + "/ssl/edge-certificates/encrypt-visitor-traffic", + "/ssl/edge-certificates/geokey-manager", + "/ssl/edge-certificates/geokey-manager/setup", + "/ssl/edge-certificates/geokey-manager/supported-options", + "/ssl/edge-certificates/staging-environment", + "/ssl/edge-certificates/universal-ssl", + "/ssl/edge-certificates/universal-ssl/alerts", + "/ssl/edge-certificates/universal-ssl/disable-universal-ssl", + "/ssl/edge-certificates/universal-ssl/enable-universal-ssl", + "/ssl/edge-certificates/universal-ssl/limitations", + "/ssl/edge-certificates/universal-ssl/troubleshooting", + "/ssl/faq", + "/ssl/get-started", + "/ssl/keyless-ssl", + "/ssl/keyless-ssl/configuration", + "/ssl/keyless-ssl/configuration/cloudflare-tunnel", + "/ssl/keyless-ssl/configuration/public-dns", + "/ssl/keyless-ssl/glossary", + "/ssl/keyless-ssl/hardware-security-modules", + "/ssl/keyless-ssl/hardware-security-modules/aws-cloud-hsm", + "/ssl/keyless-ssl/hardware-security-modules/azure-dedicated-hsm", + "/ssl/keyless-ssl/hardware-security-modules/azure-managed-hsm", + "/ssl/keyless-ssl/hardware-security-modules/configuration", + "/ssl/keyless-ssl/hardware-security-modules/entrust-nshield-connect", + "/ssl/keyless-ssl/hardware-security-modules/fortanix-dsm", + "/ssl/keyless-ssl/hardware-security-modules/google-cloud-hsm", + "/ssl/keyless-ssl/hardware-security-modules/ibm-cloud-hsm", + "/ssl/keyless-ssl/hardware-security-modules/softhsmv2", + "/ssl/keyless-ssl/reference", + "/ssl/keyless-ssl/reference/high-availability", + "/ssl/keyless-ssl/reference/keyless-delegation", + "/ssl/keyless-ssl/reference/metrics", + "/ssl/keyless-ssl/reference/scaling-and-benchmarking", + "/ssl/keyless-ssl/troubleshooting", + "/ssl/keyless-ssl/upgrading-your-key-server", + "/ssl/origin-configuration", + "/ssl/origin-configuration/authenticated-origin-pull", + "/ssl/origin-configuration/authenticated-origin-pull/aws-alb-integration", + "/ssl/origin-configuration/authenticated-origin-pull/explanation", + "/ssl/origin-configuration/authenticated-origin-pull/set-up", + "/ssl/origin-configuration/authenticated-origin-pull/set-up/global", + "/ssl/origin-configuration/authenticated-origin-pull/set-up/manage-certificates", + "/ssl/origin-configuration/authenticated-origin-pull/set-up/per-hostname", + "/ssl/origin-configuration/authenticated-origin-pull/set-up/rollback", + "/ssl/origin-configuration/authenticated-origin-pull/set-up/zone-level", + "/ssl/origin-configuration/cipher-suites", + "/ssl/origin-configuration/custom-origin-trust-store", + "/ssl/origin-configuration/origin-ca", + "/ssl/origin-configuration/origin-ca/troubleshooting", + "/ssl/origin-configuration/ssl-modes", + "/ssl/origin-configuration/ssl-modes/flexible", + "/ssl/origin-configuration/ssl-modes/full", + "/ssl/origin-configuration/ssl-modes/full-strict", + "/ssl/origin-configuration/ssl-modes/off", + "/ssl/origin-configuration/ssl-modes/ssl-only-origin-pull", + "/ssl/origin-configuration/ssl-tls-recommender", + "/ssl/post-quantum-cryptography", + "/ssl/post-quantum-cryptography/pqc-and-zero-trust", + "/ssl/post-quantum-cryptography/pqc-cloudflare-products", + "/ssl/post-quantum-cryptography/pqc-support", + "/ssl/post-quantum-cryptography/pqc-to-origin", + "/ssl/reference", + "/ssl/reference/all-features", + "/ssl/reference/browser-compatibility", + "/ssl/reference/certificate-and-hostname-priority", + "/ssl/reference/certificate-authorities", + "/ssl/reference/certificate-pinning", + "/ssl/reference/certificate-rotation", + "/ssl/reference/certificate-statuses", + "/ssl/reference/certificate-validity-periods", + "/ssl/reference/cloudflare-and-cve-2019-1559", + "/ssl/reference/compliance-and-vulnerabilities", + "/ssl/reference/migration-guides", + "/ssl/reference/migration-guides/digicert-g1-distrust", + "/ssl/reference/migration-guides/entrust-distrust", + "/ssl/reference/protocols", + "/ssl/saas", + "/ssl/troubleshooting", + "/ssl/troubleshooting/err-ssl-protocol-error", + "/ssl/troubleshooting/general-ssl-errors", + "/ssl/troubleshooting/mixed-content-errors", + "/ssl/troubleshooting/too-many-redirects", + "/ssl/troubleshooting/version-cipher-mismatch", + "/stream", + "/stream/changelog", + "/stream/edit-videos", + "/stream/edit-videos/adding-additional-audio-tracks", + "/stream/edit-videos/adding-captions", + "/stream/edit-videos/applying-watermarks", + "/stream/edit-videos/player-enhancements", + "/stream/edit-videos/video-clipping", + "/stream/examples", + "/stream/examples/android", + "/stream/examples/dash-js", + "/stream/examples/hls-js", + "/stream/examples/ios", + "/stream/examples/obs-from-scratch", + "/stream/examples/rtmps_playback", + "/stream/examples/shaka-player", + "/stream/examples/srt_playback", + "/stream/examples/stream-player", + "/stream/examples/test-webhooks-locally", + "/stream/examples/video-js", + "/stream/examples/vidstack", + "/stream/faq", + "/stream/get-started", + "/stream/getting-analytics", + "/stream/getting-analytics/fetching-bulk-analytics", + "/stream/getting-analytics/live-viewer-count", + "/stream/manage-video-library", + "/stream/manage-video-library/bindings", + "/stream/manage-video-library/creator-id", + "/stream/manage-video-library/searching", + "/stream/manage-video-library/using-webhooks", + "/stream/pricing", + "/stream/stream-api", + "/stream/stream-live", + "/stream/stream-live/custom-domains", + "/stream/stream-live/download-stream-live-videos", + "/stream/stream-live/dvr-for-live", + "/stream/stream-live/live-instant-clipping", + "/stream/stream-live/replay-recordings", + "/stream/stream-live/simulcasting", + "/stream/stream-live/start-stream-live", + "/stream/stream-live/stream-live-api", + "/stream/stream-live/troubleshooting", + "/stream/stream-live/watch-live-stream", + "/stream/stream-live/webhooks", + "/stream/transform-videos", + "/stream/transform-videos/bindings", + "/stream/transform-videos/sources", + "/stream/transform-videos/troubleshooting", + "/stream/uploading-videos", + "/stream/uploading-videos/direct-creator-uploads", + "/stream/uploading-videos/player-api", + "/stream/uploading-videos/resumable-uploads", + "/stream/uploading-videos/upload-via-link", + "/stream/uploading-videos/upload-video-file", + "/stream/viewing-videos", + "/stream/viewing-videos/displaying-thumbnails", + "/stream/viewing-videos/download-videos", + "/stream/viewing-videos/securing-your-stream", + "/stream/viewing-videos/using-own-player", + "/stream/viewing-videos/using-own-player/android", + "/stream/viewing-videos/using-own-player/ios", + "/stream/viewing-videos/using-own-player/web", + "/stream/viewing-videos/using-the-stream-player", + "/stream/viewing-videos/using-the-stream-player/using-the-player-api", + "/stream/webrtc-beta", + "/style-guide", + "/style-guide/api-content-strategy", + "/style-guide/api-content-strategy/api-content-types", + "/style-guide/api-content-strategy/api-content-types/deprecated-apis", + "/style-guide/api-content-strategy/api-content-types/endpoints", + "/style-guide/api-content-strategy/api-content-types/get-started-api", + "/style-guide/api-content-strategy/api-content-types/parameters", + "/style-guide/api-content-strategy/api-content-types/resources", + "/style-guide/api-content-strategy/guidelines-for-curl-commands", + "/style-guide/api-content-strategy/method-types-and-command-verbs", + "/style-guide/components", + "/style-guide/components/anchor-heading", + "/style-guide/components/api-request", + "/style-guide/components/available-notifications", + "/style-guide/components/badges", + "/style-guide/components/buttons", + "/style-guide/components/cards", + "/style-guide/components/curl", + "/style-guide/components/dash-button", + "/style-guide/components/description", + "/style-guide/components/details", + "/style-guide/components/directory-listing", + "/style-guide/components/example", + "/style-guide/components/feature", + "/style-guide/components/feature-table", + "/style-guide/components/file-tree", + "/style-guide/components/github-code", + "/style-guide/components/glossary", + "/style-guide/components/glossary-definition", + "/style-guide/components/glossary-tooltip", + "/style-guide/components/icons", + "/style-guide/components/inline-badge", + "/style-guide/components/link-cards", + "/style-guide/components/list-tutorials", + "/style-guide/components/markdown", + "/style-guide/components/package-managers", + "/style-guide/components/pages-build-preset", + "/style-guide/components/plan", + "/style-guide/components/product-availability-text", + "/style-guide/components/product-changelog", + "/style-guide/components/product-features", + "/style-guide/components/public-stats", + "/style-guide/components/related-product", + "/style-guide/components/render", + "/style-guide/components/resources-by-selector", + "/style-guide/components/rss-button", + "/style-guide/components/rule-id", + "/style-guide/components/steps", + "/style-guide/components/stream", + "/style-guide/components/subtract-ip-calculator", + "/style-guide/components/tabs", + "/style-guide/components/type-highlighting", + "/style-guide/components/typescript-example", + "/style-guide/components/usage", + "/style-guide/components/width", + "/style-guide/components/wrangler-command", + "/style-guide/components/wrangler-config", + "/style-guide/components/wrangler-namespace", + "/style-guide/components/youtube", + "/style-guide/contributions", + "/style-guide/documentation-content-strategy", + "/style-guide/documentation-content-strategy/accessibility", + "/style-guide/documentation-content-strategy/component-attributes", + "/style-guide/documentation-content-strategy/component-attributes/context", + "/style-guide/documentation-content-strategy/component-attributes/diagrams", + "/style-guide/documentation-content-strategy/component-attributes/dynamic-lists", + "/style-guide/documentation-content-strategy/component-attributes/examples", + "/style-guide/documentation-content-strategy/component-attributes/glossary-entry", + "/style-guide/documentation-content-strategy/component-attributes/intended-audience", + "/style-guide/documentation-content-strategy/component-attributes/introduction", + "/style-guide/documentation-content-strategy/component-attributes/last-updated", + "/style-guide/documentation-content-strategy/component-attributes/links", + "/style-guide/documentation-content-strategy/component-attributes/mathematical-operations", + "/style-guide/documentation-content-strategy/component-attributes/next-steps", + "/style-guide/documentation-content-strategy/component-attributes/notes-tips-warnings", + "/style-guide/documentation-content-strategy/component-attributes/prerequisites", + "/style-guide/documentation-content-strategy/component-attributes/product-descriptions", + "/style-guide/documentation-content-strategy/component-attributes/reference-diagram", + "/style-guide/documentation-content-strategy/component-attributes/screenshots", + "/style-guide/documentation-content-strategy/component-attributes/steps-tasks-procedures", + "/style-guide/documentation-content-strategy/component-attributes/tables", + "/style-guide/documentation-content-strategy/component-attributes/titles", + "/style-guide/documentation-content-strategy/content-types", + "/style-guide/documentation-content-strategy/content-types/3rd-party-integration-guide", + "/style-guide/documentation-content-strategy/content-types/changelog", + "/style-guide/documentation-content-strategy/content-types/concept", + "/style-guide/documentation-content-strategy/content-types/configuration", + "/style-guide/documentation-content-strategy/content-types/design-guide", + "/style-guide/documentation-content-strategy/content-types/faq", + "/style-guide/documentation-content-strategy/content-types/get-started", + "/style-guide/documentation-content-strategy/content-types/how-to", + "/style-guide/documentation-content-strategy/content-types/implementation-guide", + "/style-guide/documentation-content-strategy/content-types/navigation", + "/style-guide/documentation-content-strategy/content-types/overview", + "/style-guide/documentation-content-strategy/content-types/reference", + "/style-guide/documentation-content-strategy/content-types/reference-architecture", + "/style-guide/documentation-content-strategy/content-types/reference-architecture-diagram", + "/style-guide/documentation-content-strategy/content-types/select-content-type", + "/style-guide/documentation-content-strategy/content-types/solution-guide", + "/style-guide/documentation-content-strategy/content-types/troubleshooting", + "/style-guide/documentation-content-strategy/content-types/tutorial", + "/style-guide/documentation-content-strategy/file-conventions", + "/style-guide/documentation-content-strategy/information-architecture", + "/style-guide/documentation-content-strategy/writing-guidelines", + "/style-guide/formatting", + "/style-guide/formatting/code-block-guidelines", + "/style-guide/formatting/code-conventions-and-format", + "/style-guide/formatting/dates-and-times", + "/style-guide/formatting/example-values", + "/style-guide/formatting/external-references", + "/style-guide/formatting/file-types-and-extensions", + "/style-guide/formatting/footnotes", + "/style-guide/formatting/keyboard-keys", + "/style-guide/formatting/notes-and-other-notation-types", + "/style-guide/formatting/numbers-and-units-of-measurement", + "/style-guide/formatting/product-name-and-pluralization", + "/style-guide/formatting/structure", + "/style-guide/formatting/structure/links", + "/style-guide/formatting/structure/lists", + "/style-guide/formatting/structure/paragraphs-and-line-breaks", + "/style-guide/formatting/structure/sentence-structure", + "/style-guide/formatting/structure/tables", + "/style-guide/formatting/ui-elements", + "/style-guide/formatting/urls-and-domain-names", + "/style-guide/frontmatter", + "/style-guide/frontmatter/banner", + "/style-guide/frontmatter/custom-properties", + "/style-guide/frontmatter/sidebar", + "/style-guide/grammar", + "/style-guide/grammar/parts-of-speech", + "/style-guide/grammar/parts-of-speech/abbreviations", + "/style-guide/grammar/parts-of-speech/acronyms", + "/style-guide/grammar/parts-of-speech/anthropomorphisms", + "/style-guide/grammar/parts-of-speech/capitalization", + "/style-guide/grammar/parts-of-speech/compound-words", + "/style-guide/grammar/parts-of-speech/contractions", + "/style-guide/grammar/parts-of-speech/nouns-and-pronouns", + "/style-guide/grammar/parts-of-speech/possessives", + "/style-guide/grammar/parts-of-speech/prepositions", + "/style-guide/grammar/parts-of-speech/slang", + "/style-guide/grammar/punctuation-marks-and-symbols", + "/style-guide/grammar/punctuation-marks-and-symbols/ampersands", + "/style-guide/grammar/punctuation-marks-and-symbols/colons", + "/style-guide/grammar/punctuation-marks-and-symbols/commas", + "/style-guide/grammar/punctuation-marks-and-symbols/dashes", + "/style-guide/grammar/punctuation-marks-and-symbols/exclamation-points", + "/style-guide/grammar/punctuation-marks-and-symbols/percentages", + "/style-guide/grammar/punctuation-marks-and-symbols/periods", + "/style-guide/grammar/punctuation-marks-and-symbols/quotation-marks", + "/style-guide/grammar/punctuation-marks-and-symbols/semicolons", + "/style-guide/how-we-docs", + "/style-guide/how-we-docs/ai-consumability", + "/style-guide/how-we-docs/how-we-ai", + "/style-guide/how-we-docs/how-we-ai/control-ai-crawls", + "/style-guide/how-we-docs/how-we-ai/examples", + "/style-guide/how-we-docs/how-we-ai/examples/cloudspeaker", + "/style-guide/how-we-docs/how-we-ai/examples/clue", + "/style-guide/how-we-docs/how-we-ai/prompt-libraries", + "/style-guide/how-we-docs/how-we-ai/prompt-templates", + "/style-guide/how-we-docs/how-we-ai/when-we-use-ai", + "/style-guide/how-we-docs/how-we-video", + "/style-guide/how-we-docs/how-we-video/integration-in-docs", + "/style-guide/how-we-docs/how-we-video/maintenance", + "/style-guide/how-we-docs/how-we-video/video-production-workflow", + "/style-guide/how-we-docs/how-we-video/why-and-when-we-use-videos", + "/style-guide/how-we-docs/image-maintenance", + "/style-guide/how-we-docs/links", + "/style-guide/how-we-docs/metadata", + "/style-guide/how-we-docs/our-site", + "/style-guide/how-we-docs/redirects", + "/style-guide/how-we-docs/reviews", + "/support", + "/support/cloudflare-status", + "/support/contacting-cloudflare-support", + "/support/disruptive-maintenance", + "/support/third-party-software", + "/support/third-party-software/content-management-system-cms", + "/support/third-party-software/content-management-system-cms/cloudflare-wordpress-plugin-automatic-cache-management", + "/support/third-party-software/content-management-system-cms/how-do-i-enable-http2-server-push-in-wordpress", + "/support/third-party-software/content-management-system-cms/improving-web-security-for-content-management-systems-like-wordpress", + "/support/third-party-software/content-management-system-cms/speed-up-wordpress-and-improve-performance", + "/support/third-party-software/content-management-system-cms/what-settings-are-applied-when-i-click-optimize-cloudflare-for-wordpress-in-cloudflares-wordpress-plugin", + "/support/third-party-software/content-management-system-cms/wordpress-jetpack-and-cloudflare", + "/support/third-party-software/content-management-system-cms/wordpresscom-and-cloudflare", + "/support/third-party-software/forum-software", + "/support/third-party-software/forum-software/using-cloudflare-with-various-forums-vbulletin-xenforo-mybb", + "/support/third-party-software/others", + "/support/third-party-software/others/configure-cloudflare-and-heroku-over-https", + "/support/third-party-software/others/reduce-data-transfer-egress-costs-between-azure-and-cloudflare", + "/support/troubleshooting", + "/support/troubleshooting/general-troubleshooting", + "/support/troubleshooting/general-troubleshooting/cannot-locate-dashboard-account", + "/support/troubleshooting/general-troubleshooting/gathering-information-for-troubleshooting-sites", + "/support/troubleshooting/general-troubleshooting/geographic-traffic-routing", + "/support/troubleshooting/general-troubleshooting/not-receiving-cloudflare-emails", + "/support/troubleshooting/general-troubleshooting/potential-isp-blocking", + "/support/troubleshooting/general-troubleshooting/service-disruption", + "/support/troubleshooting/general-troubleshooting/third-party-load-balancers", + "/support/troubleshooting/general-troubleshooting/troubleshooting-crawl-errors", + "/support/troubleshooting/http-status-codes", + "/support/troubleshooting/http-status-codes/1xx-informational", + "/support/troubleshooting/http-status-codes/2xx-success", + "/support/troubleshooting/http-status-codes/3xx-redirection", + "/support/troubleshooting/http-status-codes/4xx-client-error", + "/support/troubleshooting/http-status-codes/4xx-client-error/error-400", + "/support/troubleshooting/http-status-codes/4xx-client-error/error-401", + "/support/troubleshooting/http-status-codes/4xx-client-error/error-403", + "/support/troubleshooting/http-status-codes/4xx-client-error/error-404", + "/support/troubleshooting/http-status-codes/4xx-client-error/error-405", + "/support/troubleshooting/http-status-codes/4xx-client-error/error-406", + "/support/troubleshooting/http-status-codes/4xx-client-error/error-407", + "/support/troubleshooting/http-status-codes/4xx-client-error/error-408", + "/support/troubleshooting/http-status-codes/4xx-client-error/error-409", + "/support/troubleshooting/http-status-codes/4xx-client-error/error-410", + "/support/troubleshooting/http-status-codes/4xx-client-error/error-411", + "/support/troubleshooting/http-status-codes/4xx-client-error/error-412", + "/support/troubleshooting/http-status-codes/4xx-client-error/error-413", + "/support/troubleshooting/http-status-codes/4xx-client-error/error-414", + "/support/troubleshooting/http-status-codes/4xx-client-error/error-415", + "/support/troubleshooting/http-status-codes/4xx-client-error/error-416", + "/support/troubleshooting/http-status-codes/4xx-client-error/error-417", + "/support/troubleshooting/http-status-codes/4xx-client-error/error-429", + "/support/troubleshooting/http-status-codes/4xx-client-error/error-451", + "/support/troubleshooting/http-status-codes/4xx-client-error/error-499", + "/support/troubleshooting/http-status-codes/cloudflare-10xxx-errors", + "/support/troubleshooting/http-status-codes/cloudflare-10xxx-errors/error-10028", + "/support/troubleshooting/http-status-codes/cloudflare-10xxx-errors/error-10043", + "/support/troubleshooting/http-status-codes/cloudflare-10xxx-errors/error-10044", + "/support/troubleshooting/http-status-codes/cloudflare-10xxx-errors/error-10045", + "/support/troubleshooting/http-status-codes/cloudflare-10xxx-errors/error-10046", + "/support/troubleshooting/http-status-codes/cloudflare-10xxx-errors/error-10047", + "/support/troubleshooting/http-status-codes/cloudflare-10xxx-errors/error-10048", + "/support/troubleshooting/http-status-codes/cloudflare-10xxx-errors/error-10049", + "/support/troubleshooting/http-status-codes/cloudflare-10xxx-errors/error-10050", + "/support/troubleshooting/http-status-codes/cloudflare-10xxx-errors/error-10051", + "/support/troubleshooting/http-status-codes/cloudflare-10xxx-errors/error-10052", + "/support/troubleshooting/http-status-codes/cloudflare-10xxx-errors/error-10053", + "/support/troubleshooting/http-status-codes/cloudflare-10xxx-errors/error-10054", + "/support/troubleshooting/http-status-codes/cloudflare-10xxx-errors/error-10055", + "/support/troubleshooting/http-status-codes/cloudflare-10xxx-errors/error-10056", + "/support/troubleshooting/http-status-codes/cloudflare-10xxx-errors/error-10058", + "/support/troubleshooting/http-status-codes/cloudflare-10xxx-errors/error-10059", + "/support/troubleshooting/http-status-codes/cloudflare-10xxx-errors/error-10060", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1000", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1001", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1002", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1003", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1004", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1005", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1006", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1009", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1010", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1011", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1012", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1013", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1014", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1015", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1016", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1018", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1019", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1020", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1023", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1025", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1033", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1034", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1035", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1036", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1037", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1040", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1041", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1101", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1102", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1104", + "/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1200", + "/support/troubleshooting/http-status-codes/cloudflare-5xx-errors", + "/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-500", + "/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-501", + "/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-502-504", + "/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-503", + "/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-520", + "/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-521", + "/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-522", + "/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-523", + "/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-524", + "/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-525", + "/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-526", + "/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-530", + "/support/troubleshooting/http-status-codes/cloudflare-error-headers", + "/support/troubleshooting/restoring-visitor-ips", + "/support/troubleshooting/restoring-visitor-ips/restoring-original-visitor-ips", + "/tenant", + "/tenant/get-started", + "/tenant/glossary", + "/tenant/how-to", + "/tenant/how-to/get-account-details", + "/tenant/how-to/get-tenant-details", + "/tenant/how-to/manage-accounts", + "/tenant/how-to/manage-subscriptions", + "/tenant/reference", + "/tenant/reference/subscriptions", + "/tenant/structure", + "/terraform", + "/terraform/additional-configurations", + "/terraform/additional-configurations/ddos-managed-rulesets", + "/terraform/additional-configurations/link-bulk-redirects", + "/terraform/additional-configurations/link-cache-rules", + "/terraform/additional-configurations/link-configuration-rules", + "/terraform/additional-configurations/link-origin-rules", + "/terraform/additional-configurations/link-single-redirects", + "/terraform/additional-configurations/link-snippets", + "/terraform/additional-configurations/link-workers", + "/terraform/additional-configurations/rate-limiting-rules", + "/terraform/additional-configurations/transform-rules", + "/terraform/additional-configurations/waf-custom-rules", + "/terraform/additional-configurations/waf-managed-rulesets", + "/terraform/advanced-topics", + "/terraform/advanced-topics/best-practices", + "/terraform/advanced-topics/import-cloudflare-resources", + "/terraform/advanced-topics/provider-customization", + "/terraform/advanced-topics/remote-backend", + "/terraform/how-to", + "/terraform/how-to/create-partial-zone", + "/terraform/how-to/create-secondary-zone", + "/terraform/installing", + "/terraform/troubleshooting", + "/terraform/troubleshooting/authentication-error-dns-records", + "/terraform/troubleshooting/rule-id-changes", + "/terraform/tutorial", + "/terraform/tutorial/add-page-rules", + "/terraform/tutorial/configure-https-settings", + "/terraform/tutorial/initialize-terraform", + "/terraform/tutorial/revert-configuration", + "/terraform/tutorial/track-history", + "/terraform/tutorial/use-load-balancing", + "/time-services", + "/time-services/ntp", + "/time-services/ntp/usage", + "/time-services/nts", + "/time-services/roughtime", + "/time-services/roughtime/deprecation", + "/time-services/roughtime/recipes", + "/time-services/roughtime/usage", + "/time-services/tos", + "/tunnel", + "/tunnel/advanced", + "/tunnel/advanced/granular-permissions", + "/tunnel/advanced/local-management", + "/tunnel/advanced/local-management/as-a-service", + "/tunnel/advanced/local-management/as-a-service/linux", + "/tunnel/advanced/local-management/as-a-service/macos", + "/tunnel/advanced/local-management/as-a-service/windows", + "/tunnel/advanced/local-management/configuration-file", + "/tunnel/advanced/local-management/create-local-tunnel", + "/tunnel/advanced/local-management/local-tunnel-terms", + "/tunnel/advanced/local-management/tunnel-permissions", + "/tunnel/advanced/local-management/tunnel-useful-commands", + "/tunnel/advanced/origin-parameters", + "/tunnel/advanced/run-parameters", + "/tunnel/advanced/tunnel-tokens", + "/tunnel/changelog", + "/tunnel/configuration", + "/tunnel/deployment-guides", + "/tunnel/deployment-guides/ansible", + "/tunnel/deployment-guides/aws", + "/tunnel/deployment-guides/azure", + "/tunnel/deployment-guides/google-cloud-platform", + "/tunnel/deployment-guides/kubernetes", + "/tunnel/deployment-guides/terraform", + "/tunnel/downloads", + "/tunnel/downloads/system-requirements", + "/tunnel/downloads/update-cloudflared", + "/tunnel/integrations", + "/tunnel/monitoring", + "/tunnel/routing", + "/tunnel/setup", + "/tunnel/troubleshooting", + "/tunnel/tutorials", + "/tunnel/tutorials/grafana", + "/turnstile", + "/turnstile/additional-configuration", + "/turnstile/additional-configuration/ephemeral-id", + "/turnstile/additional-configuration/hostname-management", + "/turnstile/additional-configuration/hostname-management/any-hostname", + "/turnstile/additional-configuration/hostname-management/pre-clearance", + "/turnstile/additional-configuration/offlabel", + "/turnstile/additional-configuration/pre-clearance-support", + "/turnstile/changelog", + "/turnstile/community-resources", + "/turnstile/concepts", + "/turnstile/concepts/challenges", + "/turnstile/concepts/widget", + "/turnstile/extensions", + "/turnstile/extensions/google-firebase", + "/turnstile/extensions/pages-plugin", + "/turnstile/extensions/waiting-room", + "/turnstile/get-started", + "/turnstile/get-started/client-side-rendering", + "/turnstile/get-started/client-side-rendering/widget-configurations", + "/turnstile/get-started/mobile-implementation", + "/turnstile/get-started/server-side-validation", + "/turnstile/get-started/widget-management", + "/turnstile/get-started/widget-management/api", + "/turnstile/get-started/widget-management/dashboard", + "/turnstile/get-started/widget-management/terraform", + "/turnstile/migration", + "/turnstile/migration/hcaptcha", + "/turnstile/migration/recaptcha", + "/turnstile/plans", + "/turnstile/reference", + "/turnstile/reference/content-security-policy", + "/turnstile/reference/supported-browsers", + "/turnstile/reference/supported-languages", + "/turnstile/reference/turnstile-privacy-addendum", + "/turnstile/spin", + "/turnstile/troubleshooting", + "/turnstile/troubleshooting/challenge-solve-issues", + "/turnstile/troubleshooting/client-side-errors", + "/turnstile/troubleshooting/client-side-errors/error-codes", + "/turnstile/troubleshooting/feedback-reports", + "/turnstile/troubleshooting/rotate-secret-key", + "/turnstile/troubleshooting/testing", + "/turnstile/turnstile-analytics", + "/turnstile/turnstile-analytics/challenge-outcomes", + "/turnstile/turnstile-analytics/token-validation", + "/turnstile/tutorials", + "/turnstile/tutorials/conditionally-enforcing-turnstile", + "/turnstile/tutorials/excluding-turnstile-from-e2e-tests", + "/turnstile/tutorials/fraud-detection-with-ephemeral-ids", + "/turnstile/tutorials/integrating-turnstile-waf-and-bot-management", + "/turnstile/tutorials/login-pages", + "/use-cases", + "/use-cases/ai", + "/use-cases/ai/build-and-run", + "/use-cases/ai/control-costs", + "/use-cases/ai/store-and-retrieve-context", + "/use-cases/apis", + "/use-cases/apis/deploy-apis", + "/use-cases/apis/internal-services", + "/use-cases/apis/monitor-apis", + "/use-cases/apis/protect-apis", + "/use-cases/application-security", + "/use-cases/application-security/api-endpoints", + "/use-cases/application-security/block-attacks", + "/use-cases/application-security/bots", + "/use-cases/application-security/client-side", + "/use-cases/application-security/ddos", + "/use-cases/company-security", + "/use-cases/company-security/data-loss-prevention", + "/use-cases/company-security/device-security", + "/use-cases/company-security/email-security", + "/use-cases/company-security/employee-access", + "/use-cases/company-security/internet-access", + "/use-cases/e-commerce", + "/use-cases/e-commerce/analytics", + "/use-cases/e-commerce/performance", + "/use-cases/e-commerce/protect", + "/use-cases/e-commerce/traffic-at-scale", + "/use-cases/media-streaming", + "/use-cases/media-streaming/cache-delivery", + "/use-cases/media-streaming/image-optimization", + "/use-cases/media-streaming/secure-content", + "/use-cases/media-streaming/store-media", + "/use-cases/media-streaming/video-delivery", + "/use-cases/performance", + "/use-cases/performance/caching", + "/use-cases/performance/connections", + "/use-cases/performance/image-optimization", + "/use-cases/performance/load-balancing", + "/use-cases/performance/monitoring", + "/use-cases/performance/web-assets", + "/use-cases/saas", + "/use-cases/saas/code-deployment", + "/use-cases/saas/custom-domains", + "/use-cases/saas/data-isolation", + "/use-cases/saas/protect-platform", + "/use-cases/saas/usage-analytics", + "/use-cases/solutions", + "/use-cases/solutions/discover-secure-api-endpoints", + "/use-cases/solutions/encrypt-all-keep-site-secure", + "/use-cases/solutions/protect-sensitive-forms-fraud-abuse", + "/use-cases/solutions/stop-account-takeover-attacks", + "/use-cases/solutions/stop-malicious-bots", + "/use-cases/web-apps", + "/use-cases/web-apps/deploy-frontend", + "/use-cases/web-apps/performance", + "/use-cases/web-apps/real-time", + "/use-cases/web-apps/security", + "/use-cases/web-apps/serverless-backends", + "/use-cases/web-apps/store-data", + "/vectorize", + "/vectorize/best-practices", + "/vectorize/best-practices/create-indexes", + "/vectorize/best-practices/insert-vectors", + "/vectorize/best-practices/list-vectors", + "/vectorize/best-practices/query-vectors", + "/vectorize/demos", + "/vectorize/examples", + "/vectorize/examples/agents", + "/vectorize/examples/langchain", + "/vectorize/examples/rag", + "/vectorize/get-started", + "/vectorize/get-started/embeddings", + "/vectorize/get-started/intro", + "/vectorize/platform", + "/vectorize/platform/changelog", + "/vectorize/platform/event-subscriptions", + "/vectorize/platform/limits", + "/vectorize/platform/pricing", + "/vectorize/platform/storage-options", + "/vectorize/reference", + "/vectorize/reference/client-api", + "/vectorize/reference/metadata-filtering", + "/vectorize/reference/transition-vectorize-legacy", + "/vectorize/reference/what-is-a-vector-database", + "/vectorize/reference/wrangler-commands", + "/vectorize/tutorials", + "/vectorize/vectorize-api", + "/version-management", + "/version-management/about", + "/version-management/changelog", + "/version-management/get-started", + "/version-management/how-to", + "/version-management/how-to/compare-versions", + "/version-management/how-to/enable", + "/version-management/how-to/environments", + "/version-management/how-to/versions", + "/version-management/reference", + "/version-management/reference/available-configurations", + "/version-management/reference/read-only-environments", + "/version-management/reference/traffic-filters", + "/videos", + "/videos/app-sec-dashboard", + "/videos/app-sec-get-started", + "/videos/china-network-acceleration", + "/videos/china-network-inside-china", + "/videos/content-compression", + "/videos/create-api-tokens", + "/videos/full-ssl-encryption", + "/videos/how-cf-works", + "/videos/life-of-a-request-1", + "/videos/life-of-a-request-2", + "/videos/life-of-a-request-3", + "/videos/life-of-a-request-4", + "/videos/load-balancing", + "/videos/manage-account-members", + "/videos/onboard-domain-cf", + "/videos/review-your-dns-records", + "/videos/sase-1-evolution-corp-networks", + "/videos/sase-2-stop-your-own-vpn", + "/videos/sase-3-secure-remote-access", + "/videos/sase-4-connect-secure", + "/videos/sase-5-protect-users", + "/videos/set-up-access-policies", + "/videos/set-up-cf-tunnel", + "/videos/ssl-cipher-mismatch", + "/videos/strict-ssl-concepts", + "/videos/strict-ssl-encryption", + "/videos/the-online-address-book", + "/videos/warp-1-basics", + "/videos/warp-2-diagnostic-logs", + "/videos/what-is-cf-tunnel", + "/waf", + "/waf/account", + "/waf/account/custom-rulesets", + "/waf/account/custom-rulesets/create-api", + "/waf/account/custom-rulesets/create-dashboard", + "/waf/account/custom-rulesets/link-create-terraform", + "/waf/account/managed-rulesets", + "/waf/account/managed-rulesets/deploy-api", + "/waf/account/managed-rulesets/deploy-dashboard", + "/waf/account/managed-rulesets/link-create-exceptions", + "/waf/account/managed-rulesets/link-create-terraform", + "/waf/account/rate-limiting-rulesets", + "/waf/account/rate-limiting-rulesets/create-api", + "/waf/account/rate-limiting-rulesets/create-dashboard", + "/waf/account/rate-limiting-rulesets/link-create-terraform", + "/waf/analytics", + "/waf/analytics/security-analytics", + "/waf/analytics/security-events", + "/waf/change-log", + "/waf/change-log/changelog", + "/waf/change-log/changelog/2", + "/waf/change-log/changelog/3", + "/waf/change-log/changelog/4", + "/waf/change-log/historical-2022", + "/waf/change-log/historical-2023", + "/waf/change-log/historical-2024", + "/waf/change-log/scheduled-changes", + "/waf/concepts", + "/waf/custom-rules", + "/waf/custom-rules/create-api", + "/waf/custom-rules/create-dashboard", + "/waf/custom-rules/custom-rulesets", + "/waf/custom-rules/link-create-terraform", + "/waf/custom-rules/skip", + "/waf/custom-rules/skip/api-examples", + "/waf/custom-rules/skip/options", + "/waf/custom-rules/use-cases", + "/waf/custom-rules/use-cases/allow-traffic-from-ips-in-allowlist", + "/waf/custom-rules/use-cases/allow-traffic-from-specific-countries", + "/waf/custom-rules/use-cases/allow-traffic-from-verified-bots", + "/waf/custom-rules/use-cases/block-attack-score", + "/waf/custom-rules/use-cases/block-by-geographical-location", + "/waf/custom-rules/use-cases/block-ms-exchange-autodiscover", + "/waf/custom-rules/use-cases/block-traffic-from-specific-countries", + "/waf/custom-rules/use-cases/challenge-bad-bots", + "/waf/custom-rules/use-cases/check-jwt-claim-to-protect-admin-user", + "/waf/custom-rules/use-cases/configure-token-authentication", + "/waf/custom-rules/use-cases/exempt-partners-hotlink-protection", + "/waf/custom-rules/use-cases/require-specific-cookie", + "/waf/custom-rules/use-cases/require-specific-headers", + "/waf/custom-rules/use-cases/require-specific-http-ports", + "/waf/custom-rules/use-cases/sequence-custom-rules", + "/waf/custom-rules/use-cases/site-admin-only-known-ips", + "/waf/custom-rules/use-cases/stop-rudy-attacks", + "/waf/custom-rules/use-cases/update-rules-customers-partners", + "/waf/detections", + "/waf/detections/ai-security-for-apps", + "/waf/detections/ai-security-for-apps/example-rules", + "/waf/detections/ai-security-for-apps/fields", + "/waf/detections/ai-security-for-apps/get-started", + "/waf/detections/ai-security-for-apps/log-mode-vs-production-mode", + "/waf/detections/ai-security-for-apps/pii-detection", + "/waf/detections/ai-security-for-apps/prompt-injection", + "/waf/detections/ai-security-for-apps/token-counting", + "/waf/detections/ai-security-for-apps/unsafe-topics", + "/waf/detections/attack-score", + "/waf/detections/leaked-credentials", + "/waf/detections/leaked-credentials/api-calls", + "/waf/detections/leaked-credentials/examples", + "/waf/detections/leaked-credentials/get-started", + "/waf/detections/leaked-credentials/terraform-examples", + "/waf/detections/link-bots", + "/waf/detections/malicious-uploads", + "/waf/detections/malicious-uploads/api-calls", + "/waf/detections/malicious-uploads/example-rules", + "/waf/detections/malicious-uploads/get-started", + "/waf/detections/malicious-uploads/terraform-examples", + "/waf/detections/threat-intelligence", + "/waf/detections/threat-intelligence/example-rules", + "/waf/detections/threat-intelligence/fields", + "/waf/detections/threat-intelligence/get-started", + "/waf/feature-interoperability", + "/waf/get-started", + "/waf/glossary", + "/waf/managed-rules", + "/waf/managed-rules/check-for-exposed-credentials", + "/waf/managed-rules/check-for-exposed-credentials/configure-api", + "/waf/managed-rules/check-for-exposed-credentials/configure-terraform", + "/waf/managed-rules/check-for-exposed-credentials/how-checks-work", + "/waf/managed-rules/check-for-exposed-credentials/monitor-events", + "/waf/managed-rules/check-for-exposed-credentials/test-configuration", + "/waf/managed-rules/check-for-exposed-credentials/upgrade-to-leaked-credentials-detection", + "/waf/managed-rules/deploy-api", + "/waf/managed-rules/deploy-zone-dashboard", + "/waf/managed-rules/link-deploy-terraform", + "/waf/managed-rules/payload-logging", + "/waf/managed-rules/payload-logging/command-line", + "/waf/managed-rules/payload-logging/command-line/decrypt-payload", + "/waf/managed-rules/payload-logging/command-line/generate-key-pair", + "/waf/managed-rules/payload-logging/configure", + "/waf/managed-rules/payload-logging/configure-api", + "/waf/managed-rules/payload-logging/decrypt-in-logs", + "/waf/managed-rules/payload-logging/view", + "/waf/managed-rules/reference", + "/waf/managed-rules/reference/cloudflare-managed-ruleset", + "/waf/managed-rules/reference/exposed-credentials-check", + "/waf/managed-rules/reference/owasp-core-ruleset", + "/waf/managed-rules/reference/owasp-core-ruleset/concepts", + "/waf/managed-rules/reference/owasp-core-ruleset/configure-api", + "/waf/managed-rules/reference/owasp-core-ruleset/configure-dashboard", + "/waf/managed-rules/reference/owasp-core-ruleset/example", + "/waf/managed-rules/reference/owasp-core-ruleset/link-configure-terraform", + "/waf/managed-rules/reference/sensitive-data-detection", + "/waf/managed-rules/troubleshooting", + "/waf/managed-rules/waf-exceptions", + "/waf/managed-rules/waf-exceptions/define-api", + "/waf/managed-rules/waf-exceptions/define-dashboard", + "/waf/rate-limiting-rules", + "/waf/rate-limiting-rules/best-practices", + "/waf/rate-limiting-rules/create-api", + "/waf/rate-limiting-rules/create-zone-dashboard", + "/waf/rate-limiting-rules/find-rate-limit", + "/waf/rate-limiting-rules/link-create-terraform", + "/waf/rate-limiting-rules/parameters", + "/waf/rate-limiting-rules/request-rate", + "/waf/rate-limiting-rules/troubleshooting", + "/waf/rate-limiting-rules/use-cases", + "/waf/reference", + "/waf/reference/alerts", + "/waf/reference/legacy", + "/waf/reference/legacy/firewall-rules-upgrade", + "/waf/reference/legacy/link-firewall-rules", + "/waf/reference/legacy/old-rate-limiting", + "/waf/reference/legacy/old-rate-limiting/troubleshooting", + "/waf/reference/legacy/old-rate-limiting/upgrade", + "/waf/reference/legacy/old-waf-managed-rules", + "/waf/reference/legacy/old-waf-managed-rules/troubleshooting", + "/waf/reference/legacy/old-waf-managed-rules/upgrade", + "/waf/reference/phases", + "/waf/tools", + "/waf/tools/browser-integrity-check", + "/waf/tools/ip-access-rules", + "/waf/tools/ip-access-rules/actions", + "/waf/tools/ip-access-rules/create", + "/waf/tools/ip-access-rules/parameters", + "/waf/tools/link-security-txt", + "/waf/tools/lists", + "/waf/tools/lists/create-dashboard", + "/waf/tools/lists/custom-lists", + "/waf/tools/lists/link-bulk-redirect-lists", + "/waf/tools/lists/lists-api", + "/waf/tools/lists/lists-api/endpoints", + "/waf/tools/lists/lists-api/json-object", + "/waf/tools/lists/managed-lists", + "/waf/tools/lists/use-in-expressions", + "/waf/tools/privacy-pass", + "/waf/tools/replace-insecure-js-libraries", + "/waf/tools/scrape-shield", + "/waf/tools/scrape-shield/email-address-obfuscation", + "/waf/tools/scrape-shield/hotlink-protection", + "/waf/tools/security-level", + "/waf/tools/user-agent-blocking", + "/waf/tools/validation-checks", + "/waf/tools/zone-lockdown", + "/waf/troubleshooting", + "/waf/troubleshooting/blocked-bing-site-scans", + "/waf/troubleshooting/facebook-sharing", + "/waf/troubleshooting/fake-bot-managed-rules", + "/waf/troubleshooting/faq", + "/waf/troubleshooting/phase-interactions", + "/waf/troubleshooting/samesite-cookie-interaction", + "/waiting-room", + "/waiting-room/about", + "/waiting-room/additional-options", + "/waiting-room/additional-options/create-events", + "/waiting-room/additional-options/embed-waiting-room-in-iframe", + "/waiting-room/additional-options/ssl-for-saas", + "/waiting-room/additional-options/test-waiting-room", + "/waiting-room/additional-options/waiting-room-rules", + "/waiting-room/additional-options/waiting-room-rules/bypass-rules", + "/waiting-room/api-reference", + "/waiting-room/get-started", + "/waiting-room/glossary", + "/waiting-room/how-to", + "/waiting-room/how-to/control-user-session", + "/waiting-room/how-to/control-waiting-room", + "/waiting-room/how-to/create-waiting-room", + "/waiting-room/how-to/customize-waiting-room", + "/waiting-room/how-to/edit-delete-waiting-room", + "/waiting-room/how-to/json-response", + "/waiting-room/how-to/monitor-waiting-room", + "/waiting-room/how-to/place-waiting-room", + "/waiting-room/how-to/waiting-room-dashboard", + "/waiting-room/plans", + "/waiting-room/reference", + "/waiting-room/reference/best-practices", + "/waiting-room/reference/configuration-settings", + "/waiting-room/reference/queueing-methods", + "/waiting-room/reference/waiting-room-api", + "/waiting-room/reference/waiting-room-cookie", + "/waiting-room/troubleshooting", + "/waiting-room/waiting-room-analytics", + "/warp-client", + "/warp-client/get-started", + "/warp-client/get-started/android", + "/warp-client/get-started/ios", + "/warp-client/get-started/linux", + "/warp-client/get-started/macos", + "/warp-client/get-started/windows", + "/warp-client/known-issues-and-faq", + "/warp-client/legal", + "/warp-client/legal/3rdparty", + "/warp-client/privacy", + "/warp-client/warp-modes", + "/web-analytics", + "/web-analytics/about", + "/web-analytics/changelog", + "/web-analytics/configuration-options", + "/web-analytics/configuration-options/filters", + "/web-analytics/configuration-options/rules", + "/web-analytics/data-metrics", + "/web-analytics/data-metrics/core-web-vitals", + "/web-analytics/data-metrics/data-origin-and-collection", + "/web-analytics/data-metrics/dimensions", + "/web-analytics/data-metrics/high-level-metrics", + "/web-analytics/data-metrics/page-load-time-summary", + "/web-analytics/faq", + "/web-analytics/get-started", + "/web-analytics/get-started/notifications", + "/web-analytics/get-started/rum-beacon", + "/web-analytics/get-started/web-analytics-spa", + "/web-analytics/limits", + "/web3", + "/web3/about", + "/web3/api-reference", + "/web3/ethereum-gateway", + "/web3/ethereum-gateway/concepts", + "/web3/ethereum-gateway/concepts/ethereum", + "/web3/ethereum-gateway/concepts/node-types", + "/web3/ethereum-gateway/reference", + "/web3/ethereum-gateway/reference/kill-switches", + "/web3/ethereum-gateway/reference/rinkeby-deprecation", + "/web3/ethereum-gateway/reference/supported-api-methods", + "/web3/ethereum-gateway/reference/supported-networks", + "/web3/get-started", + "/web3/how-to", + "/web3/how-to/customize-cloudflare-settings", + "/web3/how-to/enable-gateways", + "/web3/how-to/manage-gateways", + "/web3/how-to/restrict-gateway-access", + "/web3/how-to/use-ethereum-gateway", + "/web3/how-to/use-ipfs-gateway", + "/web3/ipfs-gateway", + "/web3/ipfs-gateway/concepts", + "/web3/ipfs-gateway/concepts/dnslink", + "/web3/ipfs-gateway/concepts/ipfs", + "/web3/ipfs-gateway/concepts/universal-gateway", + "/web3/ipfs-gateway/reference", + "/web3/ipfs-gateway/reference/automated-deployment", + "/web3/ipfs-gateway/reference/peering-with-content-providers", + "/web3/ipfs-gateway/reference/updating-for-ipfs", + "/web3/ipfs-gateway/troubleshooting", + "/web3/reference", + "/web3/reference/gateway-dns-records", + "/web3/reference/gateway-status", + "/web3/reference/limits", + "/web3/reference/migration-guide", + "/workers", + "/workers-ai", + "/workers-ai/agents", + "/workers-ai/api-reference", + "/workers-ai/changelog", + "/workers-ai/configuration", + "/workers-ai/configuration/ai-sdk", + "/workers-ai/configuration/bindings", + "/workers-ai/configuration/hugging-face-chat-ui", + "/workers-ai/configuration/open-ai-compatibility", + "/workers-ai/features", + "/workers-ai/features/batch-api", + "/workers-ai/features/batch-api/rest-api", + "/workers-ai/features/batch-api/workers-binding", + "/workers-ai/features/fine-tunes", + "/workers-ai/features/fine-tunes/loras", + "/workers-ai/features/fine-tunes/public-loras", + "/workers-ai/features/function-calling", + "/workers-ai/features/function-calling/embedded", + "/workers-ai/features/function-calling/embedded/api-reference", + "/workers-ai/features/function-calling/embedded/examples", + "/workers-ai/features/function-calling/embedded/examples/fetch", + "/workers-ai/features/function-calling/embedded/examples/kv", + "/workers-ai/features/function-calling/embedded/examples/openapi", + "/workers-ai/features/function-calling/embedded/get-started", + "/workers-ai/features/function-calling/embedded/troubleshooting", + "/workers-ai/features/function-calling/traditional", + "/workers-ai/features/json-mode", + "/workers-ai/features/markdown-conversion", + "/workers-ai/features/markdown-conversion/conversion-options", + "/workers-ai/features/markdown-conversion/how-it-works", + "/workers-ai/features/markdown-conversion/supported-formats", + "/workers-ai/features/markdown-conversion/usage", + "/workers-ai/features/markdown-conversion/usage/binding", + "/workers-ai/features/markdown-conversion/usage/rest-api", + "/workers-ai/features/prompt-caching", + "/workers-ai/features/prompting", + "/workers-ai/get-started", + "/workers-ai/get-started/dashboard", + "/workers-ai/get-started/rest-api", + "/workers-ai/get-started/workers-wrangler", + "/workers-ai/guides", + "/workers-ai/guides/agents", + "/workers-ai/guides/demos-architectures", + "/workers-ai/guides/tutorials", + "/workers-ai/guides/tutorials/build-a-retrieval-augmented-generation-ai", + "/workers-ai/guides/tutorials/build-a-workers-ai-whisper-with-chunking", + "/workers-ai/guides/tutorials/explore-code-generation-using-deepseek-coder-models", + "/workers-ai/guides/tutorials/explore-workers-ai-models-using-a-jupyter-notebook", + "/workers-ai/guides/tutorials/fine-tune-models-with-autotrain", + "/workers-ai/guides/tutorials/how-to-choose-the-right-text-generation-model", + "/workers-ai/guides/tutorials/image-generation-playground", + "/workers-ai/guides/tutorials/image-generation-playground/image-generator-flux", + "/workers-ai/guides/tutorials/image-generation-playground/image-generator-flux-newmodels", + "/workers-ai/guides/tutorials/image-generation-playground/image-generator-store-and-catalog", + "/workers-ai/guides/tutorials/llama-vision-tutorial", + "/workers-ai/guides/tutorials/using-bigquery-with-workers-ai", + "/workers-ai/models", + "/workers-ai/models/aura-1", + "/workers-ai/models/aura-2-en", + "/workers-ai/models/aura-2-es", + "/workers-ai/models/bart-large-cnn", + "/workers-ai/models/bge-base-en-v1.5", + "/workers-ai/models/bge-large-en-v1.5", + "/workers-ai/models/bge-m3", + "/workers-ai/models/bge-reranker-base", + "/workers-ai/models/bge-small-en-v1.5", + "/workers-ai/models/deepseek-r1-distill-qwen-32b", + "/workers-ai/models/detr-resnet-50", + "/workers-ai/models/distilbert-sst-2-int8", + "/workers-ai/models/dreamshaper-8-lcm", + "/workers-ai/models/embeddinggemma-300m", + "/workers-ai/models/flux", + "/workers-ai/models/flux-1-schnell", + "/workers-ai/models/flux-2-dev", + "/workers-ai/models/flux-2-klein-4b", + "/workers-ai/models/flux-2-klein-9b", + "/workers-ai/models/gemma-2b-it-lora", + "/workers-ai/models/gemma-3-12b-it", + "/workers-ai/models/gemma-4-26b-a4b-it", + "/workers-ai/models/gemma-7b-it", + "/workers-ai/models/gemma-7b-it-lora", + "/workers-ai/models/gemma-sea-lion-v4-27b-it", + "/workers-ai/models/glm-4.7-flash", + "/workers-ai/models/glm-5.2", + "/workers-ai/models/gpt-oss-120b", + "/workers-ai/models/gpt-oss-20b", + "/workers-ai/models/granite-4.0-h-micro", + "/workers-ai/models/hermes-2-pro-mistral-7b", + "/workers-ai/models/indictrans2-en-indic-1B", + "/workers-ai/models/kimi-k2.5", + "/workers-ai/models/kimi-k2.6", + "/workers-ai/models/kimi-k2.7-code", + "/workers-ai/models/llama-2-7b-chat-fp16", + "/workers-ai/models/llama-2-7b-chat-hf-lora", + "/workers-ai/models/llama-2-7b-chat-int8", + "/workers-ai/models/llama-3-8b-instruct", + "/workers-ai/models/llama-3-8b-instruct-awq", + "/workers-ai/models/llama-3.1-70b-instruct", + "/workers-ai/models/llama-3.1-8b-instruct", + "/workers-ai/models/llama-3.1-8b-instruct-awq", + "/workers-ai/models/llama-3.1-8b-instruct-fast", + "/workers-ai/models/llama-3.1-8b-instruct-fp8", + "/workers-ai/models/llama-3.2-11b-vision-instruct", + "/workers-ai/models/llama-3.2-1b-instruct", + "/workers-ai/models/llama-3.2-3b-instruct", + "/workers-ai/models/llama-3.3-70b-instruct-fp8-fast", + "/workers-ai/models/llama-4-scout-17b-16e-instruct", + "/workers-ai/models/llama-guard-3-8b", + "/workers-ai/models/llava-1.5-7b-hf", + "/workers-ai/models/lucid-origin", + "/workers-ai/models/m2m100-1.2b", + "/workers-ai/models/melotts", + "/workers-ai/models/meta-llama-3-8b-instruct", + "/workers-ai/models/mistral-7b-instruct-v0.1", + "/workers-ai/models/mistral-7b-instruct-v0.2", + "/workers-ai/models/mistral-7b-instruct-v0.2-lora", + "/workers-ai/models/mistral-small-3.1-24b-instruct", + "/workers-ai/models/nemotron-3-120b-a12b", + "/workers-ai/models/nova-3", + "/workers-ai/models/phi-2", + "/workers-ai/models/phoenix-1.0", + "/workers-ai/models/plamo-embedding-1b", + "/workers-ai/models/qwen2.5-coder-32b-instruct", + "/workers-ai/models/qwen3-30b-a3b-fp8", + "/workers-ai/models/qwen3-embedding-0.6b", + "/workers-ai/models/qwq-32b", + "/workers-ai/models/resnet-50", + "/workers-ai/models/smart-turn-v2", + "/workers-ai/models/sqlcoder-7b-2", + "/workers-ai/models/stable-diffusion-v1-5-img2img", + "/workers-ai/models/stable-diffusion-v1-5-inpainting", + "/workers-ai/models/stable-diffusion-xl-base-1.0", + "/workers-ai/models/stable-diffusion-xl-lightning", + "/workers-ai/models/uform-gen2-qwen-500m", + "/workers-ai/models/whisper", + "/workers-ai/models/whisper-large-v3-turbo", + "/workers-ai/models/whisper-tiny-en", + "/workers-ai/platform", + "/workers-ai/platform/ai-gateway", + "/workers-ai/platform/data-usage", + "/workers-ai/platform/errors", + "/workers-ai/platform/event-subscriptions", + "/workers-ai/platform/glossary", + "/workers-ai/platform/limits", + "/workers-ai/platform/pricing", + "/workers-ai/platform/storage-options", + "/workers-ai/playground", + "/workers-vpc", + "/workers-vpc/api", + "/workers-vpc/configuration", + "/workers-vpc/configuration/tunnel", + "/workers-vpc/configuration/tunnel/hardware-requirements", + "/workers-vpc/configuration/vpc-networks", + "/workers-vpc/configuration/vpc-services", + "/workers-vpc/configuration/vpc-services/terraform", + "/workers-vpc/examples", + "/workers-vpc/examples/connect-to-cloudflare-mesh", + "/workers-vpc/examples/private-api", + "/workers-vpc/examples/private-database", + "/workers-vpc/examples/private-s3-bucket", + "/workers-vpc/examples/route-across-private-services", + "/workers-vpc/get-started", + "/workers-vpc/reference", + "/workers-vpc/reference/limits", + "/workers-vpc/reference/pricing", + "/workers-vpc/reference/troubleshooting", + "/workers-vpc/reference/wrangler-commands", + "/workers/best-practices", + "/workers/best-practices/workers-best-practices", + "/workers/ci-cd", + "/workers/ci-cd/builds", + "/workers/ci-cd/builds/advanced-setups", + "/workers/ci-cd/builds/api-reference", + "/workers/ci-cd/builds/automatic-prs", + "/workers/ci-cd/builds/build-branches", + "/workers/ci-cd/builds/build-caching", + "/workers/ci-cd/builds/build-image", + "/workers/ci-cd/builds/build-watch-paths", + "/workers/ci-cd/builds/configuration", + "/workers/ci-cd/builds/deploy-hooks", + "/workers/ci-cd/builds/event-subscriptions", + "/workers/ci-cd/builds/git-integration", + "/workers/ci-cd/builds/git-integration/github-integration", + "/workers/ci-cd/builds/git-integration/gitlab-integration", + "/workers/ci-cd/builds/limits-and-pricing", + "/workers/ci-cd/builds/mcp-server", + "/workers/ci-cd/builds/troubleshoot", + "/workers/ci-cd/external-cicd", + "/workers/ci-cd/external-cicd/github-actions", + "/workers/ci-cd/external-cicd/gitlab-cicd", + "/workers/configuration", + "/workers/configuration/bindings", + "/workers/configuration/compatibility-dates", + "/workers/configuration/compatibility-flags", + "/workers/configuration/cron-triggers", + "/workers/configuration/environment-variables", + "/workers/configuration/integrations", + "/workers/configuration/integrations/apis", + "/workers/configuration/integrations/external-services", + "/workers/configuration/multipart-upload-metadata", + "/workers/configuration/placement", + "/workers/configuration/previews", + "/workers/configuration/routing", + "/workers/configuration/routing/custom-domains", + "/workers/configuration/routing/routes", + "/workers/configuration/routing/workers-dev", + "/workers/configuration/secrets", + "/workers/configuration/sites", + "/workers/configuration/sites/configuration", + "/workers/configuration/sites/start-from-existing", + "/workers/configuration/sites/start-from-scratch", + "/workers/configuration/sites/start-from-worker", + "/workers/configuration/versions-and-deployments", + "/workers/configuration/versions-and-deployments/gradual-deployments", + "/workers/configuration/versions-and-deployments/rollbacks", + "/workers/configuration/versions-and-deployments/version-overrides", + "/workers/configuration/workers-with-page-rules", + "/workers/databases", + "/workers/databases/analytics-engine", + "/workers/databases/connecting-to-databases", + "/workers/databases/d1", + "/workers/databases/hyperdrive", + "/workers/databases/third-party-integrations", + "/workers/databases/third-party-integrations/neon", + "/workers/databases/third-party-integrations/planetscale", + "/workers/databases/third-party-integrations/supabase", + "/workers/databases/third-party-integrations/turso", + "/workers/databases/third-party-integrations/upstash", + "/workers/databases/third-party-integrations/xata", + "/workers/databases/vectorize", + "/workers/demos", + "/workers/demos/chatgpt-app", + "/workers/examples", + "/workers/examples/103-early-hints", + "/workers/examples/ab-testing", + "/workers/examples/accessing-the-cloudflare-object", + "/workers/examples/aggregate-requests", + "/workers/examples/alter-headers", + "/workers/examples/analytics-engine", + "/workers/examples/auth-with-headers", + "/workers/examples/basic-auth", + "/workers/examples/block-on-tls", + "/workers/examples/bulk-origin-proxy", + "/workers/examples/bulk-redirects", + "/workers/examples/cache-api", + "/workers/examples/cache-post-request", + "/workers/examples/cache-tags", + "/workers/examples/cache-using-fetch", + "/workers/examples/conditional-response", + "/workers/examples/cors-header-proxy", + "/workers/examples/country-code-redirect", + "/workers/examples/cron-trigger", + "/workers/examples/data-loss-prevention", + "/workers/examples/debugging-logs", + "/workers/examples/extract-cookie-value", + "/workers/examples/fetch-html", + "/workers/examples/fetch-json", + "/workers/examples/geolocation-app-weather", + "/workers/examples/geolocation-custom-styling", + "/workers/examples/geolocation-hello-world", + "/workers/examples/hot-link-protection", + "/workers/examples/images-workers", + "/workers/examples/logging-headers", + "/workers/examples/modify-request-property", + "/workers/examples/modify-response", + "/workers/examples/multiple-cron-triggers", + "/workers/examples/openai-sdk-streaming", + "/workers/examples/post-json", + "/workers/examples/protect-against-timing-attacks", + "/workers/examples/read-post", + "/workers/examples/redirect", + "/workers/examples/respond-with-another-site", + "/workers/examples/return-html", + "/workers/examples/return-json", + "/workers/examples/rewrite-links", + "/workers/examples/security-headers", + "/workers/examples/signing-requests", + "/workers/examples/spa-shell", + "/workers/examples/streaming-json", + "/workers/examples/turnstile-html-rewriter", + "/workers/examples/websockets", + "/workers/framework-guides", + "/workers/framework-guides/ai-and-agents", + "/workers/framework-guides/ai-and-agents/agents-sdk", + "/workers/framework-guides/ai-and-agents/langchain", + "/workers/framework-guides/apis", + "/workers/framework-guides/apis/fast-api", + "/workers/framework-guides/apis/hono", + "/workers/framework-guides/automatic-configuration", + "/workers/framework-guides/mobile-apps", + "/workers/framework-guides/mobile-apps/expo", + "/workers/framework-guides/web-apps", + "/workers/framework-guides/web-apps/astro", + "/workers/framework-guides/web-apps/microfrontends", + "/workers/framework-guides/web-apps/more-web-frameworks", + "/workers/framework-guides/web-apps/more-web-frameworks/analog", + "/workers/framework-guides/web-apps/more-web-frameworks/angular", + "/workers/framework-guides/web-apps/more-web-frameworks/docusaurus", + "/workers/framework-guides/web-apps/more-web-frameworks/gatsby", + "/workers/framework-guides/web-apps/more-web-frameworks/hono", + "/workers/framework-guides/web-apps/more-web-frameworks/nuxt", + "/workers/framework-guides/web-apps/more-web-frameworks/qwik", + "/workers/framework-guides/web-apps/more-web-frameworks/solid", + "/workers/framework-guides/web-apps/more-web-frameworks/waku", + "/workers/framework-guides/web-apps/nextjs", + "/workers/framework-guides/web-apps/react", + "/workers/framework-guides/web-apps/react-router", + "/workers/framework-guides/web-apps/redwoodsdk", + "/workers/framework-guides/web-apps/sveltekit", + "/workers/framework-guides/web-apps/tanstack-start", + "/workers/framework-guides/web-apps/vike", + "/workers/framework-guides/web-apps/vue", + "/workers/get-started", + "/workers/get-started/dashboard", + "/workers/get-started/guide", + "/workers/get-started/prompting", + "/workers/get-started/quickstarts", + "/workers/glossary", + "/workers/languages", + "/workers/languages/javascript", + "/workers/languages/javascript/examples", + "/workers/languages/python", + "/workers/languages/python/basics", + "/workers/languages/python/examples", + "/workers/languages/python/ffi", + "/workers/languages/python/how-python-workers-work", + "/workers/languages/python/packages", + "/workers/languages/python/packages/fastapi", + "/workers/languages/python/packages/langchain", + "/workers/languages/python/stdlib", + "/workers/languages/rust", + "/workers/languages/rust/crates", + "/workers/languages/typescript", + "/workers/languages/typescript/examples", + "/workers/local-development", + "/workers/local-development/bindings-per-env", + "/workers/local-development/environment-variables", + "/workers/local-development/local-data", + "/workers/local-development/local-dev-tunnels", + "/workers/local-development/local-explorer", + "/workers/local-development/multi-workers", + "/workers/local-development/vite-plugin", + "/workers/local-development/wrangler-vs-vite", + "/workers/observability", + "/workers/observability/dev-tools", + "/workers/observability/dev-tools/breakpoints", + "/workers/observability/dev-tools/cpu-usage", + "/workers/observability/dev-tools/memory-usage", + "/workers/observability/errors", + "/workers/observability/exporting-opentelemetry-data", + "/workers/observability/exporting-opentelemetry-data/axiom", + "/workers/observability/exporting-opentelemetry-data/grafana-cloud", + "/workers/observability/exporting-opentelemetry-data/honeycomb", + "/workers/observability/exporting-opentelemetry-data/posthog", + "/workers/observability/exporting-opentelemetry-data/sentry", + "/workers/observability/logs", + "/workers/observability/logs/logpush", + "/workers/observability/logs/real-time-logs", + "/workers/observability/logs/tail-workers", + "/workers/observability/logs/workers-logs", + "/workers/observability/mcp-server", + "/workers/observability/metrics-and-analytics", + "/workers/observability/query-builder", + "/workers/observability/source-maps", + "/workers/observability/third-party-integrations", + "/workers/observability/third-party-integrations/sentry", + "/workers/observability/traces", + "/workers/observability/traces/custom-spans", + "/workers/observability/traces/known-limitations", + "/workers/observability/traces/spans-and-attributes", + "/workers/platform", + "/workers/platform/betas", + "/workers/platform/built-with-cloudflare", + "/workers/platform/changelog", + "/workers/platform/changelog/historical-changelog", + "/workers/platform/changelog/wrangler", + "/workers/platform/claim-deployments", + "/workers/platform/deploy-buttons", + "/workers/platform/infrastructure-as-code", + "/workers/platform/known-issues", + "/workers/platform/limits", + "/workers/platform/pricing", + "/workers/platform/storage-options", + "/workers/platform/workers-for-platforms", + "/workers/playground", + "/workers/reference", + "/workers/reference/how-the-cache-works", + "/workers/reference/how-workers-works", + "/workers/reference/migrate-to-module-workers", + "/workers/reference/protocols", + "/workers/reference/security-model", + "/workers/runtime-apis", + "/workers/runtime-apis/bindings", + "/workers/runtime-apis/bindings/ai", + "/workers/runtime-apis/bindings/analytics-engine", + "/workers/runtime-apis/bindings/assets", + "/workers/runtime-apis/bindings/browser-run", + "/workers/runtime-apis/bindings/d1", + "/workers/runtime-apis/bindings/dispatcher", + "/workers/runtime-apis/bindings/durable-objects", + "/workers/runtime-apis/bindings/environment-variables", + "/workers/runtime-apis/bindings/hyperdrive", + "/workers/runtime-apis/bindings/images", + "/workers/runtime-apis/bindings/kv", + "/workers/runtime-apis/bindings/media", + "/workers/runtime-apis/bindings/mtls", + "/workers/runtime-apis/bindings/queues", + "/workers/runtime-apis/bindings/r2", + "/workers/runtime-apis/bindings/rate-limit", + "/workers/runtime-apis/bindings/secrets", + "/workers/runtime-apis/bindings/secrets-store", + "/workers/runtime-apis/bindings/service-bindings", + "/workers/runtime-apis/bindings/service-bindings/http", + "/workers/runtime-apis/bindings/service-bindings/rpc", + "/workers/runtime-apis/bindings/stream", + "/workers/runtime-apis/bindings/vectorize", + "/workers/runtime-apis/bindings/version-metadata", + "/workers/runtime-apis/bindings/worker-loader", + "/workers/runtime-apis/bindings/workflows", + "/workers/runtime-apis/cache", + "/workers/runtime-apis/console", + "/workers/runtime-apis/context", + "/workers/runtime-apis/encoding", + "/workers/runtime-apis/eventsource", + "/workers/runtime-apis/fetch", + "/workers/runtime-apis/handlers", + "/workers/runtime-apis/handlers/alarm", + "/workers/runtime-apis/handlers/email", + "/workers/runtime-apis/handlers/fetch", + "/workers/runtime-apis/handlers/queue", + "/workers/runtime-apis/handlers/scheduled", + "/workers/runtime-apis/handlers/tail", + "/workers/runtime-apis/headers", + "/workers/runtime-apis/html-rewriter", + "/workers/runtime-apis/messagechannel", + "/workers/runtime-apis/nodejs", + "/workers/runtime-apis/nodejs/assert", + "/workers/runtime-apis/nodejs/asynclocalstorage", + "/workers/runtime-apis/nodejs/buffer", + "/workers/runtime-apis/nodejs/crypto", + "/workers/runtime-apis/nodejs/diagnostics-channel", + "/workers/runtime-apis/nodejs/dns", + "/workers/runtime-apis/nodejs/eventemitter", + "/workers/runtime-apis/nodejs/fs", + "/workers/runtime-apis/nodejs/http", + "/workers/runtime-apis/nodejs/https", + "/workers/runtime-apis/nodejs/net", + "/workers/runtime-apis/nodejs/path", + "/workers/runtime-apis/nodejs/process", + "/workers/runtime-apis/nodejs/streams", + "/workers/runtime-apis/nodejs/string-decoder", + "/workers/runtime-apis/nodejs/test", + "/workers/runtime-apis/nodejs/timers", + "/workers/runtime-apis/nodejs/tls", + "/workers/runtime-apis/nodejs/url", + "/workers/runtime-apis/nodejs/util", + "/workers/runtime-apis/nodejs/zlib", + "/workers/runtime-apis/performance", + "/workers/runtime-apis/request", + "/workers/runtime-apis/response", + "/workers/runtime-apis/rpc", + "/workers/runtime-apis/rpc/error-handling", + "/workers/runtime-apis/rpc/lifecycle", + "/workers/runtime-apis/rpc/reserved-methods", + "/workers/runtime-apis/rpc/typescript", + "/workers/runtime-apis/rpc/visibility", + "/workers/runtime-apis/scheduler", + "/workers/runtime-apis/streams", + "/workers/runtime-apis/streams/readablestream", + "/workers/runtime-apis/streams/readablestreambyobreader", + "/workers/runtime-apis/streams/readablestreamdefaultreader", + "/workers/runtime-apis/streams/transformstream", + "/workers/runtime-apis/streams/writablestream", + "/workers/runtime-apis/streams/writablestreamdefaultwriter", + "/workers/runtime-apis/tcp-sockets", + "/workers/runtime-apis/web-crypto", + "/workers/runtime-apis/web-standards", + "/workers/runtime-apis/webassembly", + "/workers/runtime-apis/webassembly/javascript", + "/workers/runtime-apis/websockets", + "/workers/static-assets", + "/workers/static-assets/billing-and-limitations", + "/workers/static-assets/binding", + "/workers/static-assets/direct-upload", + "/workers/static-assets/get-started", + "/workers/static-assets/headers", + "/workers/static-assets/migration-guides", + "/workers/static-assets/migration-guides/migrate-from-pages", + "/workers/static-assets/migration-guides/netlify-to-workers", + "/workers/static-assets/migration-guides/vercel-to-workers", + "/workers/static-assets/redirects", + "/workers/static-assets/routing", + "/workers/static-assets/routing/advanced", + "/workers/static-assets/routing/advanced/gradual-rollouts", + "/workers/static-assets/routing/advanced/html-handling", + "/workers/static-assets/routing/advanced/serving-a-subdirectory", + "/workers/static-assets/routing/full-stack-application", + "/workers/static-assets/routing/single-page-application", + "/workers/static-assets/routing/static-site-generation", + "/workers/static-assets/routing/worker-script", + "/workers/testing", + "/workers/testing/miniflare", + "/workers/testing/miniflare/core", + "/workers/testing/miniflare/core/compatibility", + "/workers/testing/miniflare/core/fetch", + "/workers/testing/miniflare/core/modules", + "/workers/testing/miniflare/core/multiple-workers", + "/workers/testing/miniflare/core/queues", + "/workers/testing/miniflare/core/scheduled", + "/workers/testing/miniflare/core/standards", + "/workers/testing/miniflare/core/variables-secrets", + "/workers/testing/miniflare/core/web-sockets", + "/workers/testing/miniflare/developing", + "/workers/testing/miniflare/developing/debugger", + "/workers/testing/miniflare/developing/live-reload", + "/workers/testing/miniflare/get-started", + "/workers/testing/miniflare/migrations", + "/workers/testing/miniflare/migrations/from-v2", + "/workers/testing/miniflare/storage", + "/workers/testing/miniflare/storage/cache", + "/workers/testing/miniflare/storage/d1", + "/workers/testing/miniflare/storage/durable-objects", + "/workers/testing/miniflare/storage/kv", + "/workers/testing/miniflare/storage/r2", + "/workers/testing/miniflare/writing-tests", + "/workers/testing/unstable_startworker", + "/workers/testing/vitest-integration", + "/workers/testing/vitest-integration/configuration", + "/workers/testing/vitest-integration/debugging", + "/workers/testing/vitest-integration/isolation-and-concurrency", + "/workers/testing/vitest-integration/known-issues", + "/workers/testing/vitest-integration/migration-guides", + "/workers/testing/vitest-integration/migration-guides/migrate-from-miniflare-2", + "/workers/testing/vitest-integration/migration-guides/migrate-from-unstable-dev", + "/workers/testing/vitest-integration/migration-guides/migrate-from-vitest-3-to-vitest-4", + "/workers/testing/vitest-integration/recipes", + "/workers/testing/vitest-integration/test-apis", + "/workers/testing/vitest-integration/write-your-first-test", + "/workers/tutorials", + "/workers/tutorials/build-a-jamstack-app", + "/workers/tutorials/build-a-qr-code-generator", + "/workers/tutorials/build-a-slackbot", + "/workers/tutorials/connect-to-turso-using-workers", + "/workers/tutorials/create-finetuned-chatgpt-ai-models-with-r2", + "/workers/tutorials/deploy-a-realtime-chat-app", + "/workers/tutorials/deploy-an-express-app", + "/workers/tutorials/generate-youtube-thumbnails-with-workers-and-images", + "/workers/tutorials/github-sms-notifications-using-twilio", + "/workers/tutorials/handle-form-submissions-with-airtable", + "/workers/tutorials/mysql", + "/workers/tutorials/openai-function-calls-workers", + "/workers/tutorials/postgres", + "/workers/tutorials/send-emails-with-postmark", + "/workers/tutorials/send-emails-with-resend", + "/workers/tutorials/upload-assets-with-r2", + "/workers/tutorials/using-prisma-postgres-with-workers", + "/workers/tutorials/workers-kv-from-rust", + "/workers/vite-plugin", + "/workers/vite-plugin/get-started", + "/workers/vite-plugin/reference", + "/workers/vite-plugin/reference/api", + "/workers/vite-plugin/reference/cloudflare-environments", + "/workers/vite-plugin/reference/debugging", + "/workers/vite-plugin/reference/migrating-from-wrangler-dev", + "/workers/vite-plugin/reference/non-javascript-modules", + "/workers/vite-plugin/reference/programmatic-configuration", + "/workers/vite-plugin/reference/secrets", + "/workers/vite-plugin/reference/static-assets", + "/workers/vite-plugin/reference/vite-environments", + "/workers/vite-plugin/tutorial", + "/workers/wrangler", + "/workers/wrangler/api", + "/workers/wrangler/bundling", + "/workers/wrangler/commands", + "/workers/wrangler/commands/artifacts", + "/workers/wrangler/commands/browser", + "/workers/wrangler/commands/certificates", + "/workers/wrangler/commands/containers", + "/workers/wrangler/commands/d1", + "/workers/wrangler/commands/general", + "/workers/wrangler/commands/hyperdrive", + "/workers/wrangler/commands/kv", + "/workers/wrangler/commands/pages", + "/workers/wrangler/commands/pipelines", + "/workers/wrangler/commands/queues", + "/workers/wrangler/commands/r2", + "/workers/wrangler/commands/secrets-store", + "/workers/wrangler/commands/tunnel", + "/workers/wrangler/commands/vectorize", + "/workers/wrangler/commands/vpc", + "/workers/wrangler/commands/workers", + "/workers/wrangler/commands/workers-for-platforms", + "/workers/wrangler/commands/workflows", + "/workers/wrangler/configuration", + "/workers/wrangler/custom-builds", + "/workers/wrangler/deprecations", + "/workers/wrangler/environments", + "/workers/wrangler/install-and-update", + "/workers/wrangler/migration", + "/workers/wrangler/migration/update-v2-to-v3", + "/workers/wrangler/migration/update-v3-to-v4", + "/workers/wrangler/migration/v1-to-v2", + "/workers/wrangler/migration/v1-to-v2/eject-webpack", + "/workers/wrangler/migration/v1-to-v2/update-v1-to-v2", + "/workers/wrangler/migration/v1-to-v2/wrangler-legacy", + "/workers/wrangler/migration/v1-to-v2/wrangler-legacy/authentication", + "/workers/wrangler/migration/v1-to-v2/wrangler-legacy/commands", + "/workers/wrangler/migration/v1-to-v2/wrangler-legacy/configuration", + "/workers/wrangler/migration/v1-to-v2/wrangler-legacy/install-update", + "/workers/wrangler/migration/v1-to-v2/wrangler-legacy/webpack", + "/workers/wrangler/system-environment-variables", + "/workflows", + "/workflows/build", + "/workflows/build/call-workflows-from-pages", + "/workflows/build/events-and-parameters", + "/workflows/build/local-development", + "/workflows/build/rules-of-workflows", + "/workflows/build/sleeping-and-retrying", + "/workflows/build/step-context", + "/workflows/build/test-workflows", + "/workflows/build/trigger-workflows", + "/workflows/build/visualizer", + "/workflows/build/workers-api", + "/workflows/examples", + "/workflows/examples/agents", + "/workflows/examples/backup-d1", + "/workflows/examples/dynamic-workflows-link", + "/workflows/examples/send-invoices", + "/workflows/examples/twilio", + "/workflows/examples/wait-for-event", + "/workflows/get-started", + "/workflows/get-started/durable-agents", + "/workflows/get-started/guide", + "/workflows/observability", + "/workflows/observability/metrics-analytics", + "/workflows/python", + "/workflows/python/bindings", + "/workflows/python/dag", + "/workflows/python/python-workers-api", + "/workflows/reference", + "/workflows/reference/changelog", + "/workflows/reference/event-subscriptions", + "/workflows/reference/glossary", + "/workflows/reference/limits", + "/workflows/reference/pricing", + "/workflows/reference/wrangler-commands", + "/workflows/videos", + "/workflows/workflows-api", + "/zaraz", + "/zaraz/advanced", + "/zaraz/advanced/blocking-triggers", + "/zaraz/advanced/context-enricher", + "/zaraz/advanced/datalayer-compatibility", + "/zaraz/advanced/domains-not-proxied", + "/zaraz/advanced/google-consent-mode", + "/zaraz/advanced/import-export", + "/zaraz/advanced/load-custom-managed-component", + "/zaraz/advanced/load-selectively", + "/zaraz/advanced/load-zaraz-manually", + "/zaraz/advanced/logpush", + "/zaraz/advanced/using-jsonata", + "/zaraz/changelog", + "/zaraz/consent-management", + "/zaraz/consent-management/api", + "/zaraz/consent-management/custom-css", + "/zaraz/consent-management/enable-consent-management", + "/zaraz/consent-management/iab-tcf-compliance", + "/zaraz/custom-actions", + "/zaraz/custom-actions/additional-fields", + "/zaraz/custom-actions/create-action", + "/zaraz/custom-actions/create-trigger", + "/zaraz/custom-actions/edit-tools-and-actions", + "/zaraz/custom-actions/edit-triggers", + "/zaraz/embeds", + "/zaraz/faq", + "/zaraz/get-started", + "/zaraz/history", + "/zaraz/history/preview-mode", + "/zaraz/history/versions", + "/zaraz/http-events-api", + "/zaraz/monitoring", + "/zaraz/monitoring/monitoring-api", + "/zaraz/pricing-info", + "/zaraz/reference", + "/zaraz/reference/context", + "/zaraz/reference/properties-reference", + "/zaraz/reference/settings", + "/zaraz/reference/supported-tools", + "/zaraz/reference/triggers", + "/zaraz/variables", + "/zaraz/variables/create-variables", + "/zaraz/variables/edit-variables", + "/zaraz/variables/worker-variables", + "/zaraz/web-api", + "/zaraz/web-api/debug-mode", + "/zaraz/web-api/ecommerce", + "/zaraz/web-api/set", + "/zaraz/web-api/track" + ], + "opaqueNamespaces": [] +} diff --git a/.prettierignore b/.prettierignore index 19d0667aa0f..09c40cff5d8 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,10 @@ dist .astro + +# Nimbus migration target (vendored app code, behind BUILD_TARGET=nimbus). +# Excluded from repo formatting during priming; format/re-enable before cutover. +src/nimbus/ +dist-nimbus/ # fetched at prebuild/predev by bin/fetch-skills.ts .tmp # generated actions JS diff --git a/astro.config.ts b/astro.config.ts index 721a58f543f..fccb0deb962 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -96,24 +96,40 @@ const externalLinkPaths = await getExternalLinkPaths("src/content/docs"); const RUN_LINK_CHECK = process.env.RUN_LINK_CHECK?.toLowerCase() === "true" || false; +// Second build target, shared in place. Nimbus reads src/nimbus while +// Starlight stays at src; content/assets stay at src/content / src/assets. +const isNimbus = process.env.BUILD_TARGET === "nimbus"; + +// Nimbus markdown/integrations/vite, loaded only when active so the default +// Starlight build never pulls nimbus-docs or src/nimbus into its graph. +const nimbus = isNimbus ? await import("./src/nimbus/astro-config.ts") : null; + // https://astro.build/config export default defineConfig({ site: "https://developers.cloudflare.com", - cacheDir: ".astro-cache", - markdown: { - gfm: true, - smartypants: false, - remarkPlugins: [remarkValidateImages], - rehypePlugins: [ - rehypeMermaid, - rehypeExternalLinks, - rehypeHeadingSlugs, - rehypeAutolinkHeadings, - // @ts-expect-error plugins types are outdated but functional - rehypeTitleFigure, - rehypeShiftHeadings, - ], - }, + // Separate cache dir so the two targets' content stores never collide. + ...(isNimbus + ? { + srcDir: "./src/nimbus", + outDir: "./dist-nimbus", + cacheDir: ".astro-cache-nimbus", + } + : { cacheDir: ".astro-cache" }), + markdown: (nimbus + ? nimbus.markdown + : { + gfm: true, + smartypants: false, + remarkPlugins: [remarkValidateImages], + rehypePlugins: [ + rehypeMermaid, + rehypeExternalLinks, + rehypeHeadingSlugs, + rehypeAutolinkHeadings, + rehypeTitleFigure, + rehypeShiftHeadings, + ], + }) as Parameters[0]["markdown"], image: { service: { entrypoint: "astro/assets/services/sharp", @@ -128,147 +144,177 @@ export default defineConfig({ server: { port: 1111, }, - integrations: [ - starlight({ - title: "Cloudflare Docs", - logo: { - src: "./src/assets/logo.svg", - }, - favicon: "/favicon.png", - social: [ - { - label: "GitHub", - icon: "github", - href: "https://github.com/cloudflare/cloudflare-docs", - }, - { label: "X.com", icon: "x.com", href: "https://x.com/cloudflare" }, - { - label: "YouTube", - icon: "youtube", - href: "https://www.youtube.com/cloudflare", - }, - ], - editLink: { - baseUrl: - "https://github.com/cloudflare/cloudflare-docs/edit/production/", - }, - components: { - Banner: "./src/components/overrides/Banner.astro", - Footer: "./src/components/overrides/Footer.astro", - Head: "./src/components/overrides/Head.astro", - Header: "./src/components/overrides/Header.astro", - Hero: "./src/components/overrides/Hero.astro", - MarkdownContent: "./src/components/overrides/MarkdownContent.astro", - Sidebar: "./src/components/overrides/Sidebar.astro", - PageTitle: "./src/components/overrides/PageTitle.astro", - SkipLink: "./src/components/overrides/SkipLink.astro", - TableOfContents: "./src/components/overrides/TableOfContents.astro", - }, - sidebar, - customCss, - pagination: false, - plugins: [ - ...(RUN_LINK_CHECK - ? [ - starlightLinksValidator({ - failOnError: false, - errorOnInvalidHashes: false, - errorOnLocalLinks: false, - reporters: { - json: true, - }, - exclude: [ - "/api/", - "/api/**", - "/changelog/**", - "/http/resources/**", - "/llms.txt", - "/llms-full.txt", - "**/llms.txt", - "**/index.md", - "{props.*}", - "/", - "/glossary/", - "/directory/", - "/rules/snippets/examples/?operation=*", - "/rules/transform/examples/?operation=*", - "/ruleset-engine/rules-language/fields/reference/**", - "/workers/examples/?languages=*", - "/workers/llms-full.txt", - "/workers-ai/models/**", - "/markdown.zip", - "/style-guide/index.md", - "/agent-setup/", - "/videos/**", - ], - }), - ] - : []), - starlightDocSearch({ - clientOptionsModule: "./src/plugins/docsearch/index.ts", - }), - starlightImageZoom(), - starlightScrollToTop({ - tooltipText: "Back to top", - showTooltip: true, - svgPath: "M12 6L6 12M12 6L18 12M12 12L6 18M12 12L18 18", - showProgressRing: true, - progressRingColor: "white", - showOnHomepage: false, // Hide on homepage (default) + integrations: nimbus + ? nimbus.integrations + : [ + starlight({ + title: "Cloudflare Docs", + logo: { + src: "./src/assets/logo.svg", + }, + favicon: "/favicon.png", + social: [ + { + label: "GitHub", + icon: "github", + href: "https://github.com/cloudflare/cloudflare-docs", + }, + { label: "X.com", icon: "x.com", href: "https://x.com/cloudflare" }, + { + label: "YouTube", + icon: "youtube", + href: "https://www.youtube.com/cloudflare", + }, + ], + editLink: { + baseUrl: + "https://github.com/cloudflare/cloudflare-docs/edit/production/", + }, + components: { + Banner: "./src/components/overrides/Banner.astro", + Footer: "./src/components/overrides/Footer.astro", + Head: "./src/components/overrides/Head.astro", + Header: "./src/components/overrides/Header.astro", + Hero: "./src/components/overrides/Hero.astro", + MarkdownContent: "./src/components/overrides/MarkdownContent.astro", + Sidebar: "./src/components/overrides/Sidebar.astro", + PageTitle: "./src/components/overrides/PageTitle.astro", + SkipLink: "./src/components/overrides/SkipLink.astro", + TableOfContents: "./src/components/overrides/TableOfContents.astro", + }, + sidebar, + customCss, + pagination: false, + plugins: [ + ...(RUN_LINK_CHECK + ? [ + starlightLinksValidator({ + failOnError: false, + errorOnInvalidHashes: false, + errorOnLocalLinks: false, + reporters: { + json: true, + }, + exclude: [ + "/api/", + "/api/**", + "/changelog/**", + "/http/resources/**", + "/llms.txt", + "/llms-full.txt", + "**/llms.txt", + "**/index.md", + "{props.*}", + "/", + "/glossary/", + "/directory/", + "/rules/snippets/examples/?operation=*", + "/rules/transform/examples/?operation=*", + "/ruleset-engine/rules-language/fields/reference/**", + "/workers/examples/?languages=*", + "/workers/llms-full.txt", + "/workers-ai/models/**", + "/markdown.zip", + "/style-guide/index.md", + "/agent-setup/", + "/videos/**", + ], + }), + ] + : []), + starlightDocSearch({ + clientOptionsModule: "./src/plugins/docsearch/index.ts", + }), + starlightImageZoom(), + starlightScrollToTop({ + tooltipText: "Back to top", + showTooltip: true, + svgPath: "M12 6L6 12M12 6L18 12M12 12L6 18M12 12L18 18", + showProgressRing: true, + progressRingColor: "white", + showOnHomepage: false, // Hide on homepage (default) + }), + ], + lastUpdated: true, + markdown: { + headingLinks: false, + processedDirs: [ + "./src/content/partials/", + "./src/content/changelog/", + ], + }, + disable404Route: true, }), - ], - lastUpdated: true, - markdown: { - headingLinks: false, - processedDirs: ["./src/content/partials/", "./src/content/changelog/"], - }, - disable404Route: true, - }), - icon(), - sitemap({ - filter(page) { - if (page.includes("/style-guide/")) { - return false; - } + icon(), + sitemap({ + filter(page) { + if (page.includes("/style-guide/")) { + return false; + } - if (page.endsWith("/404/")) { - return false; - } + if (page.endsWith("/404/")) { + return false; + } - const pathname = new URL(page).pathname; + const pathname = new URL(page).pathname; - // Exclude external_link pages - if (externalLinkPaths.has(pathname)) { - return false; - } + // Exclude external_link pages + if (externalLinkPaths.has(pathname)) { + return false; + } - // Exclude pages disallowed in robots.txt - if (isDisallowedByRobots(pathname)) { - return false; - } + // Exclude pages disallowed in robots.txt + if (isDisallowedByRobots(pathname)) { + return false; + } - return true; - }, - serialize: createSitemapLastmodSerializer(), - }), - react(), - skills(), - ], + return true; + }, + serialize: createSitemapLastmodSerializer(), + }), + react(), + skills(), + ], vite: { - resolve: { - alias: { - "./Page.astro": fileURLToPath( - new URL("./src/components/overrides/Page.astro", import.meta.url), - ), - "../components/Page.astro": fileURLToPath( - new URL("./src/components/overrides/Page.astro", import.meta.url), - ), - "./SidebarSublist.astro": fileURLToPath( - new URL( - "./src/components/overrides/SidebarSublist.astro", - import.meta.url, - ), - ), + ...(nimbus + ? nimbus.vite + : { + resolve: { + alias: { + "./Page.astro": fileURLToPath( + new URL( + "./src/components/overrides/Page.astro", + import.meta.url, + ), + ), + "../components/Page.astro": fileURLToPath( + new URL( + "./src/components/overrides/Page.astro", + import.meta.url, + ), + ), + "./SidebarSublist.astro": fileURLToPath( + new URL( + "./src/components/overrides/SidebarSublist.astro", + import.meta.url, + ), + ), + }, + }, + }), + // Priming-only: both targets' outputs live in the repo root, so each + // target's dev watcher would otherwise enumerate the other's ~8.5k build + // files. Astro auto-ignores the active target's outDir; this adds the + // inactive one. Revisit at cutover (single target). Dev-watcher only — + // does not affect the production build. + server: { + watch: { + ignored: [ + "**/dist-nimbus/**", + "**/.astro-cache-nimbus/**", + "**/dist/**", + "**/.astro-cache/**", + ], }, }, }, diff --git a/eslint.config.js b/eslint.config.js index 80242ad09a6..554d74d075e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -33,7 +33,11 @@ export default [ ".flue/.flue-vite/", ".flue/.wrangler/", "dist/", + "dist-nimbus/", ".github/", + // Nimbus migration target (vendored, behind BUILD_TARGET=nimbus). + // Excluded from repo lint during priming; re-enable before cutover. + "src/nimbus/", ], }, { diff --git a/package.json b/package.json index 64c42f90704..f4800231797 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,12 @@ "@expressive-code/plugin-collapsible-sections": "0.43.1", "@expressive-code/plugin-line-numbers": "0.43.1", "@floating-ui/react": "0.27.19", + "@fontsource-variable/inter": "5.2.8", + "@fontsource-variable/jetbrains-mono": "5.2.8", "@iarna/toml": "2.2.5", + "@iconify-json/ph": "1.2.2", + "@iconify-json/simple-icons": "1.2.86", + "@iconify-json/vscode-icons": "1.2.56", "@iconify/utils": "3.1.3", "@marsidev/react-turnstile": "1.5.3", "@nanostores/react": "1.1.0", @@ -85,6 +90,7 @@ "astro-icon": "1.1.5", "astro-skills": "0.1.0", "cidr-tools": "12.0.3", + "clsx": "2.1.1", "codeowners-utils": "1.0.2", "date-fns": "4.4.0", "dedent": "1.7.2", @@ -117,6 +123,7 @@ "mermaid": "11.15.0", "micromark-extension-mdxjs": "3.0.0", "nanostores": "1.3.0", + "nimbus-docs": "0.1.19", "node-html-parser": "7.1.0", "openapi-types": "12.1.3", "parse-duration": "2.1.6", @@ -141,6 +148,7 @@ "remark": "15.0.1", "remark-gfm": "4.0.1", "remark-stringify": "11.0.0", + "satteri": "0.6.3", "sharp": "0.35.1", "solarflare-theme": "0.0.6", "space-separated-tokens": "2.0.2", @@ -151,6 +159,7 @@ "starlight-showcases": "0.3.2", "strip-markdown": "6.0.0", "svgo": "4.0.1", + "tailwind-merge": "3.6.0", "tailwindcss": "4.1.4", "tippy.js": "6.3.7", "ts-blank-space": "0.9.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b1d080e2bd..99760a7d96f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,7 +25,7 @@ importers: version: 7.2.0 '@astrojs/mdx': specifier: 6.0.3 - version: 6.0.3(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 6.0.3(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0)) '@astrojs/react': specifier: 5.0.7 version: 5.0.7(@types/node@25.9.3)(@types/react-dom@19.0.4(@types/react@19.0.7))(@types/react@19.0.7)(jiti@2.7.0)(lightningcss@1.32.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(tsx@4.22.4)(yaml@2.9.0) @@ -37,16 +37,16 @@ importers: version: 3.7.3 '@astrojs/starlight': specifier: 0.40.0 - version: 0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3) + version: 0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3) '@astrojs/starlight-docsearch': specifier: 0.7.0 - version: 0.7.0(@algolia/client-search@5.54.1)(@astrojs/starlight@0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(search-insights@2.17.3) + version: 0.7.0(@algolia/client-search@5.54.1)(@astrojs/starlight@0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(search-insights@2.17.3) '@astrojs/starlight-tailwind': specifier: 5.0.0 - version: 5.0.0(@astrojs/starlight@0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3))(tailwindcss@4.1.4) + version: 5.0.0(@astrojs/starlight@0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3))(tailwindcss@4.1.4) '@base-ui/react': specifier: 1.5.0 - version: 1.5.0(@types/react@19.0.7)(date-fns@4.4.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 1.5.0(@date-fns/tz@1.5.0)(@types/react@19.0.7)(date-fns@4.4.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@cloudflare/vitest-pool-workers': specifier: 0.16.15 version: 0.16.15(@cloudflare/workers-types@4.20260615.1)(@vitest/runner@4.1.9)(@vitest/snapshot@4.1.9)(vitest@4.1.9(@types/node@25.9.3)(happy-dom@20.10.3)(vite@7.3.5(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0))) @@ -68,9 +68,24 @@ importers: '@floating-ui/react': specifier: 0.27.19 version: 0.27.19(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@fontsource-variable/inter': + specifier: 5.2.8 + version: 5.2.8 + '@fontsource-variable/jetbrains-mono': + specifier: 5.2.8 + version: 5.2.8 '@iarna/toml': specifier: 2.2.5 version: 2.2.5 + '@iconify-json/ph': + specifier: 1.2.2 + version: 1.2.2 + '@iconify-json/simple-icons': + specifier: 1.2.86 + version: 1.2.86 + '@iconify-json/vscode-icons': + specifier: 1.2.56 + version: 1.2.56 '@iconify/utils': specifier: 3.1.3 version: 3.1.3 @@ -149,6 +164,9 @@ importers: cidr-tools: specifier: 12.0.3 version: 12.0.3 + clsx: + specifier: 2.1.1 + version: 2.1.1 codeowners-utils: specifier: 1.0.2 version: 1.0.2 @@ -245,6 +263,9 @@ importers: nanostores: specifier: 1.3.0 version: 1.3.0 + nimbus-docs: + specifier: 0.1.19 + version: 0.1.19(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(shiki@4.2.0) node-html-parser: specifier: 7.1.0 version: 7.1.0 @@ -317,6 +338,9 @@ importers: remark-stringify: specifier: 11.0.0 version: 11.0.0 + satteri: + specifier: 0.6.3 + version: 0.6.3 sharp: specifier: 0.35.1 version: 0.35.1 @@ -328,25 +352,28 @@ importers: version: 2.0.2 starlight-image-zoom: specifier: 0.14.2 - version: 0.14.2(@astrojs/starlight@0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3)) + version: 0.14.2(@astrojs/starlight@0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3)) starlight-links-validator: specifier: 0.24.1 - version: 0.24.1(@astrojs/starlight@0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3))(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 0.24.1(@astrojs/starlight@0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3))(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0)) starlight-package-managers: specifier: 0.12.0 - version: 0.12.0(@astrojs/starlight@0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3)) + version: 0.12.0(@astrojs/starlight@0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3)) starlight-scroll-to-top: specifier: 1.0.1 - version: 1.0.1(@astrojs/starlight@0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3)) + version: 1.0.1(@astrojs/starlight@0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3)) starlight-showcases: specifier: 0.3.2 - version: 0.3.2(@astrojs/starlight@0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3)) + version: 0.3.2(@astrojs/starlight@0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3)) strip-markdown: specifier: 6.0.0 version: 6.0.0 svgo: specifier: 4.0.1 version: 4.0.1 + tailwind-merge: + specifier: 3.6.0 + version: 3.6.0 tailwindcss: specifier: 4.1.4 version: 4.1.4 @@ -563,6 +590,19 @@ packages: '@astrojs/markdown-remark@7.2.0': resolution: {integrity: sha512-+YxmVQu1Bd+MFfSzjq1rOJvD9+nIOJzz5YIIhdIH01RrxRkKbyKoEgyIqP3yv51MhzMDgd79QaPv+kCVPT8vHw==} + '@astrojs/markdown-satteri@0.2.1': + resolution: {integrity: sha512-rsdT8ucMLmu1v/POYApJcfH/t30Yuae6pZdfFU0yG3Kya8pwNItcJ6TLqUACRlRV9W50g4gLIxCwwfWjKsJ67w==} + + '@astrojs/mdx@6.0.1': + resolution: {integrity: sha512-J5K8F7A1LMH+cj+dcxm+uAeIznkfwxMcRpG7DD6ABNDOt8da98ph6ie4TD+4FV/ojVOJK0PQGWY4hmxQORLv0w==} + engines: {node: '>=22.12.0'} + peerDependencies: + '@astrojs/markdown-satteri': 0.2.1 + astro: ^6.4.0 + peerDependenciesMeta: + '@astrojs/markdown-satteri': + optional: true + '@astrojs/mdx@6.0.3': resolution: {integrity: sha512-+4P3ZvwsRAqAbBgY+uZMewFo3ficlIBPZfu/Luk+v4ia/ZOuFhpsw7r+7672uT2Fc1UPdp7yW0eU5egvSq0wbw==} engines: {node: '>=22.12.0'} @@ -739,6 +779,32 @@ packages: '@braintree/sanitize-url@7.1.2': resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} + '@bruits/satteri-darwin-arm64@0.6.3': + resolution: {integrity: sha512-oKgMfpmNzQ8vaqmkE37PBu8tOyVjoOc4s+DV2tLpWvO6WO467qn/+Nbcirm/ceer7wUM8v4vMLGcZO0gkRzBEg==} + cpu: [arm64] + os: [darwin] + + '@bruits/satteri-darwin-x64@0.6.3': + resolution: {integrity: sha512-9I5pbwZRWH5LvhoCtwpRr4rYSDe43/dLvps6zO70ipVF2XbH4rJ20T+EfvPcmou5jsWMsq9Ybn5GX3PwlSrBaw==} + cpu: [x64] + os: [darwin] + + '@bruits/satteri-linux-x64-gnu@0.6.3': + resolution: {integrity: sha512-aFfw2DL2HpIcAQ8I3ZEtKuz+/GoF0H0sq387jAlvr00Q7buiiFbvARFuQlvTk00N7u3SJIh/3+YkKssTJDT+yQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@bruits/satteri-wasm32-wasi@0.6.3': + resolution: {integrity: sha512-DdpfnJ+04Mb4YtHaxAeETUvhdxFKg3URnroGY39FvweJEgeXx8cFNutuF5w904BhqaiblhmF76AoBnJwNLxsXg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@bruits/satteri-win32-x64-msvc@0.6.3': + resolution: {integrity: sha512-J/CZqACnBbv77eImx/JeO5RmCuCyliihiC81u3M4VobA8eupsygaPen3/UFFqf3yfeHvMlE3myilouh/2iHMOA==} + cpu: [x64] + os: [win32] + '@capsizecss/unpack@4.0.1': resolution: {integrity: sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==} engines: {node: '>=18'} @@ -746,20 +812,26 @@ packages: '@chevrotain/types@11.1.2': resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} + '@clack/core@0.4.1': + resolution: {integrity: sha512-Pxhij4UXg8KSr7rPek6Zowm+5M22rbd2g1nfojHJkxp5YkFqiZ2+YLEM/XGVIzvGOcM0nqjIFxrpDwWRZYWYjA==} + '@clack/core@1.4.1': resolution: {integrity: sha512-FILJa1gGKEFTGZAJE9RpVhrjKz3c3h4ar60dSv6cGuDqufQ84YEIS3GAGvZiN+H6yaLbbvTFNejjCC4tXpZEuw==} engines: {node: '>= 20.12.0'} + '@clack/prompts@0.9.1': + resolution: {integrity: sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==} + '@clack/prompts@1.5.1': resolution: {integrity: sha512-zccHj2z2oCCO4yrDiRSlFOxWerGqRiysP7a5jPK6uoI9URKAquwY42Dd/iUP8JWHxEzdRe4TlbvZCo8z1/mhrw==} engines: {node: '>= 20.12.0'} '@cloudflare/kv-asset-handler@0.5.0': - resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==, tarball: https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz} + resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} engines: {node: '>=22.0.0'} '@cloudflare/unenv-preset@2.16.1': - resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==, tarball: https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz} + resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} peerDependencies: unenv: 2.0.0-rc.24 workerd: '>1.20260305.0 <2.0.0-0' @@ -768,44 +840,44 @@ packages: optional: true '@cloudflare/vitest-pool-workers@0.16.15': - resolution: {integrity: sha512-R0kZhIm4uSxOeTWPHY9xYIFPGRBEHPzl/n9BbHZSY/gk0n16uDU7T1JZe372oTF+diXG1uVBWqiiRc7Hxstdow==, tarball: https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.16.15.tgz} + resolution: {integrity: sha512-R0kZhIm4uSxOeTWPHY9xYIFPGRBEHPzl/n9BbHZSY/gk0n16uDU7T1JZe372oTF+diXG1uVBWqiiRc7Hxstdow==} peerDependencies: '@vitest/runner': ^4.1.0 '@vitest/snapshot': ^4.1.0 vitest: ^4.1.0 '@cloudflare/workerd-darwin-64@1.20260611.1': - resolution: {integrity: sha512-iJICldmi4sBGgi7IrQles8cStOGXM/Tmv95C4OODVs6VIbMsJPqThUM5h3uYVQNULuJ8I/aVvnJ3Eh/wZCKwuA==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260611.1.tgz} + resolution: {integrity: sha512-iJICldmi4sBGgi7IrQles8cStOGXM/Tmv95C4OODVs6VIbMsJPqThUM5h3uYVQNULuJ8I/aVvnJ3Eh/wZCKwuA==} engines: {node: '>=16'} cpu: [x64] os: [darwin] '@cloudflare/workerd-darwin-arm64@1.20260611.1': - resolution: {integrity: sha512-yBbVXvbZyltR3I7NJdC4C4ItkItjZSiabcA/3HzEWOUQjLVKFqRh4so6ToHr70VCYh8VGeR8EDZL23igLhXqFQ==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260611.1.tgz} + resolution: {integrity: sha512-yBbVXvbZyltR3I7NJdC4C4ItkItjZSiabcA/3HzEWOUQjLVKFqRh4so6ToHr70VCYh8VGeR8EDZL23igLhXqFQ==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] '@cloudflare/workerd-linux-64@1.20260611.1': - resolution: {integrity: sha512-PfNjpxOlaIgZFYuhD7+neEEewCN2Ud993wEEN0fmbtSOax1AK53LGqmXUDvFhnbkHxJLFAxYCSNISW8QbzaAIg==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260611.1.tgz} + resolution: {integrity: sha512-PfNjpxOlaIgZFYuhD7+neEEewCN2Ud993wEEN0fmbtSOax1AK53LGqmXUDvFhnbkHxJLFAxYCSNISW8QbzaAIg==} engines: {node: '>=16'} cpu: [x64] os: [linux] '@cloudflare/workerd-linux-arm64@1.20260611.1': - resolution: {integrity: sha512-GEp4XbuIKjlF8pakqXcUDJfKiJosD/Q7S83J0d+r+z9XIlYGfF3ntm08e2aiF5TFTwp3fnG4yMoPUAKNhNJpvQ==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260611.1.tgz} + resolution: {integrity: sha512-GEp4XbuIKjlF8pakqXcUDJfKiJosD/Q7S83J0d+r+z9XIlYGfF3ntm08e2aiF5TFTwp3fnG4yMoPUAKNhNJpvQ==} engines: {node: '>=16'} cpu: [arm64] os: [linux] '@cloudflare/workerd-windows-64@1.20260611.1': - resolution: {integrity: sha512-S6JkS0kEbcCKs19RGqEPhjCRbP8GBkQwqYLp2fhBJtD/KTlwqLzOJ9E6PQ7gQKgWHtxy1NBG3oXarlNFRNU/dw==, tarball: https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260611.1.tgz} + resolution: {integrity: sha512-S6JkS0kEbcCKs19RGqEPhjCRbP8GBkQwqYLp2fhBJtD/KTlwqLzOJ9E6PQ7gQKgWHtxy1NBG3oXarlNFRNU/dw==} engines: {node: '>=16'} cpu: [x64] os: [win32] '@cloudflare/workers-types@4.20260615.1': - resolution: {integrity: sha512-fGOiTwoLj/8bU8mj3VAfa1EULx4ceZhDwnjvY+afDBlSXI9pvY7PE9t62rGEhJjbAOGd7i5WUDun0eZCWBDrzg==, tarball: https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260615.1.tgz} + resolution: {integrity: sha512-fGOiTwoLj/8bU8mj3VAfa1EULx4ceZhDwnjvY+afDBlSXI9pvY7PE9t62rGEhJjbAOGd7i5WUDun0eZCWBDrzg==} '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} @@ -815,6 +887,9 @@ packages: resolution: {integrity: sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==} engines: {node: '>=14'} + '@date-fns/tz@1.5.0': + resolution: {integrity: sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==} + '@docsearch/css@3.9.0': resolution: {integrity: sha512-cQbnVbq0rrBwNAKegIac/t6a8nWoUAn8frnkLFW6YARaRmAQr5/Eoe6Ln2fqkUCZ40KpdrKbpSAmgrkviOxuWA==} @@ -859,9 +934,18 @@ packages: '@emmetio/stream-reader@2.2.0': resolution: {integrity: sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==} + '@emnapi/core@1.9.1': + resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} + '@emnapi/runtime@1.11.1': resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/runtime@1.9.1': + resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} + + '@emnapi/wasi-threads@1.2.0': + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + '@emotion/babel-plugin@11.13.5': resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} @@ -1464,6 +1548,12 @@ packages: '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@fontsource-variable/inter@5.2.8': + resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==} + + '@fontsource-variable/jetbrains-mono@5.2.8': + resolution: {integrity: sha512-WBA9elru6Jdp5df2mES55wuOO0WIrn3kpXnI4+W2ek5u3ZgLS9XS4gmIlcQhiZOWEKl95meYdvK7xI+ETLCq/Q==} + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -1487,6 +1577,15 @@ packages: '@iarna/toml@2.2.5': resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} + '@iconify-json/ph@1.2.2': + resolution: {integrity: sha512-PgkEZNtqa8hBGjHXQa4pMwZa93hmfu8FUSjs/nv4oUU6yLsgv+gh9nu28Kqi8Fz9CCVu4hj1MZs9/60J57IzFw==} + + '@iconify-json/simple-icons@1.2.86': + resolution: {integrity: sha512-t3jck5qPQuK1qy+bRn9eCoDQhIB7XSazKz1Fjp8hcan3XOAsTI5Mq/s3F0ekOKSvMQqkVORYK6ns6o6T9f5EMA==} + + '@iconify-json/vscode-icons@1.2.56': + resolution: {integrity: sha512-AZYFHK0IuynkOwO4h22IGZQ4+2/dHAZocUHSO9rPGwU6MrZtzAyb6atdO6iEocHOcB5PQdG7xmCkRCYsGTo3iQ==} + '@iconify/tools@4.2.0': resolution: {integrity: sha512-WRxPva/ipxYkqZd1+CkEAQmd86dQmrwH0vwK89gmp2Kh2WyyVw57XbPng0NehP3x4V1LzLsXUneP1uMfTMZmUA==} @@ -1874,6 +1973,12 @@ packages: nanostores: ^1.2.0 react: '>=18.0.0' + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@nodable/entities@2.2.0': resolution: {integrity: sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==} @@ -2172,6 +2277,10 @@ packages: resolution: {integrity: sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ==} engines: {node: '>=20'} + '@shikijs/core@4.3.0': + resolution: {integrity: sha512-EooU3i9F6IAE8kEu+AnGf9DFZWkQBZ+hJn3tLVbsH+61mtQiva5biai66fAA6nvFPXkLgvrh7BrR7YcJU83xQQ==} + engines: {node: '>=20'} + '@shikijs/engine-javascript@4.2.0': resolution: {integrity: sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og==} engines: {node: '>=20'} @@ -2188,14 +2297,26 @@ packages: resolution: {integrity: sha512-NOq+DtUkVBJtZMVXL5A0vI0Xk8nvDYaXetFHSJFlOqjDZIVhIPRYFdGkSoElDqNuegikcc3A76SNUa8dTqtAYA==} engines: {node: '>=20'} + '@shikijs/primitive@4.3.0': + resolution: {integrity: sha512-CPkz64PTa5diRW1ggzMZH9VM/du4RNChYgVtgqrFcgruvIybmCvySv8GkiHSczUHXYuuR8TdKEwFx+UnZMpgdg==} + engines: {node: '>=20'} + '@shikijs/themes@4.2.0': resolution: {integrity: sha512-RX8IHYeLv8Cu2W6ruc3RxUqWn0IYCqSrMBzi/uRGAmfyDNOnNO5BF/Px7o97n4XTpmFTo5GbRaazuOWj+2ak2w==} engines: {node: '>=20'} + '@shikijs/transformers@4.3.0': + resolution: {integrity: sha512-5/elhJEcUrxdyQ95SSx0HzrnbzVPuipk6TYiXZL67tHhw8J+4N5shzmrTYyCaudiSnpuuwPdpaVtciwRFwSA7A==} + engines: {node: '>=20'} + '@shikijs/types@4.2.0': resolution: {integrity: sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw==} engines: {node: '>=20'} + '@shikijs/types@4.3.0': + resolution: {integrity: sha512-oc8b9U2SYvofKZk8e/737nIX0qwf6eV2vHFATeObAu7r+mUVpLs8Re0BmVkIjAWAYgkmG/CzLNo7rzuBzRu/wQ==} + engines: {node: '>=20'} + '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -2343,6 +2464,9 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -2607,6 +2731,10 @@ packages: '@upsetjs/venn.js@2.0.0': resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} + '@vercel/detect-agent@1.2.3': + resolution: {integrity: sha512-VYNCgUc0nOmC4WJmWw9GkrKdfr8Zl4/rxhC5SvgacBgxiW9W/9NRttUoHHXV8xdII3MaRgkZZVX8Ikzc/Jmjag==} + engines: {node: '>=14'} + '@vitejs/plugin-react@5.2.0': resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4884,6 +5012,10 @@ packages: resolution: {integrity: sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==} engines: {node: '>=18.0.0'} + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -4913,6 +5045,20 @@ packages: resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} engines: {node: '>= 10'} + nimbus-docs@0.1.19: + resolution: {integrity: sha512-v/VUl+qm9rckB/kK0lH37i37/ZEjXrC0wEU3paX6sgtwwqM8MakJxWSnwN6KixWKGKYp8ft0qoXrGGX8TdggCQ==} + hasBin: true + peerDependencies: + astro: '>=6.4.0' + react: '>=19.0.0' + react-dom: '>=19.0.0' + shiki: ^4.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + nlcst-to-string@4.0.0: resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} @@ -5300,6 +5446,9 @@ packages: resolution: {integrity: sha512-So0Sqw869y/S2oE3Nuc0uT3Dhqgvsj8FSrwBdsuTosVsG8ME5/OcudU1GxsrIFdFABgy17GHnTVO9TYV/bLQcA==} engines: {node: '>=16.0.0'} + quotation@2.0.3: + resolution: {integrity: sha512-yEc24TEgCFLXx7D4JHJJkK4JFVtatO8fziwUxY4nB/Jbea9o9CVS3gt22mA0W7rPYAGW2fWzYDSOtD94PwOyqA==} + radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} @@ -5440,6 +5589,30 @@ packages: remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + remark-lint-emphasis-marker@4.0.1: + resolution: {integrity: sha512-BF1WWsAxai3XoKk48sfiqT3L8m02AZLj3BnipWkHDRXuLfz6VwsHVaHWyNvvE0p6b2B3A5dSYbcfJu5RmPx4tQ==} + + remark-lint-fenced-code-flag@4.2.0: + resolution: {integrity: sha512-QWGTrnYbcopOFZR98djDREmKApLonJ7hmXE7pEcOGee9JY/EUIVS7Lq54Hy9CtU3cVIvQQmiMTxCwUhfddDJFA==} + + remark-lint-heading-increment@4.0.1: + resolution: {integrity: sha512-uat7RTQn0hGlMv62p7yjLlg3tO3RljFbH6C+0M+5BNEF+s3NrA8jJgqW0UwLLNdCd3EABCKaWloHumT57ND7PQ==} + + remark-lint-no-duplicate-headings@4.0.1: + resolution: {integrity: sha512-6lggqnpIe5FepikjYF2me3ovKV4oD/rAz8WmwVbLR2cLkce1iH+PB7jyxk/A2gQQqrDcIlRMA5Ct2Yj56cEwhQ==} + + remark-lint-no-heading-punctuation@4.0.1: + resolution: {integrity: sha512-lpSVFEHPDKGWi8YPeO51xmLNVON5A2cGz0Y8VRkW0f2l6LvEkPTMjQAvA84AQu/10TrxTbIzU/tQlRLpG96QUA==} + + remark-lint-no-literal-urls@4.0.1: + resolution: {integrity: sha512-RhTANFkFFXE6bM+WxWcPo2TTPEfkWG3lJZU50ycW7tJJmxUzDNzRed/z80EVJIdGwFa0NntVooLUJp3xrogalQ==} + + remark-lint-no-multiple-toplevel-headings@4.0.1: + resolution: {integrity: sha512-8sepobIOu3PlDOuMH7jtri+LH4tFNVQU+aqKSkrlNRdp831fYz9S+jA2crTVqWqxVbTwiF96uJWePv8/9qmHnA==} + + remark-lint-unordered-list-marker-style@4.0.1: + resolution: {integrity: sha512-HMrVQC0Qbr8ktSy+1lJGRGU10qecL3T14L6s/THEQXR5Tk0wcsLLG0auNvB4r2+H+ClhVO/Vnm1TEosh1OCsfw==} + remark-mdx@3.1.1: resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==} @@ -5560,6 +5733,9 @@ packages: sass-formatter@0.7.9: resolution: {integrity: sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==} + satteri@0.6.3: + resolution: {integrity: sha512-iY5xd2tBDQveYRFkL1F0cegkabWSVoXHi64e1p49SiCs1bZDaqTQPGQI+PqEQIaWkz0iqK80CuQQUYvhsssviw==} + sax@1.6.0: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} @@ -5870,6 +6046,9 @@ packages: tabbable@6.4.0: resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + tailwind-merge@3.6.0: + resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} + tailwindcss@4.1.4: resolution: {integrity: sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==} @@ -6074,6 +6253,9 @@ packages: unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + unified-lint-rule@3.0.1: + resolution: {integrity: sha512-HxIeQOmwL19DGsxHXbeyzKHBsoSCFO7UtRVUvT2v61ptw/G+GbysWcrpHdfs5jqbIFDA11MoKngIhQK0BeTVjA==} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -6897,7 +7079,35 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/mdx@6.0.3(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))': + '@astrojs/markdown-satteri@0.2.1': + dependencies: + '@astrojs/internal-helpers': 0.10.0 + github-slugger: 2.0.0 + satteri: 0.6.3 + + '@astrojs/mdx@6.0.1(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))': + dependencies: + '@astrojs/internal-helpers': 0.10.0 + '@astrojs/markdown-remark': 7.2.0 + '@mdx-js/mdx': 3.1.1 + acorn: 8.17.0 + astro: 6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0) + es-module-lexer: 2.1.0 + estree-util-visit: 2.0.0 + hast-util-to-html: 9.0.5 + piccolore: 0.1.3 + rehype-raw: 7.0.0 + remark-gfm: 4.0.1 + remark-smartypants: 3.0.2 + source-map: 0.7.6 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + optionalDependencies: + '@astrojs/markdown-satteri': 0.2.1 + transitivePeerDependencies: + - supports-color + + '@astrojs/mdx@6.0.3(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: '@astrojs/internal-helpers': 0.10.0 '@astrojs/markdown-remark': 7.2.0 @@ -6914,6 +7124,8 @@ snapshots: source-map: 0.7.6 unist-util-visit: 5.1.0 vfile: 6.0.3 + optionalDependencies: + '@astrojs/markdown-satteri': 0.2.1 transitivePeerDependencies: - supports-color @@ -6958,9 +7170,9 @@ snapshots: stream-replace-string: 2.0.0 zod: 4.4.3 - '@astrojs/starlight-docsearch@0.7.0(@algolia/client-search@5.54.1)(@astrojs/starlight@0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(search-insights@2.17.3)': + '@astrojs/starlight-docsearch@0.7.0(@algolia/client-search@5.54.1)(@astrojs/starlight@0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(search-insights@2.17.3)': dependencies: - '@astrojs/starlight': 0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3) + '@astrojs/starlight': 0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3) '@docsearch/css': 3.9.0 '@docsearch/js': 3.9.0(@algolia/client-search@5.54.1)(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(search-insights@2.17.3) transitivePeerDependencies: @@ -6970,15 +7182,15 @@ snapshots: - react-dom - search-insights - '@astrojs/starlight-tailwind@5.0.0(@astrojs/starlight@0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3))(tailwindcss@4.1.4)': + '@astrojs/starlight-tailwind@5.0.0(@astrojs/starlight@0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3))(tailwindcss@4.1.4)': dependencies: - '@astrojs/starlight': 0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3) + '@astrojs/starlight': 0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3) tailwindcss: 4.1.4 - '@astrojs/starlight@0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3)': + '@astrojs/starlight@0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3)': dependencies: '@astrojs/markdown-remark': 7.2.0 - '@astrojs/mdx': 6.0.3(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0)) + '@astrojs/mdx': 6.0.3(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0)) '@astrojs/sitemap': 3.7.3 '@pagefind/default-ui': 1.5.2 '@types/hast': 3.0.4 @@ -7006,6 +7218,8 @@ snapshots: unified: 11.0.5 unist-util-visit: 5.1.0 vfile: 6.0.3 + optionalDependencies: + '@astrojs/markdown-satteri': 0.2.1 transitivePeerDependencies: - supports-color - typescript @@ -7136,7 +7350,7 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@base-ui/react@1.5.0(@types/react@19.0.7)(date-fns@4.4.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@base-ui/react@1.5.0(@date-fns/tz@1.5.0)(@types/react@19.0.7)(date-fns@4.4.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.29.7 '@base-ui/utils': 0.2.9(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -7146,6 +7360,7 @@ snapshots: react-dom: 19.0.0(react@19.0.0) use-sync-external-store: 1.6.0(react@19.0.0) optionalDependencies: + '@date-fns/tz': 1.5.0 '@types/react': 19.0.7 date-fns: 4.4.0 @@ -7164,17 +7379,47 @@ snapshots: '@braintree/sanitize-url@7.1.2': {} + '@bruits/satteri-darwin-arm64@0.6.3': + optional: true + + '@bruits/satteri-darwin-x64@0.6.3': + optional: true + + '@bruits/satteri-linux-x64-gnu@0.6.3': + optional: true + + '@bruits/satteri-wasm32-wasi@0.6.3': + dependencies: + '@emnapi/core': 1.9.1 + '@emnapi/runtime': 1.9.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + optional: true + + '@bruits/satteri-win32-x64-msvc@0.6.3': + optional: true + '@capsizecss/unpack@4.0.1': dependencies: fontkitten: 1.0.3 '@chevrotain/types@11.1.2': {} + '@clack/core@0.4.1': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@clack/core@1.4.1': dependencies: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 + '@clack/prompts@0.9.1': + dependencies: + '@clack/core': 0.4.1 + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@clack/prompts@1.5.1': dependencies: '@clack/core': 1.4.1 @@ -7228,6 +7473,9 @@ snapshots: '@ctrl/tinycolor@4.2.0': {} + '@date-fns/tz@1.5.0': + optional: true + '@docsearch/css@3.9.0': {} '@docsearch/js@3.9.0(@algolia/client-search@5.54.1)(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(search-insights@2.17.3)': @@ -7278,11 +7526,27 @@ snapshots: '@emmetio/stream-reader@2.2.0': {} + '@emnapi/core@1.9.1': + dependencies: + '@emnapi/wasi-threads': 1.2.0 + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.9.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emotion/babel-plugin@11.13.5': dependencies: '@babel/helper-module-imports': 7.29.7 @@ -7691,6 +7955,10 @@ snapshots: '@floating-ui/utils@0.2.11': {} + '@fontsource-variable/inter@5.2.8': {} + + '@fontsource-variable/jetbrains-mono@5.2.8': {} + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -7709,6 +7977,18 @@ snapshots: '@iarna/toml@2.2.5': {} + '@iconify-json/ph@1.2.2': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify-json/simple-icons@1.2.86': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify-json/vscode-icons@1.2.56': + dependencies: + '@iconify/types': 2.0.0 + '@iconify/tools@4.2.0': dependencies: '@iconify/types': 2.0.0 @@ -8042,6 +8322,13 @@ snapshots: nanostores: 1.3.0 react: 19.0.0 + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + dependencies: + '@emnapi/core': 1.9.1 + '@emnapi/runtime': 1.9.1 + '@tybys/wasm-util': 0.10.3 + optional: true + '@nodable/entities@2.2.0': {} '@nodelib/fs.scandir@2.1.5': @@ -8289,6 +8576,14 @@ snapshots: '@types/hast': 3.0.4 hast-util-to-html: 9.0.5 + '@shikijs/core@4.3.0': + dependencies: + '@shikijs/primitive': 4.3.0 + '@shikijs/types': 4.3.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + '@shikijs/engine-javascript@4.2.0': dependencies: '@shikijs/types': 4.2.0 @@ -8310,15 +8605,31 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + '@shikijs/primitive@4.3.0': + dependencies: + '@shikijs/types': 4.3.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + '@shikijs/themes@4.2.0': dependencies: '@shikijs/types': 4.2.0 + '@shikijs/transformers@4.3.0': + dependencies: + '@shikijs/core': 4.3.0 + '@shikijs/types': 4.3.0 + '@shikijs/types@4.2.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + '@shikijs/types@4.3.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + '@shikijs/vscode-textmate@10.0.2': {} '@sindresorhus/is@7.2.0': {} @@ -8458,6 +8769,11 @@ snapshots: '@tokenizer/token@0.3.0': {} + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': @@ -8787,6 +9103,8 @@ snapshots: d3-selection: 3.0.0 d3-transition: 3.0.1(d3-selection@3.0.0) + '@vercel/detect-agent@1.2.3': {} + '@vitejs/plugin-react@5.2.0(vite@7.3.5(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.7 @@ -11854,6 +12172,8 @@ snapshots: modern-tar@0.7.6: {} + mri@1.2.0: {} + mrmime@2.0.1: {} ms@2.1.3: {} @@ -11871,6 +12191,38 @@ snapshots: neotraverse@0.6.18: {} + nimbus-docs@0.1.19(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(shiki@4.2.0): + dependencies: + '@astrojs/markdown-satteri': 0.2.1 + '@astrojs/mdx': 6.0.1(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0)) + '@astrojs/sitemap': 3.7.3 + '@clack/prompts': 0.9.1 + '@shikijs/transformers': 4.3.0 + '@vercel/detect-agent': 1.2.3 + astro: 6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0) + clsx: 2.1.1 + github-slugger: 2.0.0 + mri: 1.2.0 + remark-lint-emphasis-marker: 4.0.1 + remark-lint-fenced-code-flag: 4.2.0 + remark-lint-heading-increment: 4.0.1 + remark-lint-no-duplicate-headings: 4.0.1 + remark-lint-no-heading-punctuation: 4.0.1 + remark-lint-no-literal-urls: 4.0.1 + remark-lint-no-multiple-toplevel-headings: 4.0.1 + remark-lint-unordered-list-marker-style: 4.0.1 + satteri: 0.6.3 + shiki: 4.2.0 + tailwind-merge: 3.6.0 + unified: 11.0.5 + vfile: 6.0.3 + yaml: 2.9.0 + optionalDependencies: + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + transitivePeerDependencies: + - supports-color + nlcst-to-string@4.0.0: dependencies: '@types/nlcst': 2.0.3 @@ -12258,6 +12610,8 @@ snapshots: '@jitl/quickjs-wasmfile-release-sync': 0.32.0 quickjs-emscripten-core: 0.32.0 + quotation@2.0.3: {} + radix3@1.1.2: {} rc@1.2.8: @@ -12501,6 +12855,85 @@ snapshots: transitivePeerDependencies: - supports-color + remark-lint-emphasis-marker@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + unified-lint-rule: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit-parents: 6.0.2 + vfile-message: 4.0.3 + + remark-lint-fenced-code-flag@4.2.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-phrasing: 4.1.0 + quotation: 2.0.3 + unified-lint-rule: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit-parents: 6.0.2 + + remark-lint-heading-increment@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-mdx: 3.0.0 + unified-lint-rule: 3.0.1 + unist-util-visit-parents: 6.0.2 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + remark-lint-no-duplicate-headings@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-mdx: 3.0.0 + mdast-util-to-string: 4.0.0 + unified-lint-rule: 3.0.1 + unist-util-visit-parents: 6.0.2 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + remark-lint-no-heading-punctuation@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-mdx: 3.0.0 + mdast-util-to-string: 4.0.0 + unified-lint-rule: 3.0.1 + unist-util-visit-parents: 6.0.2 + transitivePeerDependencies: + - supports-color + + remark-lint-no-literal-urls@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-string: 4.0.0 + micromark-util-character: 2.1.1 + unified-lint-rule: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit-parents: 6.0.2 + + remark-lint-no-multiple-toplevel-headings@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-mdx: 3.0.0 + unified-lint-rule: 3.0.1 + unist-util-visit-parents: 6.0.2 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + remark-lint-unordered-list-marker-style@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-phrasing: 4.1.0 + unified-lint-rule: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit-parents: 6.0.2 + vfile-message: 4.0.3 + remark-mdx@3.1.1: dependencies: mdast-util-mdx: 3.0.0 @@ -12689,6 +13122,19 @@ snapshots: dependencies: suf-log: 2.5.3 + satteri@0.6.3: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + optionalDependencies: + '@bruits/satteri-darwin-arm64': 0.6.3 + '@bruits/satteri-darwin-x64': 0.6.3 + '@bruits/satteri-linux-x64-gnu': 0.6.3 + '@bruits/satteri-wasm32-wasi': 0.6.3 + '@bruits/satteri-win32-x64-msvc': 0.6.3 + sax@1.6.0: {} scheduler@0.25.0: {} @@ -12895,9 +13341,9 @@ snapshots: stackback@0.0.2: {} - starlight-image-zoom@0.14.2(@astrojs/starlight@0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3)): + starlight-image-zoom@0.14.2(@astrojs/starlight@0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3)): dependencies: - '@astrojs/starlight': 0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3) + '@astrojs/starlight': 0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3) mdast-util-mdx-jsx: 3.2.0 rehype-raw: 7.0.0 unist-util-visit: 5.1.0 @@ -12905,9 +13351,9 @@ snapshots: transitivePeerDependencies: - supports-color - starlight-links-validator@0.24.1(@astrojs/starlight@0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3))(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0)): + starlight-links-validator@0.24.1(@astrojs/starlight@0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3))(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: - '@astrojs/starlight': 0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3) + '@astrojs/starlight': 0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3) '@types/picomatch': 4.0.3 astro: 6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0) github-slugger: 2.0.0 @@ -12922,19 +13368,19 @@ snapshots: transitivePeerDependencies: - supports-color - starlight-package-managers@0.12.0(@astrojs/starlight@0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3)): + starlight-package-managers@0.12.0(@astrojs/starlight@0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3)): dependencies: - '@astrojs/starlight': 0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3) + '@astrojs/starlight': 0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3) - starlight-scroll-to-top@1.0.1(@astrojs/starlight@0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3)): + starlight-scroll-to-top@1.0.1(@astrojs/starlight@0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3)): dependencies: - '@astrojs/starlight': 0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3) + '@astrojs/starlight': 0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3) - starlight-showcases@0.3.2(@astrojs/starlight@0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3)): + starlight-showcases@0.3.2(@astrojs/starlight@0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3)): dependencies: '@astro-community/astro-embed-twitter': 0.5.11 '@astro-community/astro-embed-youtube': 0.5.10 - '@astrojs/starlight': 0.40.0(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3) + '@astrojs/starlight': 0.40.0(@astrojs/markdown-satteri@0.2.1)(astro@6.4.7(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@5.9.3) std-env@4.1.0: {} @@ -13107,6 +13553,8 @@ snapshots: tabbable@6.4.0: {} + tailwind-merge@3.6.0: {} + tailwindcss@4.1.4: {} tailwindcss@4.3.1: {} @@ -13306,6 +13754,13 @@ snapshots: dependencies: pathe: 2.0.3 + unified-lint-rule@3.0.1: + dependencies: + '@types/unist': 3.0.3 + trough: 2.2.0 + unified: 11.0.5 + vfile: 6.0.3 + unified@11.0.5: dependencies: '@types/unist': 3.0.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ddb3e37e077..e66f5d67323 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,23 @@ # 1440 minutes = 24 hours. Reduces risk from newly-published compromised packages. minimumReleaseAge: 1440 +# First-party package published as part of the Nimbus migration; exempt from the +# age guard so freshly-cut versions install without a 24h wait. +minimumReleaseAgeExclude: + - nimbus-docs + +# @astrojs/mdx's @astrojs/markdown-satteri peer is optional and only engages when +# markdown.processor is Sätteri (the Nimbus target, via nimbus-docs's pinned mdx +# 6.0.1 + markdown-satteri 0.2.1). nimbus-docs pulls 0.2.1 into the tree, and pnpm +# satisfies every mdx's optional peer from it — including Starlight's mdx 6.0.3, +# which prefers 0.3.0 but never loads the Sätteri bridge (verified inert). The +# wiring is identical with or without this rule; it only silences the cosmetic +# version-mismatch warning. Pulling markdown-satteri 0.3.x instead would drag in +# an unused satteri 0.9 major, so accept 0.2.1 for the peer everywhere. +peerDependencyRules: + allowedVersions: + "@astrojs/mdx>@astrojs/markdown-satteri": "0.2.1" + # Prevent transitive dependencies from using exotic sources (git repos, direct tarball URLs). # Only direct dependencies may use exotic sources. blockExoticSubdeps: true diff --git a/src/content/changelog/waf/2026-05-04-waf-release.mdx b/src/content/changelog/waf/2026-05-04-waf-release.mdx index 1d27b8dbad9..58a075d12fa 100644 --- a/src/content/changelog/waf/2026-05-04-waf-release.mdx +++ b/src/content/changelog/waf/2026-05-04-waf-release.mdx @@ -3,21 +3,21 @@ title: "WAF Release - 2026-05-04" description: Cloudflare WAF managed rulesets 2026-05-04 release date: 2026-05-04 --- - + import { RuleID } from "~/components"; - + This week's release focuses on new detections to expand coverage across command injection, SQL injection, PHP object injection, remote code execution, and XSS attack vectors. - + **Key Findings** - + - Existing rule enhancements have been deployed to improve detection resilience against broad classes of web attacks and strengthen behavioral coverage. - - + + **Continuous Rule Improvements** - + We are continuously refining our managed rules to provide more resilient protection and deeper insights into attack patterns. To ensure an optimal security posture, we recommend consistently monitoring the Security Events dashboard and adjusting rule actions as these enhancements are deployed. - - + + diff --git a/src/content/docs/workers/wrangler/configuration.mdx b/src/content/docs/workers/wrangler/configuration.mdx index d24c425c566..42d73078287 100644 --- a/src/content/docs/workers/wrangler/configuration.mdx +++ b/src/content/docs/workers/wrangler/configuration.mdx @@ -1881,7 +1881,12 @@ A common example of using a redirected configuration is where a custom build too It also creates a `.wrangler/deploy/config.json` file that redirects Wrangler to the new, generated deployment configuration file: - - dist - index.js - wrangler.jsonc - .wrangler - deploy - config.json + - dist/ + - index.js + - wrangler.jsonc + - .wrangler/ + - deploy/ + - config.json The generated `dist/wrangler.jsonc` might contain: diff --git a/src/nimbus/README.md b/src/nimbus/README.md new file mode 100644 index 00000000000..a50d8aca137 --- /dev/null +++ b/src/nimbus/README.md @@ -0,0 +1,42 @@ +# `src/nimbus` — the Nimbus build target + +This directory is the **Nimbus** documentation app. It is built only when +`BUILD_TARGET=nimbus` (Astro `srcDir: src/nimbus` → `dist-nimbus/`); the default +build is Starlight at `srcDir: src` → `dist/`. The two never share a `src/pages` +or component graph. See `astro.config.ts` and `src/nimbus/astro-config.ts`. + +## What lives here vs. what is shared in place + +**Only app code lives here** — components, layouts, routes, schemas, util, the +`components.ts` MDX barrel, and the rehype/Sätteri pipeline. + +**Content and assets are NOT copied here — they are shared in place** from the +project root and read by both build targets: + +| Shared resource | Root location | How Nimbus reads it | +| --- | --- | --- | +| Content (MDX, data collections) | `src/content` | collection `base` is project-root-relative; `~/content` alias for direct imports | +| Images / assets | `src/assets` | `~/assets` alias → root `src/assets` | +| Local icons | `src/icons` | `astro-icon` resolves `iconDir` against the project root for both targets | + +**Do not add content, assets, or icons under `src/nimbus`.** If a component needs +shared content data, import it via `~/content/…` (not a copy). + +## Why `components/cf/*` looks like `src/components/*` + +The `cf/` components are **ports** of the root (Starlight) `src/components/*`, +not duplicates: same rendered output, different rendering stack (Nimbus +primitives + `nimbus-docs` instead of Starlight internals). Both component +graphs coexist **by design** during priming — that is the cost of a big-bang +cutover, and it collapses to one set at cleanup (Epic H1: delete the Starlight +`src/*` bits, promote `src/nimbus` → `src`). + +## Drift to watch (Epic F3) + +A few app files here are currently byte-identical to their root counterparts +because they needed no stack adaptation (the architecture diagrams, plus +`util/warp-platforms.json`, `util/content-type.ts`, `schemas/compatibility-flags.ts`). +They are separate files in separate graphs, so a root-side edit during priming +will **not** propagate automatically. The re-baseline/drift protocol (F3) tracks +these. Note `util/warp-platforms.json` and the `compatibility-flags` schema/route +are content-derived/generated — regenerate, don't hand-edit. diff --git a/src/nimbus/astro-config.ts b/src/nimbus/astro-config.ts new file mode 100644 index 00000000000..fe773ba72c4 --- /dev/null +++ b/src/nimbus/astro-config.ts @@ -0,0 +1,294 @@ +import { readdir } from "node:fs/promises"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import react from "@astrojs/react"; +import icon from "astro-icon"; +import nimbus, { defineConfig as defineNimbusConfig } from "nimbus-docs"; +import { rehypePlugins } from "./plugins/rehype"; + +// One sidebar section per top-level product directory, autogenerated from the +// shared content tree (mirrors CF's `autogenSections` in the Starlight config). +// `scope: "section"` then surfaces only the current product's tree per page; +// without the full set, off-pilot pages fell back to the whole configured list. +async function autogenSections() { + const dirs = await readdir("./src/content/docs/", { withFileTypes: true }); + return dirs + .filter((entry) => entry.isDirectory() && entry.name !== "agent-setup") + .map((entry) => ({ + label: entry.name, + items: [{ autogenerate: { directory: entry.name, collapsed: true } }], + })); +} + +const sidebarItems = await autogenSections(); + +// Resolved against this file (src/nimbus/). `~` → src/nimbus, `~/assets` → +// the shared root src/assets, partials → the shared root src/content/partials. +const here = (p: string) => + fileURLToPath(new URL(p, import.meta.url)).replace(/\/$/, ""); +const partialsRoot = here("../content/partials"); + +const nimbusConfig = defineNimbusConfig({ + site: "https://developers.cloudflare.com", + title: "Cloudflare Docs", + description: "Cloudflare's documentation.", + locale: "en", + github: "https://github.com/cloudflare/cloudflare-docs", + socialImageAlt: "Cloudflare documentation", + // Search is Algolia DocSearch (wired in E4); Pagefind off. + search: false, + sidebar: { + items: sidebarItems, + overviewLabel: "Overview", + scope: "section", + isolate: { boundaries: ["learning-paths/*"] }, + defaultCollapsed: true, + }, +}); + +// Maps Starlight icon names used in shared content to the iconify sets we ship, +// without touching content on disk: a pre-stage, MDX-scoped string rewrite. +const iconAlias = { + name: "cf-nimbus:icon-alias", + enforce: "pre" as const, + transform(code: string, id: string) { + const [pathOnly] = id.split("?", 1); + if (!pathOnly?.endsWith(".mdx") && !pathOnly?.endsWith(".md")) return null; + if (!/icon\s*=/.test(code)) return null; + + // Starlight ships a native `seti:` file-icon set; Nimbus routes icons + // through astro-icon, which has no `seti` set. Map every `seti:` name the + // shared content uses onto an installed set (vscode-icons / ph / + // simple-icons). The substitute glyphs differ visually from Starlight's + // seti set — a parity item tracked for the parity gate (Epic F), not a + // render blocker. + const SETI: Record = { + javascript: "vscode-icons:file-type-js-official", + typescript: "vscode-icons:file-type-typescript-official", + python: "vscode-icons:file-type-python", + shell: "vscode-icons:file-type-shell", + rust: "vscode-icons:file-type-rust", + video: "vscode-icons:file-type-video", + db: "vscode-icons:file-type-db", + php: "vscode-icons:file-type-php", + html: "vscode-icons:file-type-html", + docker: "vscode-icons:file-type-docker", + svelte: "vscode-icons:file-type-svelte", + powershell: "vscode-icons:file-type-powershell", + notebook: "vscode-icons:file-type-jupyter", + nix: "vscode-icons:file-type-nix", + json: "vscode-icons:file-type-json", + java: "vscode-icons:file-type-java", + go: "vscode-icons:file-type-go", + "c-sharp": "vscode-icons:file-type-csharp", + windows: "simple-icons:windows", + linux: "simple-icons:linux", + redhat: "simple-icons:redhat", + debian: "simple-icons:debian", + lock: "ph:lock", + info: "ph:info", + eye: "ph:eye", + plan: "ph:file-text", + }; + // Bare icon names (no set prefix) resolve against Starlight's built-in + // icon set in the default build; under Nimbus they go through astro-icon + // (local `src/icons` + installed iconify sets). Names not present locally + // are mapped here onto an installed set. Glyphs differ visually from + // Starlight's — a parity item for the parity gate (Epic F). + const BARE: Record = { + document: "ph:file-text", + "open-book": "ph:book-open", + pen: "ph:pencil-simple", + discord: "simple-icons:discord", + "x.com": "simple-icons:x", + apple: "ph:apple-logo", + astro: "simple-icons:astro", + bluesky: "simple-icons:bluesky", + clock: "ph:clock", + "comment-alt": "ph:chat-circle", + connection: "ph:plugs-connected", + database: "ph:database", + download: "ph:download-simple", + email: "ph:envelope-simple", + error: "ph:warning-circle", + external: "ph:arrow-square-out", + github: "ph:github-logo", + heart: "ph:heart", + information: "ph:info", + key: "ph:key", + laptop: "ph:laptop", + linux: "ph:linux-logo", + "list-format": "ph:list-bullets", + magnifier: "ph:magnifying-glass", + mastodon: "ph:mastodon-logo", + moon: "ph:moon", + puzzle: "ph:puzzle-piece", + rocket: "ph:rocket-launch", + setting: "ph:gear", + star: "ph:star", + terminal: "ph:terminal", + }; + + let out = code; + out = out.replace(/(["'])seti:([a-z0-9_-]+)\1/gi, (match, q, name) => { + const mapped = SETI[name.toLowerCase()]; + return mapped ? `${q}${mapped}${q}` : match; + }); + out = out.replace( + /\bicon\s*=\s*(["'])([a-z0-9_.-]+)\1/gi, + (match, q, name) => { + if (name.includes(":")) return match; + const mapped = BARE[name.toLowerCase()]; + return mapped ? `icon=${q}${mapped}${q}` : match; + }, + ); + return out === code ? null : { code: out, map: null }; + }, +}; + +// The Nimbus target's markdown / integrations / vite, branched into +// astro.config.ts when BUILD_TARGET=nimbus. +export const markdown = { + syntaxHighlight: { + type: "shiki" as const, + excludeLangs: ["math", "mermaid"], + }, + smartypants: false, +}; + +export const integrations = [ + icon(), + react(), + nimbus(nimbusConfig, { + mdx: { optimize: true }, + markdown: { hastPlugins: rehypePlugins }, + incrementalBuilds: false, + validateMdx: false, + partialResolver: (name: string, props: Record) => { + if (name !== "Render" || !props.file) return null; + const path = props.product + ? `${props.product}/${props.file}.mdx` + : `${props.file}.mdx`; + return resolve(partialsRoot, path); + }, + rules: { + "nimbus/frontmatter-shape": "error", + "nimbus/image-ref": [ + "error", + { aliases: { "~/assets/": "src/assets/" } }, + ], + "nimbus/internal-link": "off", + }, + }), +]; + +// Make the Nimbus app aliases authoritatively beat Astro's tsconfig-derived +// `~` (the root `~/*`→`src/*` the Starlight target needs). +// +// Astro injects the root tsconfig paths into `vite.resolve.alias` via the +// `astro:tsconfig-alias` plugin's `config()` hook, where the per-entry +// customResolver returns the ROOT file whenever `src/` exists on disk. +// Vite's built-in `vite:alias` plugin (resolve.alias) runs BEFORE user +// `enforce:"pre"` plugins, so a `resolveId` override can never win where the +// root file exists (e.g. `~/components/changelog/Header.astro`). +// +// Fix: contribute our OWN `resolve.alias` entries from a `config` hook with +// `order: "post"`. Vite's `mergeAlias` PREPENDS each later config's aliases +// (`[...newer, ...older]`), and config hooks ordered "post" run after +// `astro:tsconfig-alias`'s (unordered) hook — so our entries land FIRST in +// the final alias array and the built-in alias plugin matches them first. +// `~/assets` and `~/content` precede `~` so the shared root tree still wins. +// +// This only applies to the Nimbus target (this config is loaded solely when +// BUILD_TARGET=nimbus); the default Starlight build is untouched and keeps +// `~`→src via its own tsconfig alias. +const nimbusDir = here("."); +const rootAssets = here("../assets"); +const rootContent = here("../content"); +const componentsBarrelId = normalizeId(here("./components.ts")); + +function normalizeId(id: string) { + return id.replace(/\\/g, "/").replace(/\?.*$/, ""); +} + +const componentsBarrelSideEffects = { + name: "cf-nimbus:components-barrel-side-effects", + enforce: "pre" as const, + transform: { + filter: { id: /[\\/]components\.ts(?:\?.*)?$/ }, + handler(code: string, id: string) { + if (normalizeId(id) !== componentsBarrelId) return; + return { code, moduleSideEffects: false }; + }, + }, +}; + +const aliasResolver = { + name: "cf-nimbus:alias", + enforce: "pre" as const, + config: { + order: "post" as const, + handler() { + return { + resolve: { + alias: [ + // Shared content imports component barrels by name — both + // Starlight's (`@astrojs/starlight/components`) and Expressive + // Code's (`astro-expressive-code/components`, e.g. the workers + // terraform changelog). Map both to the Nimbus barrel (which + // re-exports those names, incl. `Code`) so byte-identical content + // resolves without pulling in Starlight + Expressive Code (whose + // `renderer.ts` needs `virtual:astro-expressive-code/config`, a + // module only the EC integration — which Nimbus omits — provides). + { + find: /^@astrojs\/starlight\/components$/, + replacement: `${nimbusDir}/components`, + }, + { + find: /^astro-expressive-code\/components$/, + replacement: `${nimbusDir}/components`, + }, + { find: /^~\/assets(\/.*)?$/, replacement: `${rootAssets}$1` }, + { find: /^~\/content(\/.*)?$/, replacement: `${rootContent}$1` }, + { find: /^~(\/.*)?$/, replacement: `${nimbusDir}$1` }, + { find: /^@(\/.*)?$/, replacement: `${nimbusDir}$1` }, + ], + }, + }; + }, + }, + // Defense-in-depth fallback for any context the alias array doesn't cover. + async resolveId( + this: { resolve: (s: string, i?: string, o?: object) => Promise<{ id: string } | null> }, + source: string, + importer: string | undefined, + options: object, + ) { + let mapped: string | null = null; + if ( + source === "@astrojs/starlight/components" || + source === "astro-expressive-code/components" + ) + mapped = nimbusDir + "/components"; + else if (source === "~/assets" || source.startsWith("~/assets/")) + mapped = rootAssets + source.slice("~/assets".length); + else if (source === "~/content" || source.startsWith("~/content/")) + mapped = rootContent + source.slice("~/content".length); + else if (source === "~" || source.startsWith("~/")) + mapped = nimbusDir + source.slice(1); + else if (source === "@" || source.startsWith("@/")) + mapped = nimbusDir + source.slice(1); + if (mapped === null) return null; + const resolved = await this.resolve(mapped, importer, { ...options, skipSelf: true }); + return resolved ?? { id: mapped }; + }, +}; + +export const vite = { + // Force a single React instance. `nimbus-docs` is consumed via a `link:` + // symlink to the monorepo, which has its own React; without dedupe, Vite + // can load two React copies across the symlink in dev and client islands + // fail to hydrate with "jsxDEV is not a function". + resolve: { dedupe: ["react", "react-dom"] }, + plugins: [aliasResolver, componentsBarrelSideEffects, iconAlias], +}; diff --git a/src/nimbus/components.ts b/src/nimbus/components.ts new file mode 100644 index 00000000000..eb43fefc811 --- /dev/null +++ b/src/nimbus/components.ts @@ -0,0 +1,78 @@ +export { Aside } from "./components/ui/aside"; +export { Card } from "./components/ui/card"; +export { CardGrid } from "./components/ui/card-grid"; +export { PackageManagers } from "./components/ui/package-managers"; +export { Step, Steps } from "./components/ui/steps"; +export { TabItem, Tabs } from "./components/ui/tabs"; +export { Badge } from "./components/ui/badge"; +export { Code } from "./components/ui/code"; +export { FileTree } from "./components/ui/file-tree"; +export { default as LinkButton } from "./components/ui/link-button/LinkButton.astro"; +export { Icon, Icon as AstroIcon } from "astro-icon/components"; + +export { default as Render } from "./components/Render.astro"; +export { default as APIRequest } from "./components/cf/APIRequest.astro"; +export { default as DashButton } from "./components/cf/DashButton.astro"; +export { default as DirectoryListing } from "./components/cf/DirectoryListing.astro"; +export { default as Description } from "./components/cf/Description.astro"; +export { default as Details } from "./components/cf/Details.astro"; +export { default as MetaInfo } from "./components/cf/MetaInfo.astro"; +export { default as Type } from "./components/cf/Type.astro"; +export { default as WranglerConfig } from "./components/cf/WranglerConfig.astro"; +export { default as WranglerNamespace } from "./components/cf/WranglerNamespace.astro"; +export { default as Feature } from "./components/cf/Feature.astro"; +export { default as Glossary } from "./components/cf/Glossary.astro"; +export { default as GlossaryTooltip } from "./components/cf/GlossaryTooltip.astro"; +export { default as LinkCard } from "./components/cf/LinkCard.astro"; +export { default as LinkTitleCard } from "./components/cf/LinkTitleCard.astro"; +export { default as ListTutorials } from "./components/cf/ListTutorials.astro"; +export { default as Plan } from "./components/cf/Plan.astro"; +export { default as ProductReleaseNotes } from "./components/cf/ProductReleaseNotes.astro"; +export { default as ProductChangelog } from "./components/cf/ProductChangelog.astro"; +export { default as RelatedProduct } from "./components/cf/RelatedProduct.astro"; +export { default as TypeScriptExample } from "./components/cf/TypeScriptExample.astro"; +export { default as TunnelCalculator } from "./components/cf/TunnelCalculator.astro"; +export { default as InlineBadge } from "./components/cf/InlineBadge.astro"; +export { default as YouTube } from "./components/cf/YouTube.astro"; +export { default as Example } from "./components/cf/Example.astro"; +export { default as Markdown } from "./components/cf/Markdown.astro"; +export { default as CURL } from "./components/cf/CURL.astro"; +export { default as GitHubCode } from "./components/cf/GitHubCode.astro"; +export { default as Flex } from "./components/cf/Flex.astro"; +export { default as Width } from "./components/cf/Width.astro"; +export { default as RuleID } from "./components/cf/RuleID.astro"; +export { default as PublicStats } from "./components/cf/PublicStats.astro"; +export { default as RSSButton } from "./components/cf/RSSButton.astro"; +export { default as GlossaryDefinition } from "./components/cf/GlossaryDefinition.astro"; +export { default as WranglerCommand } from "./components/cf/WranglerCommand.astro"; +export { default as AnchorHeading } from "./components/cf/AnchorHeading.astro"; +export { default as FeatureTable } from "./components/cf/FeatureTable.astro"; +export { default as ProductFeatures } from "./components/cf/ProductFeatures.astro"; +export { default as PagesBuildPreset } from "./components/cf/PagesBuildPreset.astro"; +export { default as PagesBuildPresetsTable } from "./components/cf/PagesBuildPresetsTable.astro"; +export { default as ComponentsUsage } from "./components/cf/ComponentsUsage.astro"; +export { default as GranularControlApplicationsList } from "./components/cf/GranularControlApplicationsList.astro"; +export { default as ProductAvailabilityText } from "./components/cf/ProductAvailabilityText.astro"; +export { default as WorkersTemplates } from "./components/cf/WorkersTemplates.astro"; +export { default as AvailableNotifications } from "./components/cf/AvailableNotifications.astro"; +export { default as ExtraFlagDetails } from "./components/cf/ExtraFlagDetails.astro"; +export { default as FourCardGrid } from "./components/cf/FourCardGrid.astro"; +export { default as ListCard } from "./components/cf/ListCard.astro"; +export { default as Stream } from "./components/cf/Stream.astro"; +export { default as PagesBuildEnvironment } from "./components/cf/PagesBuildEnvironment.astro"; +export { default as PagesBuildEnvironmentLanguages } from "./components/cf/PagesBuildEnvironmentLanguages.astro"; +export { default as PagesBuildEnvironmentTools } from "./components/cf/PagesBuildEnvironmentTools.astro"; +export { default as WARPReleases } from "./components/cf/WARPReleases.astro"; +export { default as WARPRelease } from "./components/cf/WARPRelease.astro"; +export { default as CompatibilityFlags } from "./components/cf/CompatibilityFlags.astro"; +export { default as AutoconfigDiagram } from "./components/cf/AutoconfigDiagram.astro"; +export { default as WorkersArchitectureDiagram } from "./components/cf/WorkersArchitectureDiagram.astro"; +export { default as WorkersIsolateDiagram } from "./components/cf/WorkersIsolateDiagram.astro"; +export { default as AnimatedWorkflowDiagram } from "./components/cf/AnimatedWorkflowDiagram.astro"; +export { default as AgentsPlatformDiagram } from "./components/cf/AgentsPlatformDiagram.astro"; +export { default as R2LocalUploadsDiagram } from "./components/cf/R2LocalUploadsDiagram.astro"; +export { default as WorkersVPCEgressDiagram } from "./components/cf/WorkersVPCEgressDiagram.astro"; +export { default as WorkersVPCOverviewDiagram } from "./components/cf/WorkersVPCOverviewDiagram.astro"; +export { default as ResourcesBySelector } from "./components/cf/ResourcesBySelector.astro"; +export { default as SubtractIPCalculator } from "./components/react/SubtractIPCalculator"; +export { AgentPrimitivesDiagram } from "./components/react/diagram-showcase/AgentPrimitivesDiagram"; diff --git a/src/nimbus/components/AgentDirective.astro b/src/nimbus/components/AgentDirective.astro new file mode 100644 index 00000000000..c2617dfc23e --- /dev/null +++ b/src/nimbus/components/AgentDirective.astro @@ -0,0 +1,16 @@ +--- +interface Props { + /** Absolute or site-relative URL for this page's markdown version. */ + markdownUrl: string; + /** Absolute or site-relative URL for the top-level llms.txt index. */ + llmsUrl: string; +} + +const { markdownUrl, llmsUrl } = Astro.props; +--- + + diff --git a/src/nimbus/components/AvailableChangelogFeeds.astro b/src/nimbus/components/AvailableChangelogFeeds.astro new file mode 100644 index 00000000000..e577b3bd927 --- /dev/null +++ b/src/nimbus/components/AvailableChangelogFeeds.astro @@ -0,0 +1,104 @@ +--- +import { Aside } from "~/components"; + +import AnchorHeading from "./cf/AnchorHeading.astro"; +import RSSButton from "./cf/RSSButton.astro"; +import Details from "./cf/Details.astro"; + +import { getCollection } from "astro:content"; +import { getChangelogs } from "~/util/changelog"; + +const changelogs = await getChangelogs({}); +const productsInChangelog = changelogs.flatMap((entry) => + entry.data.products.map((product) => product.id), +); + +const directory = await getCollection("directory", (entry) => { + return productsInChangelog.includes(entry.id); +}); + +const directoryByGroup = Object.entries( + Object.groupBy( + directory.filter((entry) => Boolean(entry.data.entry.group)), + (entry) => entry.data.entry.group, + ), +).sort(); +--- + + + + +

+ This feed contains entries for all Cloudflare products in the changelog: +

+ + + +

+ Cloudflare also offers RSS feeds scoped to specific product areas or products + in the changelog. +

+ +{ + directoryByGroup.map(([group, entries]) => ( + <> + +

+ This feed is for all {group} products in the changelog:{" "} + +

+
+ +
+ {group === "Application security" && ( + + )} + {group === "Core platform" && ( + + )} + + )) +} diff --git a/src/nimbus/components/AvailableDashRoutes.astro b/src/nimbus/components/AvailableDashRoutes.astro new file mode 100644 index 00000000000..c032b5cb5f0 --- /dev/null +++ b/src/nimbus/components/AvailableDashRoutes.astro @@ -0,0 +1,34 @@ +--- +import { Code } from "~/components"; +import AnchorHeading from "./cf/AnchorHeading.astro"; +import CoreRoutes from "~/content/dash-routes/core.json"; +import ZeroTrustRoutes from "~/content/dash-routes/zero-trust.json"; +--- + +
+ +
    + { + CoreRoutes.map((route) => ( +
  • + {(route.parent ?? []).concat(route.name).join(" > ")} +
    + `} /> +
  • + )) + } +
+ + +
    + { + ZeroTrustRoutes.map((route) => ( +
  • + {route.name} +
    + `} /> +
  • + )) + } +
+
diff --git a/src/nimbus/components/BackgroundLines.astro b/src/nimbus/components/BackgroundLines.astro new file mode 100644 index 00000000000..e9819b46d78 --- /dev/null +++ b/src/nimbus/components/BackgroundLines.astro @@ -0,0 +1,60 @@ +--- +/** + * BackgroundLines — decorative blueprint backdrop modelled on cloudflare.com + * and the "big move" blog. A centered content-width inner frame is flanked by + * dashed vertical rules; a wider outer frame carries a dot-grid and its own + * dashed rules. The inner frame paints a solid background so the dots show + * only in the side gutters between the two frames. Desktop-only, purely + * decorative, animation-free. + * + * Tokens: dashed rules use --nb-border; dots use --nb-grid-line. + */ +interface Props { + /** Inner (content) frame width in px — match the content max-width. */ + inner?: number; + /** Outer frame width in px — defines the dot-grid gutters. */ + outer?: number; +} + +const { inner = 1120, outer = 1360 } = Astro.props; + +// 1px vertical dashed rule: 16px dash + 16px gap (32px period). +const dashed = + "background-image:linear-gradient(to bottom,var(--nb-border) 50%,transparent 50%);background-size:1px 32px;background-repeat:repeat-y"; +--- + + diff --git a/src/nimbus/components/BaseSchemaProperties.astro b/src/nimbus/components/BaseSchemaProperties.astro new file mode 100644 index 00000000000..4d61f80144d --- /dev/null +++ b/src/nimbus/components/BaseSchemaProperties.astro @@ -0,0 +1,168 @@ +--- +import { z } from "astro/zod"; +import { baseSchema } from "~/schemas"; +import { marked } from "marked"; + +import Type from "./cf/Type.astro"; +import MetaInfo from "./cf/MetaInfo.astro"; +import AnchorHeading from "./cf/AnchorHeading.astro"; +import Details from "./cf/Details.astro"; + +const fullSchema = baseSchema({ + // @ts-expect-error Normally passed in by Astro, but we are using the schema standalone. + image: () => z.function(), +}); + +// Override properties that JSON schema isn't going to understand because they're Astro-specific +const shape = { + ...fullSchema.shape, + products: z + .array(z.string()) + .default([]) + .describe( + "The names of related directory entries (according to their file name in `src/content/directory`). Usually, these correspond to file paths, but not always, such as with `cloudflare-tunnel`", + ), +}; + +const schema = z.object(shape); + +// Convert to JSON Schema +const jsonSchema = z.toJSONSchema(schema, { + // Handle the unrepresentable image() function + unrepresentable: "any", +}); + +// Type guard for objects +function isObject(val: unknown): val is Record { + return typeof val === "object" && val !== null && !Array.isArray(val); +} + +// Extract the type from a JSON Schema property +function getPropertyType(property: unknown): string { + if (!isObject(property)) return "unknown"; + + // Handle oneOf (used for nullable types) + if (property.oneOf && Array.isArray(property.oneOf)) { + const nonNullTypes = property.oneOf.filter( + (schema: unknown) => + isObject(schema) && schema.type && schema.type !== "null", + ); + if (nonNullTypes.length > 0 && isObject(nonNullTypes[0])) { + return String(nonNullTypes[0].type); + } + } + + return String(property.type); +} + +// Check if a property is optional +function isOptional(key: string, required: unknown): boolean { + if (!Array.isArray(required)) return true; + return !required.includes(key); +} + +// Extract enum values from a property +function getEnumValues(property: unknown): string[] | null { + if (!isObject(property)) return null; + + // Handle oneOf union types (Zod 4 represents z.union([z.literal(...)]) as oneOf with const) + if (property.oneOf && Array.isArray(property.oneOf)) { + const literals: string[] = []; + for (const option of property.oneOf) { + if (isObject(option)) { + // Check for const (literal values) + if (option.const !== undefined) { + literals.push(String(option.const)); + } + // Also check for enum values within the oneOf + else if (option.enum && Array.isArray(option.enum)) { + literals.push(...option.enum.map(String)); + } + } + } + return literals.length > 0 ? literals : null; + } + + // Handle anyOf union types (alternative union representation) + if (property.anyOf && Array.isArray(property.anyOf)) { + const literals: string[] = []; + for (const option of property.anyOf) { + if (isObject(option)) { + if (option.const !== undefined) { + literals.push(String(option.const)); + } else if (option.enum && Array.isArray(option.enum)) { + literals.push(...option.enum.map(String)); + } + } + } + return literals.length > 0 ? literals : null; + } + + // Handle direct enum (z.enum()) + if (property.enum && Array.isArray(property.enum)) { + return property.enum.map(String); + } + + return null; +} + +// Extract description from a property +function getDescription(property: unknown): string | null { + if (!isObject(property)) return null; + return property.description ? String(property.description) : null; +} + +// Get properties from the JSON Schema +const properties = + isObject(jsonSchema) && isObject(jsonSchema.properties) + ? jsonSchema.properties + : {}; + +const required = isObject(jsonSchema) ? jsonSchema.required : []; +--- + +{ + Object.entries(properties) + .sort() + .map(([key, property]) => { + const propertyType = getPropertyType(property); + const optional = isOptional(key, required); + const description = getDescription(property); + const enumValues = getEnumValues(property); + + return ( + <> + +

+ Type: + + {optional && ( + <> + {" "} + + + )} +

+ {description && ( +

+ Description: + +

+ )} + {enumValues && enumValues.length > 0 && ( +

+

+
    + {enumValues.map((value) => ( +
  • + {value} +
  • + ))} +
+
+

+ )} + + ); + }) +} diff --git a/src/nimbus/components/CopyPromptButton.astro b/src/nimbus/components/CopyPromptButton.astro new file mode 100644 index 00000000000..5dad9599b75 --- /dev/null +++ b/src/nimbus/components/CopyPromptButton.astro @@ -0,0 +1,91 @@ +--- +import { AGENTS, type AgentId } from "./agents"; +import { Button } from "@/components/ui/button"; + +interface Props { + /** Agent ids shown in the icon row. */ + agentIds?: readonly AgentId[]; + class?: string; + /** Optional native title tooltip. */ + tooltip?: string; +} + +const { + agentIds = ["claude", "cursor", "codex", "opencode"] as const, + class: className = "", + tooltip, +} = Astro.props; + +const featuredAgents = AGENTS.filter((a) => + (agentIds as readonly string[]).includes(a.id), +); +--- + + + + diff --git a/src/nimbus/components/CornerMarks.astro b/src/nimbus/components/CornerMarks.astro new file mode 100644 index 00000000000..5bfad982e5f --- /dev/null +++ b/src/nimbus/components/CornerMarks.astro @@ -0,0 +1,45 @@ +--- +type Corner = "tl" | "tr" | "bl" | "br"; +type Tone = "neutral" | "accent"; + +interface Props { + corners?: Corner[]; + size?: number; + offset?: number; + tone?: Tone; + class?: string; +} + +const { + corners = ["tl", "tr", "bl", "br"] as Corner[], + size = 14, + offset = -7, + tone = "neutral", + class: className = "", +} = Astro.props; + +const positions: Record = { + tl: `left:${offset}px;top:${offset}px;`, + tr: `right:${offset}px;top:${offset}px;`, + bl: `left:${offset}px;bottom:${offset}px;`, + br: `right:${offset}px;bottom:${offset}px;`, +}; + +const borderColor = + tone === "accent" ? "rgba(255, 251, 245, 0.32)" : "var(--color-border)"; + +const baseStyle = `width:${size}px;height:${size}px;border:1px solid ${borderColor};border-radius:3px;`; + +const fillClasses = + tone === "accent" ? "bg-primary" : "bg-background dark:bg-background"; +--- + +{ + corners.map((c) => ( +
+ + + { + COLUMNS.map((col, i) => ( + + )) + } + + + + { + rows.map(({ agent, sortKeys }) => ( + + + + + + + )) + } + +
+ +
+ + {agent.name} + + + + + + + {agent.pricing_model ? ( + + {capabilityLabels[ + agent.pricing_model as keyof typeof capabilityLabels + ] ?? agent.pricing_model} + + ) : ( + + )} + + {agent.model_flexibility ? ( + + {capabilityLabels[ + agent.model_flexibility as keyof typeof capabilityLabels + ] ?? agent.model_flexibility} + + ) : ( + + )} + + {agent.context_approach ? ( + + {capabilityLabels[ + agent.context_approach as keyof typeof capabilityLabels + ] ?? agent.context_approach} + + ) : ( + + )} + +
+ +

+ Every agent listed supports Skills and MCP. +

+ + + diff --git a/src/nimbus/components/agent-setup/AgentHeader.astro b/src/nimbus/components/agent-setup/AgentHeader.astro new file mode 100644 index 00000000000..71bf483f8e9 --- /dev/null +++ b/src/nimbus/components/agent-setup/AgentHeader.astro @@ -0,0 +1,105 @@ +--- +// Top-of-page header for a single agent detail MDX page. +// Renders the back button + icon + title + vendor block, the description or +// a custom intro slot, the capability badge, and external links. +// +// Usage from an MDX page: +// +// custom intro markup here +import { AGENTS } from "./agents"; +import AgentIcon from "./AgentIcon.astro"; +import CapabilityBadge from "./CapabilityBadge.astro"; +import "~/styles/agent-setup.css"; + +interface Props { + slug: string; +} + +const { slug } = Astro.props; + +const agent = AGENTS.find((a) => a.slug === slug); + +if (!agent) { + throw new Error(`[AgentHeader] No agent found for slug="${slug}"`); +} + +const externalLinks = [ + agent.links.skills && { + label: "Cloudflare Skills", + href: agent.links.skills, + }, + agent.links.mcp_server && { + label: "Cloudflare Code Mode API MCP", + href: agent.links.mcp_server, + }, + agent.links.mcp_server_domain && { + label: "Cloudflare Domain Specific MCPs", + href: agent.links.mcp_server_domain, + }, + agent.links.cli && { label: "CLI", href: agent.links.cli }, + agent.links.docs && { label: `${agent.name} Docs`, href: agent.links.docs }, +].filter(Boolean) as { label: string; href: string }[]; +--- + + + + All agents + + +
+ +
+

+ {agent.name} + Cloudflare +

+ + {agent.vendor} + +
+
+ +{ + Astro.slots.has("default") ? ( +
+ +
+ ) : ( +

+ {agent.description} +

+ ) +} + +
+ +
+ +{ + externalLinks.length > 0 && ( + + ) +} diff --git a/src/nimbus/components/agent-setup/AgentIcon.astro b/src/nimbus/components/agent-setup/AgentIcon.astro new file mode 100644 index 00000000000..8556d0a6877 --- /dev/null +++ b/src/nimbus/components/agent-setup/AgentIcon.astro @@ -0,0 +1,31 @@ +--- +interface Props { + icon: string; + name: string; + size?: number; +} + +const { icon, name, size = 36 } = Astro.props; +const lightSrc = `/icons/agents/${icon}/light.svg`; +const darkSrc = `/icons/agents/${icon}/dark.svg`; +--- + + + {`${name} + {`${name} + diff --git a/src/nimbus/components/agent-setup/AgentPrimer.astro b/src/nimbus/components/agent-setup/AgentPrimer.astro new file mode 100644 index 00000000000..ebcdc78adfe --- /dev/null +++ b/src/nimbus/components/agent-setup/AgentPrimer.astro @@ -0,0 +1,282 @@ +--- +// Agent-neutral primer: explains what AI coding agents are, the common types, +// key concepts like Skills and MCP, and the tradeoffs worth knowing about. +// Organized visually rather than as a wall of text. +--- + +
+ +
+
+

Workflow

+

Where the agent runs changes how you interact with it.

+
+
+
+ +
Terminal
+

+ Runs in a shell. Best for automation, scripting, and CI pipelines. +

+
+ +
+ +
IDE
+

+ Full code editor with AI first-class. Visual diffs, multi-file edits. +

+
+ +
+ +
Cloud
+

Hosted infrastructure. Ideal for async, long-running work.

+
+ +
+ +
Extension
+

+ Plugs into an existing editor. Lightest install, keeps your setup. +

+
+
+
+ + +
+
+

Key concepts

+

The vocabulary you'll run into when comparing agents.

+
+
+
+
+ + Skills +
+

+ Reusable prompt packages that teach an agent about a specific domain. + Think of them as plugins made of instructions plus slash commands. +

+
+ +
+
+ + MCP +
+

+ The Model Context Protocol — a standard that lets agents call external + tools and APIs. Connect an MCP server and the agent knows how to use + it. +

+
+ +
+
+ + Model flexibility +
+

+ Which foundation models you can use. Locked + supports only the vendor's own models. BYOK (Bring Your + Own Key) lets you bring your own API key. Multi-provider + supports several providers out of the box. +

+
+ +
+
+ + Context +
+

+ How the agent retains information about your project. + Session only remembers the current conversation. + Project memory persists across sessions. + Indexed codebase builds a searchable index of your whole + repository. +

+
+
+
+ + +
+
+

Common tradeoffs

+

Decisions you'll make when picking an agent.

+
+
+
+
+ Cloud + vs. + Local +
+

+ Cloud agents run on hosted infrastructure and read your code over the + network. Local agents run on your own machine, with no code leaving + it. +

+
+ +
+
+ Proprietary + vs. + Open source +
+

+ Proprietary agents ship under a closed license you don't control. + Open-source agents publish their source under an open license, so you + can read, modify, or fork the code. +

+
+ +
+
+ Locked model + vs. + BYOK +
+

+ Locked agents only work with the vendor's own proprietary models. BYOK + agents let you bring your own API key and switch between providers and + models. +

+
+ +
+
+ Session + vs. + Indexed codebase +
+

+ Session context resets when you close the conversation. An indexed + codebase is built up front and persists, letting the agent retrieve + any file in the repo on demand. +

+
+
+
+
diff --git a/src/nimbus/components/agent-setup/BuildAgentsCallout.astro b/src/nimbus/components/agent-setup/BuildAgentsCallout.astro new file mode 100644 index 00000000000..6b69a6fa5d1 --- /dev/null +++ b/src/nimbus/components/agent-setup/BuildAgentsCallout.astro @@ -0,0 +1,56 @@ +--- +// Footer callout: points users toward building agents ON Cloudflare +// (Agents SDK, Workers AI, Code Mode SDK, Worker Loader), distinguishing +// Cloudflare from a pure deploy target. +--- + +
+
+ Also worth knowing +

+ Cloudflare is not just a deploy target for agents, it is a full stack for + building your own. +

+
+ + +
diff --git a/src/nimbus/components/agent-setup/CapabilityBadge.astro b/src/nimbus/components/agent-setup/CapabilityBadge.astro new file mode 100644 index 00000000000..09e08353fea --- /dev/null +++ b/src/nimbus/components/agent-setup/CapabilityBadge.astro @@ -0,0 +1,47 @@ +--- +import { + capabilityDefinitions, + capabilityLabels, + type CapabilityKey, +} from "./definitions"; + +interface Props { + capabilities: Record; +} + +const { capabilities } = Astro.props; +const active = Object.entries(capabilities).filter(([, v]) => v); +--- + +{ + active.length > 0 && ( +
+ {active.map(([key]) => { + const k = key as CapabilityKey; + const label = capabilityLabels[k] ?? key; + const tooltip = capabilityDefinitions[k] ?? ""; + return ( + + {label} + + ); + })} +
+ ) +} + + diff --git a/src/nimbus/components/agent-setup/CatalogWithFilter.astro b/src/nimbus/components/agent-setup/CatalogWithFilter.astro new file mode 100644 index 00000000000..3a4c5d1e97d --- /dev/null +++ b/src/nimbus/components/agent-setup/CatalogWithFilter.astro @@ -0,0 +1,230 @@ +--- +import type { AgentData } from "./types"; +import { + capabilityDefinitions, + capabilityLabels, + type CapabilityKey, +} from "./definitions"; + +interface Props { + agents: AgentData[]; +} + +const { agents } = Astro.props; + +const FILTERS = [ + { key: "all", label: "All" }, + { key: "terminal", label: "Terminal" }, + { key: "ide", label: "IDE" }, + { key: "cloud", label: "Cloud" }, + { key: "extension", label: "Extension" }, +] as const; + +// Capabilities shown as chips on the card — Skills/MCP are excluded since +// every listed agent supports them (mentioned in the table footnote instead). +const CARD_CAPABILITY_KEYS: (keyof AgentData["capabilities"])[] = [ + "terminal", + "ide", + "standalone", + "cloud", + "extension", + "open_source", +]; +--- + +
+ {/* Filter strip */} +
+ Filter by workflow: +
+ { + FILTERS.map((f, i) => ( + + )) + } +
+
+ + {/* Grid */} +
+ { + agents.map((agent) => { + const activeCaps = CARD_CAPABILITY_KEYS.filter( + (k) => agent.capabilities[k], + ); + const matchKeys = [ + ...Object.entries(agent.capabilities) + .filter(([, v]) => v) + .map(([k]) => k), + ] + .filter(Boolean) + .join(" "); + + return ( + +
+ + {`${agent.name} + {`${agent.name} + +
+
{agent.name}
+
{agent.vendor}
+
+
+ +
+

{agent.description}

+ +
+ {activeCaps.map((key) => { + const k = key as CapabilityKey; + const label = capabilityLabels[k] ?? key; + const tooltip = capabilityDefinitions[k] ?? ""; + return ( + + {label} + + ); + })} +
+ +
    + {agent.features.slice(0, 4).map((feature) => ( +
  • {feature}
  • + ))} +
+ + + View guide + +
+
+ ); + }) + } +
+ + {/* Empty state (hidden unless no cards match) */} + +
+ + diff --git a/src/nimbus/components/agent-setup/CloudflareToolsBanner.astro b/src/nimbus/components/agent-setup/CloudflareToolsBanner.astro new file mode 100644 index 00000000000..0545d11317e --- /dev/null +++ b/src/nimbus/components/agent-setup/CloudflareToolsBanner.astro @@ -0,0 +1,52 @@ +--- +// Shown between the page title and the agent catalog. +// Communicates what Cloudflare provides before the user picks an agent. +--- + +
+
+
+

Skills

+

+ Reusable prompt packages that teach agents about Cloudflare products and + APIs. +

+ +
+ +
+

MCP

+

+ Connect agents to your Cloudflare account. Two flavors: Code Mode ↗ for broad API coverage across all products, or domain-specific servers ↗ for purpose-built tools within one product area. +

+
+ +
+

CLI

+

+ Wrangler for deploying and managing Workers, + KV, D1, R2, and more. + cf CLI ↗ is a next-generation CLI covering every Cloudflare product — in technical + preview. +

+
+
+
diff --git a/src/nimbus/components/agent-setup/ExamplePrompts.astro b/src/nimbus/components/agent-setup/ExamplePrompts.astro new file mode 100644 index 00000000000..d76f554b666 --- /dev/null +++ b/src/nimbus/components/agent-setup/ExamplePrompts.astro @@ -0,0 +1,107 @@ +--- +// Click-to-copy example prompt chips. Each prompt is a button; clicking +// anywhere on the chip copies the text (without surrounding quotes) to the +// clipboard and swaps the copy icon for a checkmark for 1.5s. +// +// All prompts are rendered in the HTML but only 5 randomly-selected ones are +// shown on each page load (client-side shuffle). +// +// Mirrors the inline-script copy pattern used in PackageManagers.astro. +interface Props { + prompts: string[]; + count?: number; +} + +const { prompts, count = 5 } = Astro.props; +--- + +
+ { + prompts.map((prompt) => ( + + )) + } +
+ + diff --git a/src/nimbus/components/agent-setup/ExamplePromptsList.astro b/src/nimbus/components/agent-setup/ExamplePromptsList.astro new file mode 100644 index 00000000000..a847d917b69 --- /dev/null +++ b/src/nimbus/components/agent-setup/ExamplePromptsList.astro @@ -0,0 +1,15 @@ +--- +// Shared set of example prompts reused across every agent detail page. +// The prompts are intentionally agent-agnostic — they describe what a user +// might ask of any Cloudflare-aware coding agent, regardless of which one. +// +// Use from MDX: +// +// +// No `slug` prop is required. When we find a genuinely agent-specific prompt +// (something only one agent can do), we can add an override prop later. +import ExamplePrompts from "./ExamplePrompts.astro"; +import { SHARED_PROMPTS } from "./prompts"; +--- + + diff --git a/src/nimbus/components/agent-setup/FAQItem.astro b/src/nimbus/components/agent-setup/FAQItem.astro new file mode 100644 index 00000000000..d275d3f7d73 --- /dev/null +++ b/src/nimbus/components/agent-setup/FAQItem.astro @@ -0,0 +1,22 @@ +--- +// Single FAQ accordion item for use from MDX. The answer is authored as +// MDX children, so it can use ``, links, and inline markdown naturally: +// +// +// The first time Claude calls a Cloudflare tool, you'll be redirected +// to authorize via OAuth and choose what permissions to grant. +// + +interface Props { + question: string; +} + +const { question } = Astro.props; +--- + +
+ {question} +
+ +
+
diff --git a/src/nimbus/components/agent-setup/FAQList.astro b/src/nimbus/components/agent-setup/FAQList.astro new file mode 100644 index 00000000000..85d4a70d690 --- /dev/null +++ b/src/nimbus/components/agent-setup/FAQList.astro @@ -0,0 +1,10 @@ +--- +// Wrapper for a series of children. +// +// +// answer body +// answer body +// +--- + + diff --git a/src/nimbus/components/agent-setup/McpServerList.astro b/src/nimbus/components/agent-setup/McpServerList.astro new file mode 100644 index 00000000000..d0efb7a7c3a --- /dev/null +++ b/src/nimbus/components/agent-setup/McpServerList.astro @@ -0,0 +1,59 @@ +--- +import { getCollection } from "astro:content"; + +interface Props { + /** Whether to show the "bundled" chip. Only relevant for plugin-based agents (Claude Code, Cursor). */ + showBundled?: boolean; +} + +const { showBundled = false } = Astro.props; + +const CODE_MODE_URL = "https://mcp.cloudflare.com/mcp"; + +const allServers = await getCollection("cloudflare-mcps-manifest"); + +// Code Mode first, then the rest in manifest order +const servers = [ + ...allServers.filter((s) => s.data.url === CODE_MODE_URL), + ...allServers.filter((s) => s.data.url !== CODE_MODE_URL), +]; + +/** + * URLs of servers included in the cloudflare/skills plugin bundle + * (Claude Code, Cursor). + */ +const BUNDLED = new Set([ + "https://mcp.cloudflare.com/mcp", + "https://docs.mcp.cloudflare.com/mcp", + "https://bindings.mcp.cloudflare.com/mcp", + "https://builds.mcp.cloudflare.com/mcp", + "https://observability.mcp.cloudflare.com/mcp", +]); +--- + +
    + { + servers.map((s) => ( +
  • + + {s.data.name} + {s.data.url === CODE_MODE_URL && ( + + code mode + + )} + {showBundled && BUNDLED.has(s.data.url) && ( + + bundled + + )} + + {s.data.description} + {s.data.url} +
  • + )) + } +
diff --git a/src/nimbus/components/agent-setup/OtherAgents.astro b/src/nimbus/components/agent-setup/OtherAgents.astro new file mode 100644 index 00000000000..7785801034e --- /dev/null +++ b/src/nimbus/components/agent-setup/OtherAgents.astro @@ -0,0 +1,59 @@ +--- +// Lists every agent in the collection except the current one. +// Replaces the old YAML-driven RelatedAgents, which required each agent to +// explicitly name which others it was related to. With only 6 agents total, +// showing "everything else" is more useful and stays correct as the list grows. +// +// Self-fetches from the collection so MDX callers only need to pass `currentSlug`: +// +import { AGENTS } from "./agents"; + +interface Props { + currentSlug: string; +} + +const { currentSlug } = Astro.props; + +const others = AGENTS.filter((a) => a.slug !== currentSlug); +--- + + diff --git a/src/nimbus/components/agent-setup/PlatformAccessDetails.astro b/src/nimbus/components/agent-setup/PlatformAccessDetails.astro new file mode 100644 index 00000000000..1250a0e3f1d --- /dev/null +++ b/src/nimbus/components/agent-setup/PlatformAccessDetails.astro @@ -0,0 +1,24 @@ +--- +// Bespoke
card used inside . Unlike the site-wide +//
, this one: +// - Renders the header as plain text (not marked.parse →

), so the +//

keeps a tight body-weight look instead of inheriting prose +// paragraph styling. +// - Uses its own class so it can coexist with the Starlight +// `.sl-markdown-content` prose styles WITHOUT needing `.not-content` on an +// ancestor. That keeps inline , links, and lists inside the body +// correctly themed. +interface Props { + header: string; + open?: boolean; +} + +const { header, open } = Astro.props; +--- + +
+ {header} +
+ +
+
diff --git a/src/nimbus/components/agent-setup/PlatformAccessSection.astro b/src/nimbus/components/agent-setup/PlatformAccessSection.astro new file mode 100644 index 00000000000..b647b9a1654 --- /dev/null +++ b/src/nimbus/components/agent-setup/PlatformAccessSection.astro @@ -0,0 +1,120 @@ +--- +import PlatformAccessDetails from "./PlatformAccessDetails.astro"; +import McpServerList from "./McpServerList.astro"; +import SkillsList from "./SkillsList.astro"; +--- + +
+

Expand any section to learn more.

+ + +

+ Persistent platform context that teaches the agent how Cloudflare works. +

+

+ Skills are instructions the agent loads on demand. The{" "} + + cloudflare/skills + {" "} + bundle covers every layer of the platform — so the agent knows your conventions + without you re-explaining them. +

+ +
+ + +

+ Live access to the Cloudflare API, docs, and observability. +

+

+ MCP servers provide typed tools to call into Cloudflare at runtime. There + are two options: Code Mode — a single server that covers the entire Cloudflare API (2,500+ endpoints + in ~1,000 tokens) — or a set of focused, domain-specific servers hosted in the{ + " " + } + + cloudflare/mcp-server-cloudflare + {" "} + repo. The full catalog is also in the{" "} + + MCP servers for Cloudflare + {" "} + docs. +

+ +
+ + +

+ Local dev, deploys, and Workers-specific commands. +

+

+ Use Wrangler for local development, deploys, + and product-specific commands like{" "} + wrangler d1 migrations apply or wrangler tail. + The bundled wrangler Skill teaches the agent when to reach + for it. +

+ +
+ + +

+ Token-efficient references optimized for agents. +

+

+ Append /index.md to any Cloudflare docs URL for a clean markdown + version. Every top-level product section also has its own{" "} + llms.txt — a page index sized for a single context window. A few + useful ones: +

+ +

+ For a full overview of how these docs are structured for agents, refer to + the Docs for Agents guide. +

+
+
diff --git a/src/nimbus/components/agent-setup/PromptCopyBlock.astro b/src/nimbus/components/agent-setup/PromptCopyBlock.astro new file mode 100644 index 00000000000..76868b14886 --- /dev/null +++ b/src/nimbus/components/agent-setup/PromptCopyBlock.astro @@ -0,0 +1,290 @@ +--- +interface Props { + text: string; + hideLabel?: boolean; +} + +const { text, hideLabel = false } = Astro.props; + +// Split "Fetch " into verb + url for syntax colouring. +// Falls back to rendering the whole string unsplit if the pattern doesn't match. +const match = text.match(/^(\S+)\s+(.+)$/); +const verb = match ? match[1] : null; +const rest = match ? match[2] : text; +--- + +
+ { + !hideLabel && ( +

+ Paste into any AI coding agent to install Cloudflare agent tooling: +

+ ) + } +
+
+ + {verb && {verb}} + {verb && " "} + {rest} + + +
+
+
+ + + + diff --git a/src/nimbus/components/agent-setup/RandomPrompt.astro b/src/nimbus/components/agent-setup/RandomPrompt.astro new file mode 100644 index 00000000000..bc2ec2556b3 --- /dev/null +++ b/src/nimbus/components/agent-setup/RandomPrompt.astro @@ -0,0 +1,15 @@ +--- +// Renders one randomly-selected example prompt as a plain-text code block. +// "Random" is resolved at build time (SSG) — each build picks a different +// prompt, which is good enough for "feels fresh" without client-side JS. +// +// Use from MDX: +// +import { Code } from "~/components"; +import { SHARED_PROMPTS } from "./prompts"; + +const prompt = + SHARED_PROMPTS[Math.floor(Math.random() * SHARED_PROMPTS.length)]; +--- + + diff --git a/src/nimbus/components/agent-setup/SkillsList.astro b/src/nimbus/components/agent-setup/SkillsList.astro new file mode 100644 index 00000000000..f6e0852225d --- /dev/null +++ b/src/nimbus/components/agent-setup/SkillsList.astro @@ -0,0 +1,19 @@ +--- +import { getCollection } from "astro:content"; + +const skills = await getCollection("cloudflare-skills-manifest"); + +// Sort alphabetically so the order is stable and deterministic +skills.sort((a, b) => a.id.localeCompare(b.id)); +--- + +
    + { + skills.map((s) => ( +
  • + {s.data.name} + {s.data.description} +
  • + )) + } +
diff --git a/src/nimbus/components/agent-setup/TipsList.astro b/src/nimbus/components/agent-setup/TipsList.astro new file mode 100644 index 00000000000..fcff41a9ed7 --- /dev/null +++ b/src/nimbus/components/agent-setup/TipsList.astro @@ -0,0 +1,52 @@ +--- +// Wrapper for tips, authored as a plain Markdown list: +// +// +// - Tip one with `code` and [links](...) +// - Tip two +// +// +// MDX renders the list as
. The global `.agent-setup-tips` +// styling is applied to that inner
    via the `:global` selector below. +--- + +
    + +
    + + diff --git a/src/nimbus/components/agent-setup/TroubleshootingItem.astro b/src/nimbus/components/agent-setup/TroubleshootingItem.astro new file mode 100644 index 00000000000..29db850273e --- /dev/null +++ b/src/nimbus/components/agent-setup/TroubleshootingItem.astro @@ -0,0 +1,40 @@ +--- +// Single troubleshooting accordion item for use from MDX. The solution is +// authored as MDX children, so it can freely use ``, links, lists, etc: +// +// +// Run `claude mcp list` to verify the server is registered. Try removing +// and re-adding with `claude mcp remove cloudflare` then re-add it. +// + +interface Props { + issue: string; +} + +const { issue } = Astro.props; +--- + +
    + + + {issue} + + +
    + +
    +
    diff --git a/src/nimbus/components/agent-setup/TroubleshootingList.astro b/src/nimbus/components/agent-setup/TroubleshootingList.astro new file mode 100644 index 00000000000..f87d0c6b725 --- /dev/null +++ b/src/nimbus/components/agent-setup/TroubleshootingList.astro @@ -0,0 +1,11 @@ +--- +// Wrapper for a series of children. +// +// +// solution body +// +--- + +
    + +
    diff --git a/src/nimbus/components/agent-setup/agents.ts b/src/nimbus/components/agent-setup/agents.ts new file mode 100644 index 00000000000..90cbe4f40f4 --- /dev/null +++ b/src/nimbus/components/agent-setup/agents.ts @@ -0,0 +1,196 @@ +import type { AgentData } from "./types"; + +export const AGENTS: AgentData[] = [ + { + name: "Claude Code", + vendor: "Anthropic", + slug: "claude-code", + icon: "claude", + description: + "Terminal-based coding agent that understands your codebase, runs commands, edits files, and manages git. Made by Anthropic.", + capabilities: { + ide: false, + terminal: true, + standalone: true, + cloud: true, + extension: true, + open_source: false, + }, + features: [ + "Full codebase understanding", + "Terminal command execution", + "Git operations", + "Multi-file editing", + ], + pricing_model: "subscription", + model_flexibility: "locked", + context_approach: "project_memory", + links: { + skills: "https://github.com/cloudflare/skills", + mcp_server: "https://github.com/cloudflare/mcp", + mcp_server_domain: "https://github.com/cloudflare/mcp-server-cloudflare", + docs: "https://docs.anthropic.com/en/docs/claude-code", + website: "https://claude.ai/code", + }, + }, + { + name: "Codex", + vendor: "OpenAI", + slug: "codex", + icon: "codex", + description: + "Lightweight open-source terminal agent that reads and writes files, runs commands, and browses the web in a sandbox. Made by OpenAI.", + capabilities: { + ide: false, + terminal: true, + standalone: true, + cloud: true, + extension: true, + open_source: true, + }, + features: [ + "File read/write operations", + "Command execution", + "Web browsing", + "Sandboxed environment", + ], + pricing_model: "hybrid", + model_flexibility: "locked", + context_approach: "project_memory", + links: { + skills: "https://github.com/cloudflare/skills", + mcp_server: "https://github.com/cloudflare/mcp", + mcp_server_domain: "https://github.com/cloudflare/mcp-server-cloudflare", + docs: "https://developers.openai.com/codex/", + website: "https://openai.com/codex", + }, + }, + { + name: "Cursor", + vendor: "Cursor", + slug: "cursor", + icon: "cursor", + description: + "AI-first IDE built on VS Code with multi-file Composer edits and background agents. Made by Cursor.", + capabilities: { + ide: true, + terminal: true, + standalone: true, + cloud: true, + extension: false, + open_source: false, + }, + features: [ + "Multi-file Composer", + "Background agents", + "Codebase indexing", + "Terminal integration", + ], + pricing_model: "subscription", + model_flexibility: "multi_provider", + context_approach: "indexed_codebase", + links: { + skills: "https://github.com/cloudflare/skills", + mcp_server: "https://github.com/cloudflare/mcp", + mcp_server_domain: "https://github.com/cloudflare/mcp-server-cloudflare", + docs: "https://docs.cursor.com", + website: "https://cursor.sh", + }, + }, + { + name: "GitHub Copilot", + vendor: "GitHub", + slug: "github-copilot", + icon: "copilot", + description: + "Editor extension and CLI with agent mode, workspace context, and native PR integration. Made by GitHub.", + capabilities: { + ide: false, + terminal: true, + standalone: false, + cloud: true, + extension: true, + open_source: false, + }, + features: [ + "Agent mode", + "Workspace context", + "CLI integration", + "PR summaries", + ], + pricing_model: "subscription", + model_flexibility: "multi_provider", + context_approach: "indexed_codebase", + links: { + skills: "https://github.com/cloudflare/skills", + mcp_server: "https://github.com/cloudflare/mcp", + mcp_server_domain: "https://github.com/cloudflare/mcp-server-cloudflare", + docs: "https://docs.github.com/en/copilot", + website: "https://github.com/features/copilot", + }, + }, + { + name: "OpenCode", + vendor: "Anomaly", + slug: "opencode", + icon: "opencode", + description: + "Open-source terminal agent with a rich TUI that works with 75+ LLMs. Made by Anomaly.", + capabilities: { + ide: false, + terminal: true, + standalone: true, + cloud: false, + extension: true, + open_source: true, + }, + features: [ + "75+ model support", + "Rich terminal TUI", + "Built-in agents (build/plan)", + "LSP integration", + ], + pricing_model: "byok", + model_flexibility: "multi_provider", + context_approach: "project_memory", + links: { + skills: "https://github.com/cloudflare/skills", + mcp_server: "https://github.com/cloudflare/mcp", + mcp_server_domain: "https://github.com/cloudflare/mcp-server-cloudflare", + docs: "https://opencode.ai/docs", + website: "https://opencode.ai", + }, + }, + { + name: "Windsurf", + vendor: "Cognition", + slug: "windsurf", + icon: "windsurf", + description: + "Agentic IDE with Cascade context and Flows for multi-step tasks. Made by Cognition.", + capabilities: { + ide: true, + terminal: false, + standalone: true, + cloud: false, + extension: false, + open_source: false, + }, + features: [ + "Cascade context engine", + "Flows automation", + "Deep codebase search", + "Command suggestions", + ], + pricing_model: "subscription", + model_flexibility: "multi_provider", + context_approach: "indexed_codebase", + links: { + skills: "https://github.com/cloudflare/skills", + mcp_server: "https://github.com/cloudflare/mcp", + mcp_server_domain: "https://github.com/cloudflare/mcp-server-cloudflare", + docs: "https://docs.windsurf.com", + website: "https://windsurf.com", + }, + }, +]; diff --git a/src/nimbus/components/agent-setup/definitions.ts b/src/nimbus/components/agent-setup/definitions.ts new file mode 100644 index 00000000000..9b6cfe2dcf4 --- /dev/null +++ b/src/nimbus/components/agent-setup/definitions.ts @@ -0,0 +1,49 @@ +// Centralized tooltip copy and display labels for agent capability badges. + +export const capabilityDefinitions = { + terminal: + "Runs as a command-line tool. Great for scripting, automation, and CI pipelines.", + ide: "A full code editor with AI built in. Visual diffs, inline suggestions, multi-file edits.", + cloud: + "Runs on hosted infrastructure. Accessible from anywhere, good for async long-running tasks.", + extension: + "Add-on that plugs into an existing editor. Lightweight install, inherits the editor's features.", + standalone: + "Can run on its own without embedding in an editor or extension host.", + open_source: "Source code is openly licensed and available on GitHub.", +} as const; + +export type CapabilityKey = keyof typeof capabilityDefinitions; + +export const capabilityTooltips: Record = { + subscription: "Fixed recurring fee billed monthly or annually.", + byok: "Bring Your Own Key — the tool is free; you pay your model provider directly.", + hybrid: + "Combines multiple pricing models, for example a free tier plus BYOK.", + locked: "Models from other providers are not supported.", + multi_provider: + "Supports multiple model providers out of the box (OpenAI, Anthropic, Google, local models, etc.).", + session: + "Context is limited to the current conversation. No memory between runs.", + project_memory: + "Retains context about your project across runs — configuration files, past conversations, preferences.", + indexed_codebase: + "Builds a semantic index of your whole codebase so the agent can reference any file.", +}; + +export const capabilityLabels: Record = { + terminal: "Terminal", + ide: "IDE", + cloud: "Cloud", + extension: "Extension", + standalone: "Standalone", + open_source: "Open Source", + subscription: "Subscription", + byok: "BYOK", + hybrid: "Hybrid", + locked: "Locked", + multi_provider: "Multi-provider", + session: "Session", + project_memory: "Project memory", + indexed_codebase: "Indexed codebase", +}; diff --git a/src/nimbus/components/agent-setup/prompts.ts b/src/nimbus/components/agent-setup/prompts.ts new file mode 100644 index 00000000000..379c385dc50 --- /dev/null +++ b/src/nimbus/components/agent-setup/prompts.ts @@ -0,0 +1,35 @@ +export const SHARED_PROMPTS = [ + // AI applications + "Build an AI chat agent using the Cloudflare Agents SDK with persistent conversation history stored in D1.", + "Create a RAG pipeline using Vectorize and Workers AI to answer questions over my documentation.", + "Set up AI Gateway to route requests across OpenAI and Workers AI with automatic fallback and cost tracking.", + "Build a serverless AI inference endpoint on Workers AI with streaming responses.", + // Web apps & serverless backends + "Deploy a full-stack React app to Cloudflare Pages with a Workers API backend and D1 database.", + "Add a D1 database to my Worker and create a users table with full CRUD endpoints.", + "Build an image upload and transformation service using R2 and Cloudflare Images.", + "Add real-time collaboration to my app using Durable Objects with WebSocket hibernation.", + "Set up a KV namespace for edge-cached session storage in my Worker.", + "Add a cron trigger to my Worker that processes a job queue every hour.", + // APIs & microservices + "Deploy a globally distributed REST API on Workers with automatic scaling and zero cold starts.", + "Connect my Worker to an existing Postgres database using Hyperdrive for connection pooling.", + "Add mTLS authentication and schema validation to protect my API endpoints.", + "Set up rate limiting and WAF rules to block abuse on my public API.", + // SaaS & multi-tenant + "Build a multi-tenant SaaS backend where each customer gets an isolated D1 database.", + "Set up custom domains with automatic SSL for my SaaS customers using SSL for SaaS.", + "Use Workers for Platforms to let my customers deploy their own code in isolated environments.", + // Security + "Add bot protection and rate limiting to my login and checkout endpoints.", + "Set up WAF rules to block SQL injection and XSS attacks on my application.", + "Configure Zero Trust access policies to protect my internal staging environment.", + // Performance & e-commerce + "Configure caching rules and cache TTLs to reduce origin load for my e-commerce store.", + "Set up a Waiting Room to handle flash sale traffic spikes without dropping requests.", + "Optimize my Worker to serve WebP images with responsive resizing using Cloudflare Images.", + // Observability & DevOps + "Check my Workers deployment logs for errors and suggest fixes.", + "Set up GitHub Actions to deploy this Worker to staging and production on Cloudflare.", + "Create a Logpush job to stream Workers analytics to my data warehouse.", +]; diff --git a/src/nimbus/components/agent-setup/types.ts b/src/nimbus/components/agent-setup/types.ts new file mode 100644 index 00000000000..d1da0975b83 --- /dev/null +++ b/src/nimbus/components/agent-setup/types.ts @@ -0,0 +1,31 @@ +export type PricingModel = "subscription" | "byok" | "hybrid"; +export type ModelFlexibility = "locked" | "multi_provider"; +export type ContextApproach = "session" | "project_memory" | "indexed_codebase"; + +export interface AgentData { + name: string; + vendor: string; + slug: string; + icon: string; + description: string; + capabilities: { + ide: boolean; + terminal: boolean; + standalone: boolean; + cloud: boolean; + extension: boolean; + open_source: boolean; + }; + features: string[]; + pricing_model?: PricingModel; + model_flexibility?: ModelFlexibility; + context_approach?: ContextApproach; + links: { + skills?: string; + mcp_server?: string; + mcp_server_domain?: string; + cli?: string; + docs?: string; + website?: string; + }; +} diff --git a/src/nimbus/components/agents.ts b/src/nimbus/components/agents.ts new file mode 100644 index 00000000000..cc9785b3f26 --- /dev/null +++ b/src/nimbus/components/agents.ts @@ -0,0 +1,20 @@ +// AI coding agents shown on the landing page, plus the prompt they copy. + +export const AGENT_SETUP_PROMPT = + "Fetch https://developers.cloudflare.com/agent-setup/prompt.md"; + +// Each id maps to /icons/agents/{id}/{light,dark}.svg. +export const AGENTS = [ + { id: "claude", label: "Claude Code", href: "/agent-setup/claude-code/" }, + { id: "codex", label: "Codex", href: "/agent-setup/codex/" }, + { id: "cursor", label: "Cursor", href: "/agent-setup/cursor/" }, + { id: "opencode", label: "OpenCode", href: "/agent-setup/opencode/" }, + { + id: "copilot", + label: "GitHub Copilot", + href: "/agent-setup/github-copilot/", + }, + { id: "windsurf", label: "Windsurf", href: "/agent-setup/windsurf/" }, +] as const; + +export type AgentId = (typeof AGENTS)[number]["id"]; diff --git a/src/nimbus/components/ai-gateway/code-example-selector.tsx b/src/nimbus/components/ai-gateway/code-example-selector.tsx new file mode 100644 index 00000000000..115ec2092c6 --- /dev/null +++ b/src/nimbus/components/ai-gateway/code-example-selector.tsx @@ -0,0 +1,504 @@ +import { useEffect, useState } from "react"; +import ReactSelect from "react-select"; + +const STORAGE_KEY = "ai-gateway-code-selector"; +const AIG_EVENT = "ai-gateway-selector-change"; + +export type Provider = + | "openai" + | "anthropic" + | "google" + | "grok" + | "dynamic" + | "workers-ai"; +export type KeyType = "byok" | "in-request" | "unified"; +export type ClientType = "openai-js" | "curl" | "aisdk"; +export type APIType = "native" | "unified"; + +export interface Config { + provider: Provider; + keyType: KeyType; + clientType: ClientType; + apiType: APIType; +} + +/** + * Shared hook to read and update the currently selected platform/framework. + * + * - Persists selection in localStorage under STORAGE_KEY. + * - Broadcasts changes via the `ai-gateway-selector-change` custom event. + * - Listens to the same event to stay in sync across multiple components. + */ +export function useAIGConfig() { + const [config, setConfig] = useState({ + provider: "openai", + keyType: "byok", + clientType: "openai-js", + apiType: "unified", + }); + + // Helper: broadcast selection changes so other listeners can sync. + function notifySelectionChange(_config: Partial) { + if (typeof window === "undefined") return; + try { + window.dispatchEvent( + new CustomEvent(AIG_EVENT, { + detail: { ...config, ..._config }, + }), + ); + } catch { + // Ignore event dispatch errors. + } + } + + // Initialise selection from localStorage (if available) on first render. + useEffect(() => { + if (typeof window === "undefined") return; + + try { + const raw = window.localStorage.getItem(STORAGE_KEY); + if (raw) { + const parsed = JSON.parse(raw) as Config; + + setConfig((prev) => ({ ...prev, ...parsed })); + notifySelectionChange(parsed); + return; + } + } catch { + // Ignore JSON or storage errors and fall back to defaults. + } + + notifySelectionChange(config); + }, []); + + // Keep local state in sync with external changes. + useEffect(() => { + if (typeof window === "undefined") return; + + function handleChange( + event: Event & { + detail?: Config; + }, + ) { + if (!event.detail) return; + setConfig((prev) => ({ ...prev, ...event.detail })); + } + + window.addEventListener(AIG_EVENT, handleChange as EventListener); + + return () => { + window.removeEventListener(AIG_EVENT, handleChange as EventListener); + }; + }, []); + + function updateConfig(c: Partial) { + setConfig((prev) => { + const updated = { ...prev, ...c }; + + if (typeof window !== "undefined") { + try { + window.localStorage.setItem( + STORAGE_KEY, + JSON.stringify({ + ...updated, + }), + ); + } catch { + // Ignore storage errors. + } + } + + notifySelectionChange(updated); + return updated; + }); + } + + return { + config, + updateConfig, + }; +} + +type ProviderItem = { + value: Provider; + label: string; + icon?: string; + invertIconDark?: boolean; +}; +const providerOptions: ProviderItem[] = [ + { + value: "openai", + label: "OpenAI", + invertIconDark: true, + icon: "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPgo8c3ZnIGZpbGw9IiMwMDAwMDAiIHdpZHRoPSI2NHB4IiBoZWlnaHQ9IjY0cHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgcm9sZT0iaW1nIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjx0aXRsZT5PcGVuQUkgaWNvbjwvdGl0bGU+PHBhdGggZD0iTTIyLjI4MTkgOS44MjExYTUuOTg0NyA1Ljk4NDcgMCAwIDAtLjUxNTctNC45MTA4IDYuMDQ2MiA2LjA0NjIgMCAwIDAtNi41MDk4LTIuOUE2LjA2NTEgNi4wNjUxIDAgMCAwIDQuOTgwNyA0LjE4MThhNS45ODQ3IDUuOTg0NyAwIDAgMC0zLjk5NzcgMi45IDYuMDQ2MiA2LjA0NjIgMCAwIDAgLjc0MjcgNy4wOTY2IDUuOTggNS45OCAwIDAgMCAuNTExIDQuOTEwNyA2LjA1MSA2LjA1MSAwIDAgMCA2LjUxNDYgMi45MDAxQTUuOTg0NyA1Ljk4NDcgMCAwIDAgMTMuMjU5OSAyNGE2LjA1NTcgNi4wNTU3IDAgMCAwIDUuNzcxOC00LjIwNTggNS45ODk0IDUuOTg5NCAwIDAgMCAzLjk5NzctMi45MDAxIDYuMDU1NyA2LjA1NTcgMCAwIDAtLjc0NzUtNy4wNzI5em0tOS4wMjIgMTIuNjA4MWE0LjQ3NTUgNC40NzU1IDAgMCAxLTIuODc2NC0xLjA0MDhsLjE0MTktLjA4MDQgNC43NzgzLTIuNzU4MmEuNzk0OC43OTQ4IDAgMCAwIC4zOTI3LS42ODEzdi02LjczNjlsMi4wMiAxLjE2ODZhLjA3MS4wNzEgMCAwIDEgLjAzOC4wNTJ2NS41ODI2YTQuNTA0IDQuNTA0IDAgMCAxLTQuNDk0NSA0LjQ5NDR6bS05LjY2MDctNC4xMjU0YTQuNDcwOCA0LjQ3MDggMCAwIDEtLjUzNDYtMy4wMTM3bC4xNDIuMDg1MiA0Ljc4MyAyLjc1ODJhLjc3MTIuNzcxMiAwIDAgMCAuNzgwNiAwbDUuODQyOC0zLjM2ODV2Mi4zMzI0YS4wODA0LjA4MDQgMCAwIDEtLjAzMzIuMDYxNUw5Ljc0IDE5Ljk1MDJhNC40OTkyIDQuNDk5MiAwIDAgMS02LjE0MDgtMS42NDY0ek0yLjM0MDggNy44OTU2YTQuNDg1IDQuNDg1IDAgMCAxIDIuMzY1NS0xLjk3MjhWMTEuNmEuNzY2NC43NjY0IDAgMCAwIC4zODc5LjY3NjVsNS44MTQ0IDMuMzU0My0yLjAyMDEgMS4xNjg1YS4wNzU3LjA3NTcgMCAwIDEtLjA3MSAwbC00LjgzMDMtMi43ODY1QTQuNTA0IDQuNTA0IDAgMCAxIDIuMzQwOCA3Ljg3MnptMTYuNTk2MyAzLjg1NThMMTMuMTAzOCA4LjM2NCAxNS4xMTkyIDcuMmEuMDc1Ny4wNzU3IDAgMCAxIC4wNzEgMGw0LjgzMDMgMi43OTEzYTQuNDk0NCA0LjQ5NDQgMCAwIDEtLjY3NjUgOC4xMDQydi01LjY3NzJhLjc5Ljc5IDAgMCAwLS40MDctLjY2N3ptMi4wMTA3LTMuMDIzMWwtLjE0Mi0uMDg1Mi00Ljc3MzUtMi43ODE4YS43NzU5Ljc3NTkgMCAwIDAtLjc4NTQgMEw5LjQwOSA5LjIyOTdWNi44OTc0YS4wNjYyLjA2NjIgMCAwIDEgLjAyODQtLjA2MTVsNC44MzAzLTIuNzg2NmE0LjQ5OTIgNC40OTkyIDAgMCAxIDYuNjgwMiA0LjY2ek04LjMwNjUgMTIuODYzbC0yLjAyLTEuMTYzOGEuMDgwNC4wODA0IDAgMCAxLS4wMzgtLjA1NjdWNi4wNzQyYTQuNDk5MiA0LjQ5OTIgMCAwIDEgNy4zNzU3LTMuNDUzN2wtLjE0Mi4wODA1TDguNzA0IDUuNDU5YS43OTQ4Ljc5NDggMCAwIDAtLjM5MjcuNjgxM3ptMS4wOTc2LTIuMzY1NGwyLjYwMi0xLjQ5OTggMi42MDY5IDEuNDk5OHYyLjk5OTRsLTIuNTk3NCAxLjQ5OTctMi42MDY3LTEuNDk5N1oiLz48L3N2Zz4=", + }, + { + value: "anthropic", + label: "Anthropic", + invertIconDark: true, + icon: "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNTYiIGhlaWdodD0iMjU2IiBmaWxsPSJub25lIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+CiAgICA8cGF0aCBmaWxsPSJ1cmwoI2EpIiBkPSJNMCAwaDU4NXY1ODVIMHoiLz4KICAgIDxkZWZzPgogICAgICA8cGF0dGVybiBpZD0iYSIgd2lkdGg9IjEiIGhlaWdodD0iMSIgcGF0dGVybkNvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICAgIDx1c2UgaHJlZj0iI2IiIHRyYW5zZm9ybT0ic2NhbGUoLjAwMTcpIi8+CiAgICAgIDwvcGF0dGVybj4KICAgICAgPGltYWdlIGlkPSJiIiB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiIgZGF0YS1uYW1lPSJBbnRocm9waWMucG5nIiBocmVmPSJkYXRhOmltYWdlL3BuZztiYXNlNjQsaVZCT1J3MEtHZ29BQUFBTlNVaEVVZ0FBQWtrQUFBSkpDQVlBQUFDK2dLTTBBQUFBQ1hCSVdYTUFBQXNUQUFBTEV3RUFtcHdZQUFBQUFYTlNSMElBcnM0YzZRQUFBQVJuUVUxQkFBQ3hqd3Y4WVFVQUFCK2lTVVJCVkhnQjdkMUJiaHpYbmNmeDhpRHJYRUNCdlJYWER1VHRERUF1TTREb3JRUHFBdEk2UjRpM1RXQ3lGQVZrSytvQTdNUnI4d0xrQWFJTDVBS2VQQ1ZNV2svLzk2cTZ1N3E3cXQ3bkF4Z0RETEpLYVBhdjMvdXk2cXR2dnZuNmx3NEFnTS84VndjQXdCZU1KQUNBZ0pFRUFCQXdrZ0FBQWtZU0FFREFTQUlBQ0JoSkFBQUJJd2tBSUdBa0FRQUVqQ1FBZ0lDUkJBQVFNSklBQUFKR0VnQkF3RWdDQUFnWVNRQUFBU01KQUNCZ0pBRUFCSXdrQUlDQWtRUUFFRENTQUFBQ1JoSUFRTUJJQWdBSUdFa0FBQUVqQ1FBZ1lDUUJBQVNNSkFDQWdKRUVBQkF3a2dBQUFrWVNBRURBU0FJQUNCaEpBQUFCSXdrQUlHQWtBUUFFakNRQWdJQ1JCQUFRTUpJQUFBSkdFZ0JBd0VnQ0FBZ1lTUUFBQVNNSkFDQmdKQUVBQkl3a0FJQ0FrUVFBRURDU0FBQUNSaElBUU1CSUFnQUlHRWtBQUFFakNRQWdZQ1FCQUFTTUpBQ0FnSkVFQUJBd2tnQUFBa1lTQUVEQVNBSUFDQmhKQUFBQkl3a0FJR0FrQVFBRWpDUUFnSUNSQkFBUU1KSUFBQUpHRWdCQXdFZ0NBQWdZU1FBQUFTTUpBQ0JnSkFFQUJJd2tBSUNBa1FRQUVEQ1NBQUFDUmhJQVFNQklBZ0FJR0VrQUFBRWpDUUFnWUNRQkFBU01KQUNBZ0pFRUFCQXdrZ0FBQWtZU0FFREFTQUlBQ0JoSkFBQUJJd2tBSUdBa0FRQUVqQ1FBZ0lDUkJBQVFNSklBQUFKR0VnQkF3RWdDQUFnWVNRQUFBU01KQUNCZ0pBRUFCSXdrQUlDQWtRUUFFRENTQUFBQ1JoSUFRTUJJQWdBSUdFa0FBQUVqQ1FBZ1lDUUJBQVNNSkFDQWdKRUVBQkF3a2dBQUFrWVNBRURBU0FJQUNCaEpBQUFCSXdrQUlHQWtBUUFFakNRQWdJQ1JCQUFRTUpJQUFBSkdFZ0JBd0VnQ0FBZ1lTUUFBQVNNSkFDQmdKQUVBQkl3a0FJQ0FrUVFBRURDU0FBQUNSaElBUU1CSUFnQUlHRWtBQUFFakNRQWdZQ1FCQUFTTUpBQ0FnSkVFQUJBd2tnQUFBa1lTQUVEQVNBSUFDUHlxZzRiOCtPTWZ1OHZMeTQ1aGZ2ZTcvKzBlSHgrN1Z2ejYxNy91ZnZycHI1LytMN0Z2di8xdDkvZS8vNzJERmpoSm9oblBuajB6a0xaMGNYSFJ0U1I5K04vZWZ1Z291N3E2NnFBVlJoTE4rTzY3N3pxMjgrclZWWE9uS3V2MVhVZForcG1BVmhoSk5PUE5tOWNkMjBrRDZjV0xGMTFMZnY3NS9oLy8vTndSU3o4VDMzM1gxczhFN1RLU2FFTDZvRS9YYld5dnhaT0Q5Zm92SFdXdlgvdkNRUnVNSkpydy9mZGFwRjJsYThybno1OTNMYm05dlJVblY2U2ZDWEU3TFRDU1dEekI5djVhRExqdjd0WWRaUUp1V21Ba3NYaUM3ZjIxR0hCLytIRGJVU2JncGdWR0Vvc24yTjVmcXdIM3c4TkRSMHpBVFF1TUpCWk5zRDJlRms4T1hMblZDYmhaT2lPSlJSTnNqNmZGZ1B2ZHUzY0M3Z29CTjB0bkpMRllndTN4dFJodzM5L2ZkNVFKdUZreUk0bkZFbXlQcjhXQSsrYm1wcU5Nd00yU0dVa3NsbUI3ZkFKdWNnSnVsc3hJWXBFRTI0Y2o0Q1luNEdhcGpDUVdTYkI5T0MzR3VnTHVPZ0UzUzJVa3NUaUM3Y05yTGRZVmNQY1RjTE5FUmhLTEk5Zyt2QmF2M0FUY2RRSnVsc2hJWW5FRTI0ZlhZcXdyNEs0VGNMTkVSaEtMSXRnK25oWmpYUUYzbllDYnBUR1NXQlRCOXZFSXVNa0p1RmthSTRuRkVHd2ZuNENibklDYkpUR1NXQXpCOXZFSnVNa0p1RmtTSTRuRkVHd2ZYNnNCOThlUEh6dGlBbTZXeEVoaUVRVGJwOU5pclB2Ky9XMUhtWUNicFRDU1dBVEI5dW0wR25CVEp1Qm1LWXdrWmsrd2ZYb3RCdHcvLy94elI1bUFteVV3a3BnOXdmYnB0UmpyWGw5ZmQ1UmRYSngzTUhkR0VyTW4yRDY5VmdOdXowd3FPenM3RTNBemUwWVNzeWJZbm80V1k5MmJHMjFTemRYVnF3N216RWhpMWdUYjB5SGdKcGRPa2dUY3pKbVJ4R3dKdHFkSHdNMm1OSkFFM015WmtjUnNDYmFuUjhCTlRzRE5uQmxKekpaZ2Uzb0UzT1FFM015WmtjUXNDYmFuUzhCTlRzRE5YQmxKekpKZ2U3b0UzT1FFM015VmtjVHNDTGFuVDhETkpnRTNjMlVrTVR1Qzdla1RjSk1UY0ROSFJoS3pJOWllUGdFM09RRTNjMlFrTVN1Qzdma1FjSk1UY0RNM1JoS3pJdGllRHdFM09RRTNjMk1rTVJ2cGwrdjV1YTVoVGdUY2JCSndNemRHRXJPUkJwSnZvZk1pNENZbjRHWk9qQ1JtbzhVUDNMa1RjSk1UY0RNblJoS3prSDZ4cG4rWUh3RTN1ZlB6aXc3bXdFaGlGcHdpelplQW05emw1VXRYNTh5Q2tjVGtDYmJuVDhETnB2VHY5TXVYTHp1WU9pT0p5Uk5zejUrQW01eUFtemt3a3BnOFYyM3pKK0FtbDY1aEJkeE1uWkhFcEFtMmwwUEFUVTdBemRRWlNVeWFVNlRsRUhDVEUzQXpkVVlTa3lYWVhoNEJONXNFM0V5ZGtjUmtDYmFYUjhCTlRzRE5sQmxKVEphcnR1VVJjSk1UY0RObFJoS1RKTmhlTGdFM09RRTNVMlVrTVVsT2taWkx3RTFPd00xVUdVbE1qbUI3K1FUY2JCSndNMVZHRXBNajJGNCtBVGM1QVRkVFpDUXhPZW5vbldVVGNKTVRjRE5GUmhLVDh1elpzMCsvTEZrK0FUYzVBVGRUWXlReEtXL2V0UGZCMlNvQk56a0JOMU5qSkRFcFRwSGFJdUJtazRDYnFUR1NtSXpMeTh0UDEyMjBROEJOVHNETmxCaEpUSVpndXowQ2JuSUNicWJFU0dJU0JOdnRFbkNURTNBekZVWVNreURZYnBlQW01eUFtNmt3a3BnRXAwaHRFM0N6U2NETlZCaEpuSnhnbXhaalhRRjNuWUNiS1RDU09EbkJObWRuWndKdVBpUGdaZ3FNSkU1S3NNMlRxNnRYWFdzRTNIVUNiazdOU09La0JOczhTYWNHQW00MkNiZzVOU09KazNLS3hKUDBZU2pnWnBPQW0xTXpramdad1RZNUFUYzVBVGVuWkNSeE1vSnRjZ0p1Y2dKdVRzbEk0aVFFMjVRSXVNa0p1RGtWSTRtVEVHeFRJdUFtSitEbVZJd2tUc0lwRWlVQ2JuSUNiazdGU09Mb0JOdjBhVEhXZFpwVUorRG1GSXdrams2d1RSOEJOemtCTjZkZ0pIRlVnbTJHYWkzZ1RnTkp3RjBuNE9iWWpDU09TckROVUMwRzNPdjF1cU5Nd00yeEdVa2NsVk1raG1veDRINTRlQkJ3VndpNE9UWWppYU1SYkxNdEFUYzVBVGZIWkNSeE5JTHRNc0Z1VE1CTlRzRE5NUmxKSElWZ3Uremp4NC9kSC83d2g0NVlhN0d1Z0x1ZmdKdGpNWkk0Q3NGMjJXcTFjbnBRMFdLc0srQ3VFM0J6TEVZU1IrRVVxZXhwSU4zZWZ1ajRVb3V4cm9DN1RzRE5zUmhKSEp4Z3UrejI5dmJUZFZ1eVh0OTF4QVRjNUFUY0hJT1J4TUVKdHN2U1NIcVNUcFNjSHNSYWpIVmR3ZFlKdURrR0k0bURFbXlYcFJPazlFRzRhYjMrUzBkTXdFMU93TTJoR1VrY2xHQzc3UDM3MnkvK2YrbGt5ZWxCVE1CTlRzRE5vUmxKSEpSVHBMTE5xN1luQXU0eUFUYzVBVGVIWmlSeE1JTHRzdlRCOXhSczV3VGNaUUp1Y2dKdURzbEk0bUFFMjJYUktkS1QxQ21sRXdTK0pPQW1KK0Rta0l3a0RrS3dYWlpPa1BxdTFPN3V0Q2dsTFFiY3JtRHJCTndjaXBIRVFRaTJ5NFkwSnE1WXl0b011RjNCMWdpNE9SUWppWU53aWxTMldsMzMvbWZTNllGZ045WmlyT3NaV25YcForTDhYSnZFK0l3a1JpZllMcXNGMjducjYvNHgxYW9XWTEzUDBLclRRSElJUmhLajg4dXFyQlpzNXdTN1pTM0d1cDZoVlpkK0pwNC9mOTdCbUl3a1JpWFlMa3NmY051ZUJuamljcG1BbTl6RmhZQ2JjUmxKakVxd1haYWVucnp0U2NBMkowK3RFWENUZS9YcVNzRE5xSXdrUnVVVXFXeVh3ZlBQOTdzSmRpTUNibkxwWitMRkM4OU1ZanhHRXFNUmJKZEZMN01keWhWTG1ZQ2JYRHBOZ3JFWVNZeEdzRjIyV3EyNlhlMXlUZGNLQVRjNUFUZGpNcElZaFdDN2J0ZFRwRVN3V3lmZ0ppZmdaaXhHRXFNUWJKZWxiLzVEbjQxVUl0Z3RFM0NURTNBekZpT0pVVGhGS3J1NzIvOERUYkJiSnVBbUorQm1MRVlTZXhOc2w2VVRwTEZDVzhGdW1ZQ2JuSUNiTVJoSjdFMndYWFozdCs3R0l0Z3RFM0NURTNBekJpT0p2UWkyNjI1dWJycXhwQS9FKy92ZEEvQ2xFM0NURTNDekx5T0p2UWkyeTdaNW1lMVFZNDZ1cFJGd2t4TndzeThqaWIwNFJTbzd4Q3RGdlBTMlRNQk5Uc0ROdm93a2RwWmlXY0YyTEowZ0hlb3F4RXR2eXdUYzVBVGM3TU5JWW1mcHI5cUlIZkxiL2J0M1JsS0pnSnRjK3Bud1pZNWRHVW5zSlAzU09UOXY3MXY3VUcvZjNuU0hrajRRWGJHVUNiakorVUxIcm93a2R1S1hUdG5EdzBQMytQallIZEwxOVhWSFRNQk56cFVidXpLUzJNbjMzeHRKSmNmNEN6UUJkNW1BbTF6Nm1XanRHcFp4R0Vsc1RiQmR0OC9MYkxjaDRDNFRjSk43L2RyalN0aWVrY1RXWExXVmpmRXkyNkhXNi9HZTVyMDBBbTV5NldmQ001UFlscEhFVmdUYmRZZDRObEpKYXA5Y3NaUUp1TWxkWFdtVDJJNlJ4RmFjSXBXbEU2UmpYYlU5Y2NWU0p1QW1KK0JtVzBZU1d4RnNsNjFXcSs3WVhMR1VDYmpKQ2JqWmxwSEVZSUx0dW1PZklpV3VXT29FM09RRTNHekRTR0l3VjIxbGQzZnJvd1hiT1Zjc1pRSnVjZ0p1dG1Fa01ZaGd1KzcyOW4xM0txNVk2Z1RjNUFUY0RHVWtNWWhUcExKMGduVHFLdzVYTEdVQ2JuSUNib1l5a2hoRXNGMDJoVk1jVnl4bEFtNXlBbTZHTXBMb0pkaXVXNjFPL3g2MU5KRHU3NDhmanMrRmdKdWNnSnNoakNSNnVXb3JTOS9XVHhWczU0N3h6cmk1RW5DVEUzQXpoSkZFbFdDNzdwaFAyTzdqcGJkMUFtNXlBbTc2R0VsVXBXOWJ4TklKMHRRK2hMejB0a3pBVFU3QVRSOGppYW8zYjl6Ymwwd3hqSDMzemtncUVYQ1RFM0RUeDBpaTZNV0xGNEx0aXJkdmI3cXBTVmNzUGhUTEJOemtCTnpVR0VrVStiUC9zb2VIaCs3eDhiR2JvdXZyMC8rMTNWUUp1TWtKdUtreGtnaWxFeVIvMVZZMjViOGtFM0RYQ2JqSkNiZ3BNWklJQ2JiclR2RXkyMjBJdU1zRTNPUUUzSlFZU1lRRTIyWHArbUlxejBZcXViL1hKWldrZ2RUYVl5MEUzSFVDYmtxTUpMNGcySzZiMHJPUlNud28xcVhUcE5ZSXVPc0UzRVNNSkw0ZzJDNUxKMGhUdjJwNzRrT3hMRjBuUDMvK3ZHdUpnTHRPd0UzRVNPSXpndTI2S2Y3WmY0a1B4YnFMQ3dFM254Tndrek9TK0l4Z3UyNjlYbmR6NFVPeExzVzZBbTQyQ2JqSkdVbDhSckJkZG5lM25ueXduZk9oV0pZR1V1cnZXcUpWcXhOd2t6T1MrRGZCZHQwY0I0Y1B4Ym9XVHc2MGFuVUNiallaU2Z5YllMdHNpaSt6SFdvdW9ma3BDTGpKQ2JqWlpDVHhpV0M3YnM2bk1WNTZXeWZnSmlmZzVvbVJ4Q2VDN2JyVmFyN3ZRL1BTMnpvQk56a0JOMCtNSkQ0UmJKZWxnVEczWUR2bnBiZGxyUWJjNlNYTnhBVGNQREdTRUd6M21NTVR0dnQ0NlcxZGl5Y0g2YTgxS1JOd2t4aEpDTFlyMHJCWXlsOERlZWx0V1lzQmQyclZET2N5QVRlSmtkUTR3WFpkZW5qa1VqNUlCTngxTFFiYzkvZis4ckZHd0kyUjFEakJkdDBTcnRxZUNManJXZ3k0YjI1dU9zb0UzQmhKalJOc2w4M3BaYlpET1UwcUUzQ1RFM0JqSkRWTXNGMjNXcTI2cFJGdzF3bTR5UW00MjJZa05VeXdYYmZFSjFWN2tHQ2RnSnVjZ0x0dFJsS2pCTnQxcVVXYSs3T1JTanhJc0U3QVRVN0EzUzRqcVZHQzdib2xCZHM1TDcydGEvSEtUY0JkSitCdWw1SFVLTUYyMlJLRDdadzN3WmUxR09zS3VPc0UzTzB5a2hvazJLNTcvMzY1cDBoUHZBbStyc1ZZVjhCZEorQnVrNUhVSU1GMjNaS3YycDRJdU90YWpIVUYzSFVDN2pZWlNZMFJiTmN0NFdXMlF3bTQ2MXFMZFFYYy9RVGM3VEdTR2lQWXJtdmhGT21KRHFWT3dFMU93TjBlSTZreGd1MnlkSUxVMmhXVURxVk13RTFPd04wZUk2a2hndTI2RnY4czNtdEs2Z1RjNUFUY2JUR1NHaUxZcmx1dHJydldlT2x0WGFzQk4yVUM3cllZU1kwUWJOZTFGR3pucnEvYkc0ZmJhREhnTnB6ckJOenRNSklhSWRpdWF5bll6bm5wYlYyTHNhN2hYQ2ZnYm9lUjFBakJkbGthQ0swL2dmcm14aFZMU2FzQnQrRmNKdUJ1aDVIVUFNRjIzWHE5YnY0RFFZZFMxMktzYXpqWFhWMjk2bGcrSTZrQmd1MjZscS9hbnVoUTZnVGM1TkpKa29CNytZeWtoUk5zMTdYd010dWhmQ2pXQ2JqWmxBYVNnSHY1aktTRkUyelhyVmFyam4vU29kUUp1TWxkWEp4M0xKdVJ0SENDN1RxblNQL2hwYmQxQW01eVoyZG5BdTZGTTVJV1RMQmRsMXFrVnArTlZPS2x0M1VDYm5JQzdtVXpraFpNc0YxM2QyY1E1TkxKZ1E2bFRNQk5Uc0M5YkViU1FnbTI2OUlKVXV2UFJpcngzMHVkZ0p0TkF1NWxNNUlXU3JCZDV5V2VaZWthVW9kU0p1QW1KK0JlTGlOcG9RVGJkVGMzTngyeE5KQ015RElCTnprQjkzSVpTUXNrMks1citXVzJRMzM0NEFHYk5RSnVjZ0x1WlRLU0ZraXdYZWNKMi8zU3lZRWhXU2JnSmlmZ1hpWWphV0VFMjNYcGc5K3pnSVo1Lzk2WXJCRndzMG5BdlV4RzBzSUl0dXY4a2gvT3lVR2RnSnVjZ0h0NWpLU0ZhZkVYOXpaV0s3L2toM0p5VUNmZ0ppZmdYaDRqYVVIU3Y2RHBIMklQRHc4Nm15MDVPYWdUY0pNVGNDK0xrYlFnVHBIcS9Obi85cHdjMUFtNHlRbTRsOFZJV29qMEwrWDV1ZnZ3R2krejNZMlRnem9CTjV2UzcrS1hMMTkyTElPUnRCQnBJUG4yVXVabHRydGJyejFZc2tiQVRVN0F2UnhHMGtLNGFxdnpiS1RkcFpiTHlVR1pnSnRjdW9ZVmNDK0RrYlFBZ3UyNmRJTGtxbTAvWG5wYkorQW1kMzUrMFRGL1J0SUNPRVdxVzYxV0hmdngwdHM2QVRlNXk4dVhFb2dGTUpKbVRyRGR6eW5TL3RKQThxVHlPZ0UzbXdUY3kyQWt6WnhndXk2OXpWNndQWTcxK3E2alRNQk5Uc0E5ZjBiU3pMbHFxN3U5ZmQ4eGpuUWk1K1NnVE1CTlRzQTlmMGJTakFtMjY5SUprdUI0WFA3N3JCTndreE53ejV1Uk5HTk9rZXFjZW94UHdGMG40Q1luNEo0M0kybW1CTnY5dk14MmZHa2czZDhMNFdzRTNHd1NjTStia1RSVGd1MjY5RXRic0gwWTNvRlhKK0FtSitDZUx5TnBwbHkxMVhuQzl1R0lkZXNFM09RRTNQTmxKTTJRWUxzdW5TQjVwczloaVhYckJOemtCTnp6WkNUTmtGT2tPbjNFNFlsMTZ3VGM1QVRjOC9Tcmp0bEp2NEFwKzgxdmZ0UDkrT01mT3c0clhhLzRwVitXQXU2V1dwMm5nTnZ2cDloVHdHMU16c3RYMzN6ejlTOGRzM0Y1ZVdrQXdBeWswZkR0dDcvdFdwSzZtei8vK2M4ZHNUUWlmL2poOXgzejRicHRadEtSTFRCOUFtNXlBdTc1TVpKbTVObXpaNDZ5WVVZRTNPUUUzUE5pSk0zSW16ZnQvY0tGT1JOd2t4Tnd6NHVSTkNOT2tXQitQSUdiVFo3QVBTOUcwa3lrWUR0ZHR3SHo0Z25jNUR5QmV6Nk1wSmtRYk1NOENiakpDYmpudzBpYUFjRTJ6TnZWMWF1dU5RTHVPZ0gzUEJoSk15RFlobmxMcHdZQ2JqWUp1T2ZCU0pvQnAwZ3diK25EVU1ETkpnSDNQQmhKRXlmWWhtVm9NZFlWY05jSnVLZlBTSm80d1RZc3c5blptWUNiendpNHA4OUltakRCTml5TGdKdWNnSHZhaktRSkUyekRzZ2k0eVFtNHA4MUltakNuU0xBc0FtNXlBdTVwTTVJbVNyQU55eVRnSmlmZ25pNGphYUlFMjdCTUxRYmNEdytQQXU0S0FmZDBHVWtUSk5pR1pXc3Q0RTREU2NCZEorQ2VKaU5wZ2dUYnNHd3RCdHpyOWJxalRNQTlUVWJTQkRsRmdtVnJNZUIrZUhnUWNGY0l1S2ZKU0pvWXdUYTBvY1ZZMStNQTZnVGMwMk1rVFl4Z0c5cmdDZHprQk56VFl5Uk5pR0FiMmlMZ0ppZmduaFlqYVVJRTI5QVdBVGM1QWZlMEdFa1Q0aFFKMnRKaXJDdmdyaE53VDR1Uk5CR0NiV2lUZ0p1Y2dIczZqS1NKRUd4RG0xcU1kUVhjZFFMdTZUQ1NKa0N3RFcxckxkWVZjUGNUY0UrRGtUUUJnbTFvVzR1eHJvQzdUc0E5RFViU0JEaEZncllKdU1rSnVLZkJTRG94d1RhUUNMakpDYmhQejBnNk1jRTJrQWk0eVFtNFQ4OUlPaUhCTnJCSndFMU93SDFhUnRJSkNiYUJUUUp1Y2dMdTB6S1NUc2dwRXJCSndFMU93SDFhUnRLSkNMYUJTSXV4N25yOWw0NHlBZmZwR0Vrbkl0Z0dJaTNHdXJlM3R3THVDZ0gzNlJoSkp5RFlCbXBhRExodmJ6OTBsTDE0NFRQakZJeWtFeEJzQXpWdEJ0eDNIV1d2WGwwSnVFL0FTRG9CcDBoQVRZdXhibnBta29DN0xQMU12SGpoeXUzWWpLUWpFMndEUXdpNHlhWFRKSTdMU0RveXdUWXdoSUNiWFBxWmVQNzhlY2Z4R0VsSEpOZ0d0aUhnSm5keDRRbmN4MlFrSFpGZ0c5aUdnSnVjZ1B1NGpLUWpjb29FYkVQQVRVN0FmVnhHMHBFSXRvRmRDTGpKQ2JpUHgwZzZFc0Uyc0FzQk56a0I5L0VZU1VjZzJBYjJJZUFtSitBK0RpUHBDQVRid0Q0RTNPUUUzTWRoSkIyQlV5UmdId0p1Y2dMdTR6Q1NEa3l3RFl4QndFMU93SDE0UnRLQnRmaUxEUmlmZ0p1Y2dQdndqS1FEU2lkSTUrZEdFakFPQVRjNUFmZGhHVWtIbEs3YUFNWWk0Q1luNEQ0c0krbUF2di9lU0FMR0krQW1KK0ErTENQcFFGS0xKTmdHeGliZ0ppZmdQaHdqNlVCY3RRR0hJT0FtbDM0bVhMa2RocEYwQUlKdDRKQUUzT1N1cnB3bUhZS1JkQUJPa1lCREVuQ1RjK1YyR0ViU0FRaTJnVU1TY0pOTFB4T3RYY01ldzY4NlJpWFk3dmZ4NDhmdWIzLzdXd2MxWjJkbk9vdUs5THZtM2J0M1hVdFN3TzAxVDJXdlg3Lyt4NUQ4ZmNkNHZ2cm1tNjkvNlJqTm4vNzBmM3FrSHYvOTMvL3phU2hCalgrWCt2M3d3dytmVGxoYWtVYnpUei85MVhpdStQYmIzNHJjUitTNmJVU0M3WDdwdU54QVlvaWJtNXVPT2dFM09RSDN1SXlrRVFtMis2VS81WVVoMGdtSmI4UjFBbTV5QXU1eEdVa2pFbXpYcFJNazN3TFp4czFOVzgzTnRnVGM1QVRjNHpLU1JpTFk3dWNYRzl0cUxVemVoU2R3azBzQk4rTXdra2JpcXEzZjI3YzNIV3dqWGJjWjEzV2V3RTNPRTdqSFl5U05RTERkNytIaG9YdDhmT3hnVzlmWDF4MTFBbTV5QXU1eEdFa2pjSXJVejE4cXNTc0JkejhCTnprQjl6aU1wQkVJdHZ1MTlDd1h4aWZncmhOd2t4TndqOE5JMnRPTEZ5OEUyejFTUCtEWlNPeGp2VjUzMUFtNHlRbTQ5MmNrN2NrcFVqL1BSbUpmcVdsemFsQW40Q1luNE42ZmtiU0hkSUtrUjZwTEowaXUyaGlEVTROK0FtNXlBdTc5R0VsNzhLTEZmcXZWcW9NeE9EWG9KK0FtSitEZWo1RzBoemR2M1BmMmNZckVXSndhOUJOd2t4Tnc3OGRJMnBGZ3U5L2QzVnF3emFpY0d2UVRjSk1UY08vT1NOcVJZTHZmN2UzN0RzYmsxS0JmeWdDZVAzL2V0Y1JWYkoyQWUzZEcwZzRFMi8zU0NaSnZkeHlDbjZ0K0Z4Y0NiajRuNE42TmtiUUR3WFkvMy9ZNUZLY0cvVktzSytCbWs0QjdOMGJTRGdUYi9WWXI3OXZpTU5KQXVyLzNCd0UxYVNDbGJySWxybUxyQk55N01aSzJKTmp1bDM1UkNiWTVKTzhDN05maXlZR3IyRG9COS9hTXBDMEp0dnQ1d2phSDVxVzMvUVRjNUFUYzJ6T1N0aURZN3BkT2tBU1VISU9YM3ZZVGNKTVRjRy9IU05xQ1lMdWZKb0JqZWZmT1NPb2o0Q1luNE42T2tiUUZ3WGEvdDI5dk9qaUdkR3BnbE5jSnVNa0p1TGRqSkEwazJPNlhydG9lSHg4N09KYnJhMzlGMlVmQVRVN0FQWnlSTkpCZ3U1K1gyWEpzRHcrUFF0MGVBbTV5QXU3aGpLUUJCTnZEZUprdHg1WStDQVhjL1ZvTXVEMUxxMDdBUFl5Uk5JQmd1MS82NXViWlNKekMvYjMrcEUrTEFiZG5hZFVKdUljeGtnWVFiUGZ6YkNST1JhamJyOVdBKytIaG9TTW00QjdHU09vaDJPNlhUcEJjdFhGS1F0MStMWjRjM04ydE84b0UzUDJNcEI2QzdYNys3SjlURStyMmF6SGdUcy9TOG5OUkp1RHVaeVJWQ0xhSFdhOTlXK08wUEdsNUdBRTNPUUYzblpGVUlkanVsNDZ6QmR0TWdTY3Q5eE53a3hOdzF4bEpGWUx0Zmo2WW1Bb0JkNzgwa003TzJycHlFM0RYQ2JqcmpLUUN3WFkvTDdObGF2d0JRYjhXWTEwQmQ1MkF1OHhJS2hCczkvT3RuYW54MHR0K0xjYTZBdTQ2QVhlWmtSUVFiQSt6V25sdkZ0UGlwYmZEdEJickNyajdDYmhqUmxKQXNOMHZmUkFKdHBraUw3M3QxMktzSytDdUUzREhqS1NBWUx1Zkoyd3pWYWxMY3JWUzEyS3NLK0N1RTNESGpLU01ZTHRmK2dEeWhHT216RXR2K3dtNHlRbTR2MlFrWlFUYi9kTERJMzFUWjhvRTNQMEUzT1FFM0Y4eWtqWUl0b2R4MWNiVUNiaUhFWENURTNCL3pramFJTmp1NTJXMnpJWFRwSDRDYm5JQzdzOFpTUnNFMi8xV3ExVUhjeURnN3RkcXdPM25va3pBL1RrajZWOEUyOE00UldJdXZQUjJtQlpqWFdGL25ZRDdQNHlrZnhGczkwc3RrbWNqTVNmZUxkaXYxWUNiTWdIM2Z4aEpuV0I3S01FMmMrT2x0OE8wR0hEN3VhZ1RjUCtUa2RRSnRvY1FiRE5YbnVuVnI4VlkxNVBaNnk0dXpqdU1wRThFMi8zZXYzZUt4RHlsRTFDaGJwMkFtOXpaMlptQXV6T1NCTnNEdVdwanJnVGN3d2k0eVYxZHZlcGExL3hJRW16Mzh6SmI1azdBM1UvQVRTNmRKTFVlY0RjOWtnVGJ3emhGWXU2ODNIUVlBVGViMGtCcVBlQnVlaVFKdHZ1bEV5UlhGU3lCbDV2MkUzQ1RhejNnYm5va0NiYjcrWmJGVXJoYTZTZmdKdGQ2d04zc1NCSnNEN05hK1piRk1yaGFHVWJBVGE3bGdMdlprU1RZN2lmWVptbGNyZlFUY0pOck9lQnVjaVFKdG9jUmJMTTBybGFHRVhDenFlV0F1OG1SSk5qdWwzNXBlRkl4UytScXBaK0FtMXlyQVhlVEkwbXczVys5WHZ2R3pTSzVXdWtuNENiWGFzRGQzRWdTYkEvanFvMmxjclV5aklDYlhJc0JkM01qU2JEZHo4dHNXVHFuU2YwRTNPUmFETGliR2tucGY5enpjMjgyN3JOYXJUcFlNbGNyd3dpNDJkUml3TjNVU0VvRHFmWDMwQXpoRkltbDg5TGJZUVRjNUZvTHVKc2FTUzMrQzcrdDFDSjVOaEl0OE5MYmZnSnVjcTBGM00yTXBQUS9iUHFIdXJzN0h4eTBJWDBZdWxycEorQW1kMzUrMGJXaW1aSGtGS2xmT2tIeWJDUmE0dWU5bjRDYjNPWGx5MlorSnBvWVNZTHRZYndsbmRhazYyVlhLLzBFM0d4S242a3ZYNzdzV3RERVNCSnNEM056YzlOQlM5S0hvUzhIL1FUYzVGb0p1SnNZU2E3YStubVpMYTM2OE1HRFUvc0l1TW1sYTlnV2ZpYSsrdWFicjMvcEFBRDRUSlB2YmdNQTZHTWtBUUFFakNRQWdJQ1JCQUFRTUpJQUFBSkdFZ0JBd0VnQ0FBZ1lTUUFBQVNNSkFDQmdKQUVBQkl3a0FJQ0FrUVFBRURDU0FBQUNSaElBUU1CSUFnQUlHRWtBQUFFakNRQWdZQ1FCQUFTTUpBQ0FnSkVFQUJBd2tnQUFBa1lTQUVEQVNBSUFDQmhKQUFBQkl3a0FJR0FrQVFBRWpDUUFnSUNSQkFBUU1KSUFBQUpHRWdCQXdFZ0NBQWdZU1FBQUFTTUpBQ0JnSkFFQUJJd2tBSUNBa1FRQUVEQ1NBQUFDUmhJQVFNQklBZ0FJR0VrQUFBRWpDUUFnWUNRQkFBU01KQUNBZ0pFRUFCQXdrZ0FBQWtZU0FFREFTQUlBQ0JoSkFBQUJJd2tBSUdBa0FRQUVqQ1FBZ0lDUkJBQVFNSklBQUFKR0VnQkF3RWdDQUFnWVNRQUFBU01KQUNCZ0pBRUFCSXdrQUlDQWtRUUFFRENTQUFBQ1JoSUFRTUJJQWdBSUdFa0FBQUVqQ1FBZ1lDUUJBQVNNSkFDQWdKRUVBQkF3a2dBQUFrWVNBRURBU0FJQUNCaEpBQUFCSXdrQUlHQWtBUUFFakNRQWdJQ1JCQUFRTUpJQUFBSkdFZ0JBd0VnQ0FBZ1lTUUFBQVNNSkFDQmdKQUVBQkl3a0FJQ0FrUVFBRURDU0FBQUNSaElBUU1CSUFnQUlHRWtBQUFFakNRQWdZQ1FCQUFTTUpBQ0FnSkVFQUJBd2tnQUFBa1lTQUVEQVNBSUFDQmhKQUFBQkl3a0FJR0FrQVFBRWpDUUFnSUNSQkFBUU1KSUFBQUpHRWdCQXdFZ0NBQWdZU1FBQUFTTUpBQ0JnSkFFQUJJd2tBSUNBa1FRQUVEQ1NBQUFDUmhJQVFNQklBZ0FJR0VrQUFBRWpDUUFnWUNRQkFBU01KQUNBZ0pFRUFCQXdrZ0FBQWtZU0FFREFTQUlBQ0JoSkFBQUJJd2tBSUdBa0FRQUVqQ1FBZ0lDUkJBQVFNSklBQUFKR0VnQkF3RWdDQUFqOFA2TDdqYlNxWkVyMUFBQUFBRWxGVGtTdVFtQ0MiLz4KICAgIDwvZGVmcz4KICA8L3N2Zz4KICA=", + }, + { + value: "google", + label: "Google AI Studio", + icon: "PHN2ZyBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZD0iTTE2IDguMDE2QTguNTIyIDguNTIyIDAgMDA4LjAxNiAxNmgtLjAzMkE4LjUyMSA4LjUyMSAwIDAwMCA4LjAxNnYtLjAzMkE4LjUyMSA4LjUyMSAwIDAwNy45ODQgMGguMDMyQTguNTIyIDguNTIyIDAgMDAxNiA3Ljk4NHYuMDMyeiIgZmlsbD0idXJsKCNwcmVmaXhfX3BhaW50MF9yYWRpYWxfOTgwXzIwMTQ3KSIvPjxkZWZzPjxyYWRpYWxHcmFkaWVudCBpZD0icHJlZml4X19wYWludDBfcmFkaWFsXzk4MF8yMDE0NyIgY3g9IjAiIGN5PSIwIiByPSIxIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxNi4xMzI2IDUuNDU1MyAtNDMuNzAwNDUgMTI5LjIzMjIgMS41ODggNi41MDMpIj48c3RvcCBvZmZzZXQ9Ii4wNjciIHN0b3AtY29sb3I9IiM5MTY4QzAiLz48c3RvcCBvZmZzZXQ9Ii4zNDMiIHN0b3AtY29sb3I9IiM1Njg0RDEiLz48c3RvcCBvZmZzZXQ9Ii42NzIiIHN0b3AtY29sb3I9IiMxQkExRTMiLz48L3JhZGlhbEdyYWRpZW50PjwvZGVmcz48L3N2Zz4=", + }, + { + value: "grok", + label: "xAI Grok", + invertIconDark: true, + icon: "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NjYuMDQgNTE2LjkzIj48cG9seWdvbiBwb2ludHM9IjAuMTIgMTgyLjcxIDIzNC4xNCA1MTYuOTIgMzM4LjE1IDUxNi45MiAxMDQuMTMgMTgyLjcxIDAuMTIgMTgyLjcxIi8+PHBvbHlnb24gcG9pbnRzPSIwIDUxNi45MiAxMDQuMDggNTE2LjkyIDE1Ni4wOCA0NDIuNjcgMTA0LjA0IDM2OC4zNCAwIDUxNi45MiIvPjxwb2x5Z29uIHBvaW50cz0iNDY2LjA0IDAgMzYxLjk2IDAgMTgyLjEgMjU2Ljg2IDIzNC4xNSAzMzEuMTggNDY2LjA0IDAiLz48cG9seWdvbiBwb2ludHM9IjM4MC43OCA1MTYuOTIgNDY2LjA0IDUxNi45MiA0NjYuMDQgMzcuMTYgMzgwLjc4IDE1OC45MiAzODAuNzggNTE2LjkyIi8+PC9zdmc+", + }, + { + value: "workers-ai", + label: "Workers AI", + icon: "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iNjRweCIgaGVpZ2h0PSI2NHB4IiB2aWV3Qm94PSIwIC03MCAyNTYgMjU2IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj4KICAgIDxnPgogICAgICAgIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAuMDAwMDAwLCAtMS4wMDAwMDApIj4KICAgICAgICAgICAgPHBhdGggZD0iTTIwMi4zNTY5LDUwLjM5NCBMMTk3LjA0NTksNDguMjcgQzE3Mi4wODQ5LDEwNC40MzQgNzIuNzg1OSw3MC4yODkgNjYuODEwOSw4Ni45OTcgQzY1LjgxNDksOTguMjgzIDEyMS4wMzc5LDg5LjE0MyAxNjAuNTE2OSw5MS4wNTYgQzE3Mi41NTU5LDkxLjYzOSAxNzguNTkyOSwxMDAuNzI3IDE3My40ODA5LDExNS41NCBMMTgzLjU0OTksMTE1LjU3MSBDMTk1LjE2NDksNzkuMzYyIDIzMi4yMzI5LDk3Ljg0MSAyMzMuNzgxOSw4NS44OTEgQzIzMS4yMzY5LDc4LjAzNCAxOTEuMTgwOSw4NS44OTEgMjAyLjM1NjksNTAuMzk0IFoiIGZpbGw9IiNGRkZGRkYiPgoKPC9wYXRoPgogICAgICAgICAgICA8cGF0aCBkPSJNMTc2LjMzMiwxMDkuMzQ4MyBDMTc3LjkyNSwxMDQuMDM3MyAxNzcuMzk0LDk4LjcyNjMgMTc0LjczOSw5NS41MzkzIEMxNzIuMDgzLDkyLjM1MjMgMTY4LjM2NSw5MC4yMjgzIDE2My41ODUsODkuNjk3MyBMNzEuMTcsODguNjM0MyBDNzAuNjM5LDg4LjYzNDMgNzAuMTA4LDg4LjEwMzMgNjkuNTc3LDg4LjEwMzMgQzY5LjA0Niw4Ny41NzIzIDY5LjA0Niw4Ny4wNDEzIDY5LjU3Nyw4Ni41MTAzIEM3MC4xMDgsODUuNDQ4MyA3MC42MzksODQuOTE2MyA3MS43MDEsODQuOTE2MyBMMTY0LjY0Nyw4My44NTQzIEMxNzUuODAxLDgzLjMyMzMgMTg3LjQ4Niw3NC4yOTQzIDE5MS43MzQsNjMuNjcyMyBMMTk3LjA0Niw0OS44NjMzIEMxOTcuMDQ2LDQ5LjMzMTMgMTk3LjU3Nyw0OC44MDAzIDE5Ny4wNDYsNDguMjY5MyBDMTkxLjIwMywyMS4xODIzIDE2Ni43NzIsMC45OTkzIDEzOC4wOTEsMC45OTkzIEMxMTEuNTM1LDAuOTk5MyA4OC42OTcsMTcuOTk1MyA4MC43Myw0MS44OTYzIEM3NS40MTksMzguMTc4MyA2OS4wNDYsMzYuMDUzMyA2MS42MSwzNi41ODUzIEM0OC44NjMsMzcuNjQ3MyAzOC43NzIsNDguMjY5MyAzNy4xNzgsNjEuMDE2MyBDMzYuNjQ3LDY0LjIwMzMgMzcuMTc4LDY3LjM5MDMgMzcuNzEsNzAuNTc2MyBDMTYuOTk2LDcxLjEwNzMgMCw4OC4xMDMzIDAsMTA5LjM0ODMgQzAsMTExLjQ3MjMgMCwxMTMuMDY2MyAwLjUzMSwxMTUuMTkwMyBDMC41MzEsMTE2LjI1MzMgMS41OTMsMTE2Ljc4NDMgMi4xMjUsMTE2Ljc4NDMgTDE3Mi42MTQsMTE2Ljc4NDMgQzE3My42NzYsMTE2Ljc4NDMgMTc0LjczOSwxMTYuMjUzMyAxNzQuNzM5LDExNS4xOTAzIEwxNzYuMzMyLDEwOS4zNDgzIFoiIGZpbGw9IiNGNDgxMUYiPgoKPC9wYXRoPgogICAgICAgICAgICA8cGF0aCBkPSJNMjA1LjU0MzYsNDkuODYyOCBMMjAyLjg4NzYsNDkuODYyOCBDMjAyLjM1NjYsNDkuODYyOCAyMDEuODI1Niw1MC4zOTM4IDIwMS4yOTQ2LDUwLjkyNDggTDE5Ny41NzY2LDYzLjY3MTggQzE5NS45ODM2LDY4Ljk4MjggMTk2LjUxNDYsNzQuMjk0OCAxOTkuMTcwNiw3Ny40ODA4IEMyMDEuODI1Niw4MC42Njc4IDIwNS41NDM2LDgyLjc5MTggMjEwLjMyMzYsODMuMzIzOCBMMjI5Ljk3NTYsODQuMzg1OCBDMjMwLjUwNjYsODQuMzg1OCAyMzEuMDM3Niw4NC45MTY4IDIzMS41Njg2LDg0LjkxNjggQzIzMi4wOTk2LDg1LjQ0NzggMjMyLjA5OTYsODUuOTc4OCAyMzEuNTY4Niw4Ni41MDk4IEMyMzEuMDM3Niw4Ny41NzI4IDIzMC41MDY2LDg4LjEwMzggMjI5LjQ0MzYsODguMTAzOCBMMjA5LjI2MTYsODkuMTY1OCBDMTk4LjEwNzYsODkuNjk2OCAxODYuNDIzNiw5OC43MjU4IDE4Mi4xNzQ2LDEwOS4zNDc4IEwxODEuMTExNiwxMTQuMTI4OCBDMTgwLjU4MDYsMTE0LjY1OTggMTgxLjExMTYsMTE1LjcyMTggMTgyLjE3NDYsMTE1LjcyMTggTDI1Mi4yODI2LDExNS43MjE4IEMyNTMuMzQ0NiwxMTUuNzIxOCAyNTMuODc1NiwxMTUuMTkwOCAyNTMuODc1NiwxMTQuMTI4OCBDMjU0LjkzNzYsMTA5Ljg3OTggMjU1Ljk5OTYsMTA1LjA5OTggMjU1Ljk5OTYsMTAwLjMxODggQzI1NS45OTk2LDcyLjcwMDggMjMzLjE2MTYsNDkuODYyOCAyMDUuNTQzNiw0OS44NjI4IiBmaWxsPSIjRkFBRDNGIj4KCjwvcGF0aD4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==", + }, + { value: "dynamic", label: "Dynamic Route" }, +]; + +const renderOptionWithIcon = (option: ProviderItem) => { + return { + label: ( +
    + {" "} + {option.label} +
    + ), + value: option.value, + }; +}; + +const keyTypeOptions: { value: KeyType; label: string }[] = [ + { value: "byok", label: "Stored Key (BYOK)" }, + { value: "in-request", label: "Key in Request" }, + { value: "unified", label: "Unified Billing" }, +]; + +const clientTypeOptions: { value: ClientType; label: string }[] = [ + { value: "openai-js", label: "OpenAI JS SDK" }, + { value: "curl", label: "cURL" }, + { value: "aisdk", label: "AI SDK" }, +]; + +const apiTypeOptions: { value: APIType; label: string }[] = [ + { value: "native", label: "Native" }, + { value: "unified", label: "Unified" }, +]; + +const BaseReactSelectStyles = { + indicatorSeparator: () => ({ display: "none" }), + input: (base: any) => ({ ...base, margin: 0, padding: 0 }), + valueContainer: (base: any) => ({ ...base, paddingTop: 0, paddingBottom: 0 }), + control: (base: any) => ({ + ...base, + backgroundColor: "var(--selector-bg-color)", + borderColor: "var(--selector-border-color)", + }), + singleValue: (base: any) => ({ + ...base, + color: "var(--selector-text-color)", + }), + option: (base: any, state: any) => ({ + ...base, + backgroundColor: state.isFocused + ? "var(--sl-color-gray-5)" + : "var(--selector-bg-color)", + color: "var(--selector-text-color)", + "&:active": { + backgroundColor: "var(--selector-bg-color)", + }, + }), +}; + +export function CodeSelector({ + forceClient, + forceApiType, +}: { + forceClient?: string; + forceApiType?: string; +}) { + const { config, updateConfig } = useAIGConfig(); + + const provider = + providerOptions.find((p) => p.value === config.provider) || + providerOptions[0]; + const clientType = + clientTypeOptions.find( + (c) => c.value === (forceClient ?? config.clientType), + ) || clientTypeOptions[0]; + const keyType = + keyTypeOptions.find((k) => k.value === config.keyType) || keyTypeOptions[0]; + const apiType = + apiTypeOptions.find((a) => a.value === (forceApiType ?? config.apiType)) || + apiTypeOptions[0]; + + if (provider.value === "dynamic" || provider.value === "workers-ai") { + forceApiType = "unified"; + } + + return ( +
    +
    +
    + Make a request to{" "} + { + if (selected) { + updateConfig({ provider: selected.value }); + } + }} + styles={{ + menu: (base) => ({ + ...base, + zIndex: 999, + minWidth: "12rem", + backgroundColor: "var(--selector-bg-color)", + }), + ...BaseReactSelectStyles, + }} + placeholder="Select an AI provider" + /> + {!forceApiType && ( + <> + { + if (selected) { + updateConfig({ apiType: selected.value }); + } + }} + styles={{ + menu: (base) => ({ + ...base, + zIndex: 999, + minWidth: "10rem", + backgroundColor: "var(--selector-bg-color)", + }), + ...BaseReactSelectStyles, + }} + placeholder="Select API type" + /> + API{" "} + + )} + {!forceClient && ( + <> + using{" "} + { + if (selected) { + updateConfig({ clientType: selected.value }); + } + }} + styles={{ + menu: (base) => ({ + ...base, + zIndex: 999, + minWidth: "10rem", + backgroundColor: "var(--selector-bg-color)", + }), + ...BaseReactSelectStyles, + }} + placeholder="Select client" + /> +
    + + )} + {!["workers-ai", "dynamic"].includes(config.provider) && ( + <> + with{" "} + { + if (selected) { + updateConfig({ keyType: selected.value }); + } + }} + styles={{ + menu: (base) => ({ + ...base, + zIndex: 999, + minWidth: "12rem", + backgroundColor: "var(--selector-bg-color)", + }), + ...BaseReactSelectStyles, + }} + placeholder="Select key type" + /> + + )} +
    +
    +
    +
    + ); +} + +export function CodeExample({ + provider, + clientType, + keyType, + apiType, + forceApiType, + forceClientType, + children, +}: { + provider: string; + clientType: string; + keyType: string; + apiType: string; + forceApiType?: string; + forceClientType?: string; + children: React.ReactNode; +}) { + const { config } = useAIGConfig(); + if ( + ["dynamic", "workers-ai"].includes(config.provider) && + config.keyType === "in-request" + ) { + config.keyType = "byok"; + } + if (provider === "dynamic" || provider === "workers-ai") { + forceApiType = "unified"; + } + const stored = config.keyType === "byok" || config.keyType === "unified"; + const resolvedClientType = forceClientType ?? config.clientType; + const resolvedApiType = forceApiType ?? config.apiType; + const isHidden = + config.provider !== provider || + resolvedClientType !== clientType || + resolvedApiType !== apiType || + (stored ? keyType !== "stored" : keyType !== "in-request"); + return
    {children}
    ; +} + +export const modelOptions = [ + { + model: "gpt-5.2", + provider: "openai", + aiSDK: { + providerFactory: "createOpenAI", + providerUsage: "openai.chat", + provider: "openai", + }, + }, + { + model: "claude-4-5-sonnet", + provider: "anthropic", + aiSDK: { + providerFactory: "createAnthropic", + providerUsage: "anthropic", + provider: "anthropic", + }, + }, + { + model: "gemini-2.5-pro", + provider: "google", + aiSDK: { + providerFactory: "createGoogle", + providerUsage: "google", + provider: "google", + }, + }, + { + model: "grok-4", + provider: "grok", + aiSDK: { + providerFactory: "createXai", + providerUsage: "xai", + provider: "xai", + }, + }, + { + model: "customer-support", + provider: "dynamic", + aiSDK: { + providerFactory: "createUnified", + providerUsage: "unified", + provider: "unified", + }, + }, + { + model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", + provider: "workers-ai", + aiSDK: { + providerFactory: "createUnified", + providerUsage: "unified", + provider: "unified", + }, + }, +]; +export const code = `import OpenAI from "openai"; + +const client = new OpenAI({ + apiKey: "{cf_api_token}", + {headerauth} + baseURL: + "https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/compat", +}); + +const response = await client.chat.completions.create({ + model: "{provider}/{model}", + messages: [{ role: "user", content: "Hello, world!" }], +});`; + +export const aiSDKUnifiedCode = `import { createAiGateway } from 'ai-gateway-provider'; +import { createUnified } from 'ai-gateway-provider/providers/unified'; +import { generateText } from "ai"; + +const aigateway = createAiGateway({ + accountId: "{CLOUDFLARE_ACCOUNT_ID}", + gateway: '{GATEWAY_NAME}', + apiKey: '{CF_AIG_TOKEN}', +}); + +const unified = createUnified({apikey}); + +const { text } = await generateText({ + model: aigateway(unified('{provider}/{model}')), + prompt: 'What is Cloudflare?', +});`; + +export const aiSDKNativeCode = `import { createAiGateway } from 'ai-gateway-provider'; +import { {providerFactory} } from 'ai-gateway-provider/providers/{provider}'; +import { generateText } from "ai"; + +const aigateway = createAiGateway({ + accountId: "{CLOUDFLARE_ACCOUNT_ID}", + gateway: '{GATEWAY_NAME}', + apiKey: '{CF_AIG_TOKEN}', +}); + +const {provider} = {providerFactory}({apikey}); + +const { text } = await generateText({ + model: aigateway({providerUsage}('{model}')), + prompt: 'What is Cloudflare?', +});`; + +export const curlCode = `curl -X POST https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/compat/chat/completions \\ + --header 'cf-aig-authorization: Bearer {CF_AIG_TOKEN}' \\ +{headerauth} + --header 'Content-Type: application/json' \\ + --data '{ + "model": "{provider}/{model}", + "messages": [ + { + "role": "user", + "content": "What is Cloudflare?" + } + ] + }'`; diff --git a/src/nimbus/components/ai-gateway/code-examples.astro b/src/nimbus/components/ai-gateway/code-examples.astro new file mode 100644 index 00000000000..91687d405b0 --- /dev/null +++ b/src/nimbus/components/ai-gateway/code-examples.astro @@ -0,0 +1,245 @@ +--- +import { + CodeExample, + CodeSelector, + modelOptions, + code, + aiSDKUnifiedCode, + aiSDKNativeCode, + curlCode, +} from "./code-example-selector"; +import { Code } from "~/components"; +import { z } from "astro/zod"; + +const props = z.object({ + forceClient: z.string().optional(), + forceAPI: z.string().optional(), +}); + +const { forceClient, forceAPI } = props.parse(Astro.props); +--- + + +
    + + { + (!forceClient || forceClient === "openai-js") && + modelOptions.map((option) => ( + + + + )) + } + { + (!forceClient || forceClient === "openai-js") && + modelOptions.map((option) => ( + + + + )) + } + { + (!forceClient || forceClient === "aisdk") && + modelOptions.map((option) => ( + + + + )) + } + { + (!forceClient || forceClient === "aisdk") && + modelOptions.map((option) => ( + + + + )) + } + + { + (!forceClient || forceClient === "aisdk") && + modelOptions.map((option) => ( + + + + )) + } + { + (!forceClient || forceClient === "aisdk") && + modelOptions.map((option) => ( + + + + )) + } + + { + (!forceClient || forceClient === "curl") && + modelOptions.map((option) => ( + + + + )) + } + { + (!forceClient || forceClient === "curl") && + modelOptions.map((option) => ( + + + + )) + } +
    diff --git a/src/nimbus/components/cf/APIRequest.astro b/src/nimbus/components/cf/APIRequest.astro new file mode 100644 index 00000000000..3a31408218c --- /dev/null +++ b/src/nimbus/components/cf/APIRequest.astro @@ -0,0 +1,200 @@ +--- +/** + * APIRequest — render a documented API call as a `curl` command, driven by the + * Cloudflare OpenAPI schema. + * + * CF source: cloudflare-docs/src/components/APIRequest.astro + * + * Mapping notes (vs upstream): + * - Schema logic (lookup, param/security/required-body validation, token + * groups) is ported verbatim; all four fail-loud `throw`s preserved. + * - Rendering already goes through CURL + Details upstream; cf-nimbus ships + * both, so no render retarget — just the local imports. + * - The optional `code` prop is typed as `Record` to match + * cf/CURL.astro's contract (upstream typed it via Starlight's Code + * `ComponentProps`; cf-nimbus's CURL is the consumer and uses this shape). + */ +import { z } from "astro/zod"; +import { getProperty } from "dot-prop"; +import { getSchema } from "~/util/api.ts"; +import type { OpenAPIV3 } from "openapi-types"; +import CURL from "./CURL.astro"; +import Details from "./Details.astro"; + +type Props = z.input; + +const Record = z.record(z.string(), z.any()); + +const props = z + .object({ + path: z.string(), + method: z.enum(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"]), + parameters: z.record(z.string(), z.any()).optional(), + json: z.union([Record, z.array(Record)]).optional(), + form: Record.optional(), + code: z.custom>().optional(), + roles: z.union([z.boolean(), z.string()]).default(true), + }) + .strict(); + +let { path, method, parameters, json, form, code, roles } = props.parse( + Astro.props, +); + +if (json && form) { + throw new Error(`[APIRequest] Cannot use both "json" and "form" properties.`); +} + +const schema = await getSchema(); + +const operation = getProperty( + schema, + `paths.${path}.${method.toLowerCase()}`, +) as unknown as + | OpenAPIV3.OperationObject<{ + "x-api-token-group"?: string[]; + }> + | undefined; + +if (!operation) { + throw new Error( + `[APIRequest] Operation ${method} ${path} not found in schema.`, + ); +} + +const url = new URL( + path.startsWith("/") ? path.slice(1) : path, + "https://api.cloudflare.com/client/v4/", +); +const headers: Record = {}; + +const providedParameters = Object.keys(parameters ?? {}); +const endpointParameters = (operation.parameters ?? + []) as OpenAPIV3.ParameterObject[]; + +const extraneousParameters = providedParameters.filter( + (parameter) => + !endpointParameters.some( + (endpointParam) => endpointParam.name === parameter, + ), +); + +if (extraneousParameters.length > 0) { + throw new Error( + `[APIRequest] Provided parameters ${extraneousParameters.join(", ")} not found in ${method} ${path} schema.`, + ); +} + +for (const parameter of endpointParameters) { + const value = parameters?.[parameter.name]; + if (value) { + if (parameter.in === "path") { + const encoded = encodeURIComponent(`{${parameter.name}}`); + url.pathname = url.pathname.replace(encoded, value); + } else if (parameter.in === "query") { + if (Array.isArray(value)) { + for (const v of value) { + url.searchParams.append(parameter.name, v); + } + } else { + url.searchParams.set(parameter.name, value); + } + } + } +} + +const segments = url.pathname.split("/").filter(Boolean); + +for (const segment of segments) { + const decoded = decodeURIComponent(segment); + + if (decoded.startsWith("{") && decoded.endsWith("}")) { + const placeholder = "$" + decoded.slice(1, -1).toUpperCase(); + url.pathname = url.pathname.replace(segment, placeholder); + } +} + +const security = operation.security as + | OpenAPIV3.SecurityRequirementObject[] + | undefined; + +if (security) { + const keys = security.flatMap((requirement) => Object.keys(requirement)); + + if (keys.includes("api_token")) { + headers["Authorization"] = `Bearer $CLOUDFLARE_API_TOKEN`; + } else if (keys.includes("api_key")) { + headers["X-Auth-Email"] = "$CLOUDFLARE_EMAIL"; + headers["X-Auth-Key"] = "$CLOUDFLARE_API_KEY"; + } +} + +const requestBody = operation?.requestBody as + | OpenAPIV3.RequestBodyObject + | undefined; + +const jsonSchema = requestBody?.content?.["application/json"]?.schema as + | OpenAPIV3.SchemaObject + | undefined; + +if (jsonSchema?.required) { + const checkProperties = (obj?: object) => { + const providedProperties = Object.keys(obj ?? {}); + const requiredProperties = jsonSchema.required!; + + const missingProperties = requiredProperties.filter( + (property) => !providedProperties.includes(property), + ); + + if (missingProperties.length > 0) { + throw new Error( + `[APIRequest] Missing the following required properties for ${method} ${path}: ${missingProperties.join(", ")}`, + ); + } + }; + + if (Array.isArray(json)) { + for (const obj of json) { + checkProperties(obj); + } + } else { + checkProperties(json); + } +} + +let tokenGroups = operation["x-api-token-group"]; + +if (typeof roles === "string") { + tokenGroups = tokenGroups?.filter((group) => + group.toLowerCase().includes(roles.toLowerCase()), + ); +} +--- + +{ + tokenGroups && roles && ( +
    + + At least one of the following{" "} + token permissions{" "} + is required: + +
      + {tokenGroups.map((group) => ( +
    • + {group} +
    • + ))} +
    +
    + ) +} + + diff --git a/src/nimbus/components/cf/AgentsPlatformDiagram.astro b/src/nimbus/components/cf/AgentsPlatformDiagram.astro new file mode 100644 index 00000000000..396b889f2e0 --- /dev/null +++ b/src/nimbus/components/cf/AgentsPlatformDiagram.astro @@ -0,0 +1,465 @@ +--- +const channels = [ + { + type: "Chat", + name: "AIChatAgent", + href: "/agents/communication-channels/chat/", + }, + { + type: "Email", + name: "onEmail", + href: "/agents/communication-channels/email/", + }, + { + type: "Voice", + name: "withVoice", + href: "/agents/communication-channels/voice/", + }, + { + type: "Slack", + name: "Events", + href: "/agents/communication-channels/slack/", + }, + { + type: "Webhook", + name: "HTTP", + href: "/agents/communication-channels/webhooks/", + }, +]; + +const runtime = [ + { label: "State", href: "/agents/runtime/lifecycle/state/" }, + { label: "Sessions", href: "/agents/runtime/lifecycle/sessions/" }, + { label: "Routing", href: "/agents/runtime/communication/routing/" }, + { label: "WebSockets", href: "/agents/runtime/communication/websockets/" }, + { label: "Scheduling", href: "/agents/runtime/execution/schedule-tasks/" }, + { label: "Fibers", href: "/agents/runtime/execution/durable-execution/" }, +]; + +const tools = [ + { type: "Sandbox", name: "Bash/Shell", href: "/agents/tools/sandbox/" }, + { type: "MCP", name: "Servers", href: "/agents/tools/mcp/" }, + { type: "Browser", name: "CDP", href: "/agents/tools/browser/" }, + { type: "AI Search", name: "Retrieval", href: "/agents/tools/ai-search/" }, + { type: "Payments", name: "x402 · MPP", href: "/agents/tools/payments/" }, +]; +--- + +
    +
    + + +
    +
    +
    Channels
    + {channels.length} +
    +
    + { + channels.map((item) => ( + + {item.type} + + )) + } +
    +
    + +
    +
    +
    + +
    Agent
    + +
    + +
    +
    +
    Agent harness
    +

    Controls planning, tool use, and response flow.

    +
    + +
    + +
    +
    +
    Agents SDK runtime
    +

    + Durable identity, state, connections, scheduling, and recovery. +

    +
    + Agent class +
    + {runtime.map((item) => {item.label})} +
    +
    +
    +
    + +
    +
    +
    Tools
    + {tools.length} +
    +
    + { + tools.map((item) => ( + + {item.type} + + )) + } +
    +
    + + + Observability + Logs · metrics · traces + +
    +
    + + diff --git a/src/nimbus/components/cf/AnchorHeading.astro b/src/nimbus/components/cf/AnchorHeading.astro new file mode 100644 index 00000000000..15fec827966 --- /dev/null +++ b/src/nimbus/components/cf/AnchorHeading.astro @@ -0,0 +1,42 @@ +--- +/** + * AnchorHeading — a heading (h1–h6) with a slugified id and anchor link. + * + * CF source: cloudflare-docs/src/components/AnchorHeading.astro + * + * Mapping notes: + * - Upstream runs the heading through the rehype autolink-headings plugin + * (`~/util/rehype`). That pipeline isn't wired here, so the id + anchor + * link are produced directly. Slugging mirrors github-slugger closely + * (lowercase, strip punctuation, spaces→hyphens). + */ +import { z } from "astro/zod"; +import { marked } from "marked"; + +type Props = z.infer; + +const props = z.object({ + title: z.string(), + slug: z.string().optional(), + depth: z.number().min(1).max(6), +}); + +const { title, slug, depth } = props.parse(Astro.props); + +function slugify(input: string): string { + return input + .trim() + .toLowerCase() + .replace(/[^\w\s-]/g, "") + .replace(/\s+/g, "-") + .replace(/-+/g, "-"); +} + +const slugified = slugify(slug ?? title); +const Tag = `h${depth}` as "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; +const inner = marked.parseInline(title) as string; +--- + + + + diff --git a/src/nimbus/components/cf/AnimatedWorkflowDiagram.astro b/src/nimbus/components/cf/AnimatedWorkflowDiagram.astro new file mode 100644 index 00000000000..7310f0c37bc --- /dev/null +++ b/src/nimbus/components/cf/AnimatedWorkflowDiagram.astro @@ -0,0 +1,361 @@ +--- +interface Props { + steps: { + id: string; + label: string; + description?: string; + duration: string; + }[]; + autoPlay?: boolean; + loop?: boolean; +} + +const { steps, autoPlay = true, loop = true } = Astro.props; +--- + + +
    +
    + + + + diff --git a/src/nimbus/components/cf/AutoconfigDiagram.astro b/src/nimbus/components/cf/AutoconfigDiagram.astro new file mode 100644 index 00000000000..bc843cdd921 --- /dev/null +++ b/src/nimbus/components/cf/AutoconfigDiagram.astro @@ -0,0 +1,560 @@ +--- +interface ConfigItem { + label: string; + file: string; + value: string; +} + +interface Props { + framework: string; + configs: ConfigItem[]; +} + +const { framework, configs } = Astro.props; +--- + + +
    +
    + +
    +
    +
    + + + + + + + +
    +
    + {framework} + Detected +
    +
    +
    + + +
    + + +
    +
    + Generated configuration +
    + { + configs.map((config, i) => ( +
    +
    {config.file}
    +
    + {config.label}: + {config.value} +
    +
    + )) + } +
    +
    +
    + + +
    + + +
    +
    +
    + + + + +
    +
    + Workers + Deployed +
    +
    +
    +
    + + +
    +
    + Wrangler handles configuration automatically +
    +
    +
    + + + + diff --git a/src/nimbus/components/cf/AvailableNotifications.astro b/src/nimbus/components/cf/AvailableNotifications.astro new file mode 100644 index 00000000000..56a5f43ce90 --- /dev/null +++ b/src/nimbus/components/cf/AvailableNotifications.astro @@ -0,0 +1,93 @@ +--- +/** + * AvailableNotifications — list notification types, grouped by product. + * + * CF source: cloudflare-docs/src/components/AvailableNotifications.astro + * + * Mapping notes: + * - Reads the `notifications` collection via getEntry; markup ported verbatim. + * - `Object.groupBy` replaced with a manual reduce for Node-version safety. + */ +import { getEntry } from "astro:content"; +import { z } from "astro/zod"; +import { marked } from "marked"; +import AnchorHeading from "./AnchorHeading.astro"; +import Details from "./Details.astro"; + +type Props = z.infer; + +const props = z + .object({ + product: z.string().optional(), + notificationFilter: z.string().optional(), + }) + .strict(); + +const { product, notificationFilter } = props.parse(Astro.props); + +const entry = await getEntry("notifications", "index"); + +if (!entry) { + throw new Error(`[AvailableNotifications] Unable to fetch notifications`); +} + +let notifications = entry.data.entries as any[]; + +if (product) { + notifications = notifications.filter( + (x) => x.associatedProducts.toLowerCase() === product.toLowerCase(), + ); +} + +if (notificationFilter) { + notifications = notifications.filter( + (x) => x.name.toLowerCase() === notificationFilter.toLowerCase(), + ); +} + +const grouped = notifications.reduce>((acc, entry) => { + (acc[entry.associatedProducts] ??= []).push(entry); + return acc; +}, {}); + +const showProductHeadings = !product && !notificationFilter; +--- + +{ + Object.entries(grouped) + .sort() + .map(([product, entries]) => ( + <> + {showProductHeadings && } + {(entries ?? []).map((notification) => ( +
    + Who is it for? +

    + + Other options / filters +

    + + Included with +

    + + What should you do if you receive one? +

    + + {notification.additional_information && ( + <> + Additional information +

    + + )} + + {notification.limitations && ( + <> + Limitations +

    + + )} +

    + ))} + + )) +} diff --git a/src/nimbus/components/cf/CURL.astro b/src/nimbus/components/cf/CURL.astro new file mode 100644 index 00000000000..d31e67bb866 --- /dev/null +++ b/src/nimbus/components/cf/CURL.astro @@ -0,0 +1,83 @@ +--- +/** + * CURL — render a formatted `curl` command from a request description. + * + * CF source: cloudflare-docs/src/components/CURL.astro + * + * Mapping notes: + * - Upstream renders through Starlight's . Nimbus uses ui/code/Code + * (Shiki via astro:components), which takes highlight metadata through a + * `meta` string rather than discrete props. A `title` passed via `code` + * is translated to `meta='title="..."'`; other passthrough keys are kept. + */ +import { z } from "astro/zod"; +import { Code } from "../ui/code"; + +type Props = z.input; + +const Record = z.record(z.string(), z.any()); + +const props = z.object({ + method: z + .enum(["GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"]) + .default("GET"), + url: z.string().url(), + headers: z.record(z.string(), z.string()).default({}), + json: z.union([Record, z.array(Record)]).optional(), + form: Record.optional(), + query: z + .record(z.string(), z.union([z.string(), z.array(z.string())])) + .optional(), + code: z.custom>().optional(), +}); + +const { method, url: baseUrl, headers, json, form, query, code } = props.parse( + Astro.props, +); + +if (json && form) { + throw new Error("[CURL] Cannot use both 'json' and 'form' properties."); +} + +const url = new URL(baseUrl); +if (query) { + for (const [key, value] of Object.entries(query)) { + if (Array.isArray(value)) { + for (const v of value) { + url.searchParams.append(key, v); + } + } else { + url.searchParams.set(key, value); + } + } +} + +const lines = [`curl "${url.toString()}"`, `\t--request ${method}`]; + +if (headers) { + for (const [key, value] of Object.entries(headers)) { + lines.push(`\t--header "${key}: ${value}"`); + } +} + +if (json) { + const jsonLines = JSON.stringify(json, null, "\t\t").split("\n"); + jsonLines[jsonLines.length - 1] = "\t" + jsonLines[jsonLines.length - 1]; + + lines.push(`\t--json '${jsonLines.join("\n").replaceAll("'", "'\\''")}'`); +} + +if (form) { + const formLines = Object.entries(form).map( + ([key, value]) => + `\t--form "${key}=${value.toString().replaceAll('"', '\\"')}"`, + ); + lines.push(...formLines); +} + +// Translate Starlight Code props (title) into a Shiki `meta` string. +const { title, ...restCode } = (code ?? {}) as Record; +const meta = title ? `title="${title}"` : undefined; +--- + + diff --git a/src/nimbus/components/cf/CompatibilityFlags.astro b/src/nimbus/components/cf/CompatibilityFlags.astro new file mode 100644 index 00000000000..7703f1d9ea1 --- /dev/null +++ b/src/nimbus/components/cf/CompatibilityFlags.astro @@ -0,0 +1,71 @@ +--- +// CF source: src/components/CompatibilityFlags.astro (verbatim). +import { z } from "astro/zod"; +import { getCollection, render } from "astro:content"; +import AnchorHeading from "./AnchorHeading.astro"; +// Forward the MDX globals so admonitions (`:::note` → `
    + + {/* Facets — visible on every breakpoint (matches the directory rail); + on mobile the rail stacks above results. */} +
    + {Object.entries(facets).map(([filterField, values]) => ( +
    + + {FACET_LABELS[filterField] ?? + filterField + .replace(/_/g, " ") + .replace(/\b\w/g, (l) => l.toUpperCase())} + +
    + {values + .slice() + .sort((a, b) => + a.localeCompare(b, undefined, { sensitivity: "base" }), + ) + .map((value) => { + const checked = + leftFilters.selectedValues[filterField]?.includes( + value, + ) || false; + return ( + + ); + })} +
    +
    + ))} + + {hasActiveFilters && ( + + )} +
    + + )} + + {filterPlacement === "top" && filters && ( +
    + +
    + )} + +
    + {filterPlacement === "left" && visibleResources.length === 0 ? ( +
    +

    + No resources found +

    +

    + Try a different search term, or broaden your search by removing + filters. +

    + {hasActiveFilters && ( + + )} +
    + ) : ( + /* One row per item, rendered as the /directory blueprint grid: + the wrapper draws the top + left edges, each cell closes its + own right + bottom edge, and corner marks sit on the line + intersections. No gap — cells share single-width lines. */ + +
    + ); +} diff --git a/src/nimbus/components/cf/RuleID.astro b/src/nimbus/components/cf/RuleID.astro new file mode 100644 index 00000000000..dff8cc03fb7 --- /dev/null +++ b/src/nimbus/components/cf/RuleID.astro @@ -0,0 +1,49 @@ +--- +/** + * RuleID — copy-to-clipboard pill showing the last 8 chars of a rule ID. + * + * CF source: cloudflare-docs/src/components/RuleID.astro + * + * Mapping notes: + * - Upstream uses `~/util/tippy` for the "Copied!" tooltip; replaced with a + * lightweight title-attribute swap to avoid the tippy dependency. + * - CF Starlight CSS vars swapped for Nimbus design tokens. + */ +import { z } from "astro/zod"; + +type Props = z.infer; + +const props = z + .object({ + id: z.string(), + }) + .strict(); + +const { id } = props.parse(Astro.props); +--- + + + + + + diff --git a/src/nimbus/components/cf/Stream.astro b/src/nimbus/components/cf/Stream.astro new file mode 100644 index 00000000000..c9da661bb88 --- /dev/null +++ b/src/nimbus/components/cf/Stream.astro @@ -0,0 +1,199 @@ +--- +/** + * Stream — Cloudflare Stream video player with optional chapters. + * + * CF source: cloudflare-docs/src/components/Stream.astro + * + * Mapping notes: + * - Supports both the inline (`id`/`title`) and `file` (stream collection) + * variants, like upstream. + * - `parse-duration` replaced with an inline parser (handles `mm:ss` / + * `hh:mm:ss` and unit forms like `1m30s`). + * - `~/util/zaraz` analytics tracking dropped; the chapter-seek interaction + * is preserved. + * - Starlight → ui/badge. + */ +import { z } from "astro/zod"; +import { Badge } from "../ui/badge"; +import Details from "./Details.astro"; +import { reference, getEntry } from "astro:content"; + +type Props = z.input; + +const props = z + .object({ + id: z.string(), + title: z.string(), + thumbnail: z.string().optional(), + chapters: z.record(z.string(), z.string()).optional(), + showMoreVideos: z.boolean().default(false), + expandChapters: z.boolean().default(false), + }) + .strict() + .or( + z + .object({ + file: reference("stream"), + showMoreVideos: z.boolean().default(false), + expandChapters: z.boolean().default(false), + }) + .strict(), + ); + +const input = props.parse(Astro.props); + +let id; +let title; +let thumbnail; +let chapters; +const { showMoreVideos, expandChapters } = input; + +if ("id" in input) { + id = input.id; + title = input.title; + thumbnail = input.thumbnail; + chapters = input.chapters; +} else { + const entry = await getEntry(input.file); + + if (!entry) { + throw new Error(`[Stream] Could not find "${input.file.id}"`); + } + + id = entry.data.id; + title = entry.data.title; + chapters = entry.data.chapters; + + if (entry.data.thumbnail) { + if ("url" in entry.data.thumbnail) { + thumbnail = entry.data.thumbnail.url; + } else { + thumbnail = entry.data.thumbnail.timestamp; + } + } +} + +// Inline replacement for parse-duration: returns whole seconds. +function parseSeconds(time: string): number { + if (time.includes(":")) { + const parts = time.split(":").map(Number); + return parts.reduce((acc, n) => acc * 60 + (Number.isFinite(n) ? n : 0), 0); + } + let total = 0; + const re = /(\d+(?:\.\d+)?)\s*(h|m|s)/g; + let m: RegExpExecArray | null; + while ((m = re.exec(time))) { + const n = parseFloat(m[1]); + total += m[2] === "h" ? n * 3600 : m[2] === "m" ? n * 60 : n; + } + return Math.round(total || Number(time) || 0); +} + +const BASE_URL = `https://customer-1mwganm1ma0xgnmj.cloudflarestream.com/`; + +const url = new URL(`${id}/iframe`, BASE_URL); +const thumbnailUrl = new URL(`${id}/thumbnails/thumbnail.jpg`, BASE_URL); + +url.searchParams.set("preload", "true"); +url.searchParams.set("letterboxColor", "transparent"); + +if (thumbnail) { + if (thumbnail.startsWith("http")) { + url.searchParams.set("poster", thumbnail); + } else { + thumbnailUrl.searchParams.set("fit", "crop"); + thumbnailUrl.searchParams.set("time", thumbnail); + url.searchParams.set("poster", encodeURI(thumbnailUrl.toString())); + } +} +--- + +
    + +
    + +
    + + { + chapters && ( +
    +

    +

      + {Object.entries(chapters).map(([chapter, time]) => { + const totalSeconds = parseSeconds(time); + + const thumb = new URL(thumbnailUrl); + thumb.searchParams.set("fit", "crop"); + thumb.searchParams.set("time", `${totalSeconds}s`); + + return ( +
    • + +
    • + ); + })} +
    +

    +
    + ) + } + + { + showMoreVideos && ( + + Watch more videos on our Developer Channel + + ) + } +
    +
    + + + + diff --git a/src/nimbus/components/cf/TunnelCalculator.astro b/src/nimbus/components/cf/TunnelCalculator.astro new file mode 100644 index 00000000000..7d1f9031c14 --- /dev/null +++ b/src/nimbus/components/cf/TunnelCalculator.astro @@ -0,0 +1,211 @@ +--- +import { Card } from "../ui/card"; + +const configuration = Object.entries({ + available_ports_per_host: { + title: "Available ports per host", + default: 50000, + }, + cloudflared_replicas: { + title: "Number of cloudflared replicas", + default: 2, + }, + dns_udp_timeout_in_sec: { + title: "DNS UDP session timeout (in seconds)", + default: 5, + readonly: true, + }, + avg_non_dns_udp_session_timeout: { + title: "Average non-DNS UDP session timeout (seconds)", + default: 60, + readonly: true, + }, +}); + +const metrics = Object.entries({ + tcp_per_sec: { + title: "TCP requests per second", + default: 50000, + }, + non_dns_udp_req_per_sec: { + title: "Non-DNS UDP requests per second", + default: 0, + }, + private_dns_req_per_sec: { + title: "Private DNS requests per second", + default: 0, + }, +}); +--- + + + { + configuration.map(([key, value]) => ( +
    + +
    + +
    + )) + } +
    + + + { + metrics.map(([key, value]) => ( +
    + +
    + +
    + )) + } +
    + + +
    + +
    + +
    +
    + +
    + +
    +
    + +
    + +
    +
    + + This calculator is for informational purposes only and all results are + estimates. + + + diff --git a/src/nimbus/components/cf/Type.astro b/src/nimbus/components/cf/Type.astro new file mode 100644 index 00000000000..91ffce4eb3c --- /dev/null +++ b/src/nimbus/components/cf/Type.astro @@ -0,0 +1,21 @@ +--- +/** + * Type — inline type-annotation badge for API reference prose + * (e.g. `Promise` next to a method signature). + * + * CF source: cloudflare-docs/src/components/Type.astro + * Mapping notes: upstream styles with `--sl-color-gray-6` / table-fill + * vars; swapped for Nimbus muted tokens. + */ +interface Props { + text: string; +} + +const { text } = Astro.props; +--- + + + {text} + diff --git a/src/nimbus/components/cf/TypeScriptExample.astro b/src/nimbus/components/cf/TypeScriptExample.astro new file mode 100644 index 00000000000..e139061c91e --- /dev/null +++ b/src/nimbus/components/cf/TypeScriptExample.astro @@ -0,0 +1,75 @@ +--- +/** + * TypeScriptExample — author one ```ts block, render synced JS + TS tabs. + * The JS variant is generated at build time, so the MDX stays single-source. + * + * CF source: cloudflare-docs/src/components/TypeScriptExample.astro + * + * Mapping notes: + * - Upstream scrapes the raw code from Expressive Code's copy-button + * `data-code` attribute. Nimbus renders fenced blocks with Shiki and + * attaches copy buttons client-side, so the raw source is recovered + * from the rendered `
    ` text instead.
    + *   - With a `filename`, both tabs render through `` so the title
    + *     bar matches upstream; without one, the TS tab re-uses the
    + *     already-rendered slot HTML untouched.
    + *   - `playground` (CF's Expressive Code plugin meta) has no Nimbus
    + *     equivalent and is accepted-but-ignored.
    + */
    +import { parse } from "node-html-parser";
    +import { format } from "prettier";
    +import tsBlankSpace from "ts-blank-space";
    +import { Code } from "../ui/code";
    +import { TabItem, Tabs } from "../ui/tabs";
    +
    +interface Props {
    +  filename?: string;
    +  omitTabs?: boolean;
    +  playground?: boolean;
    +}
    +
    +const { filename, omitTabs = false } = Astro.props;
    +
    +const slot = await Astro.slots.render("default");
    +// `blockTextElements: {}` — by default node-html-parser keeps 
     content
    +// as an unparsed raw-text blob, which makes `pre code` unmatchable.
    +const codeEl = parse(slot, { blockTextElements: {} }).querySelector("pre code");
    +
    +if (!codeEl) {
    +  throw new Error(
    +    `[TypeScriptExample] Expected a fenced \`\`\`ts code block as the only child (on "${Astro.url.pathname}").`,
    +  );
    +}
    +
    +// `.text` is node-html-parser's entity-decoded text content; Shiki keeps
    +// the newlines between line spans, so this round-trips the original source.
    +let raw = codeEl.text;
    +
    +// Upstream convention: `compatibilityDate: "$today"` becomes the build date.
    +raw = raw.replaceAll(
    +  /(?<=compatibilityDate:\s*)"\$today"/gi,
    +  `"${new Date().toISOString().split("T")[0]}"`,
    +);
    +
    +const js = await format(tsBlankSpace(raw), { parser: "babel", useTabs: true });
    +
    +const Wrapper = omitTabs ? Fragment : Tabs;
    +const wrapperProps = omitTabs ? {} : { syncKey: "workersExamples" };
    +---
    +
    +
    + + + + + + { + filename ? ( + + ) : ( + + ) + } + + +
    diff --git a/src/nimbus/components/cf/UsageList.astro b/src/nimbus/components/cf/UsageList.astro new file mode 100644 index 00000000000..67a44dd4818 --- /dev/null +++ b/src/nimbus/components/cf/UsageList.astro @@ -0,0 +1,91 @@ +--- +import { z } from "astro/zod"; +import { slug } from "github-slugger"; + +const props = z.object({ + usage: z.object({ + count: z.number(), + pages: z.set(z.string()), + }), +}); + +const { usage } = props.parse(Astro.props); +--- + +<> +

    + Used {usage.count} times. +

    +

    + Pages +

    +
      + { + [...usage.pages] + .filter((path) => path.startsWith("src/content/docs/")) + .sort() + .map((path) => { + const slugified = + "/" + + path + .replace("src/content/docs/", "") + .replace("/index.mdx", "") + .replace(".mdx", "") + .split("/") + .map((segment) => { + if (segment === "1.1.1.1") { + return segment; + } + return slug(segment); + }) + .join("/") + + "/"; + + return ( +
    • + + {slugified} + + + - + + Source + + +
    • + ); + }) + } +
    +

    + Partials +

    +
      + { + [...usage.pages] + .filter((path) => path.startsWith("src/content/partials/")) + .sort() + .map((path) => { + return ( +
    • + + {path} + +
    • + ); + }) + } +
    + diff --git a/src/nimbus/components/cf/WARPRelease.astro b/src/nimbus/components/cf/WARPRelease.astro new file mode 100644 index 00000000000..4a55b393587 --- /dev/null +++ b/src/nimbus/components/cf/WARPRelease.astro @@ -0,0 +1,132 @@ +--- +// CF source: src/components/WARPRelease.astro (verbatim). +import Details from "./Details.astro"; +import { marked } from "marked"; +import { z } from "astro/zod"; +import prettyBytes from "pretty-bytes"; +import { warpReleasesSchema } from "~/schemas"; +import platforms from "~/util/warp-platforms.json"; + +type Props = z.infer; + +const props = z.object({ + header: z.string(), + open: z.boolean().optional(), + release: warpReleasesSchema, +}); + +const { header, open, release } = props.parse(Astro.props); + +const getPrettyLinuxName = (platform: string) => { + const platformInfo = platforms.find((p) => p.platform === platform); + + if (platformInfo) { + return platformInfo.display_name; + } + + return platform; +}; + +const sortedPlatforms = Object.entries(release.linuxPlatforms ?? {}).sort( + (a, b) => { + return getPrettyLinuxName(a[0]).localeCompare(getPrettyLinuxName(b[0])); + }, +); +--- + +
    + +

    +

    + + Version: + {release.platformName} + {release.version} + + + Date: + {release.releaseDate.toISOString().split("T")[0]} + + { + release.packageSize && ( + + Size: + {prettyBytes(release.packageSize)} + + ) + } +
    +

    +

    + + { + sortedPlatforms.length > 0 ? ( + <> + + + Download + + + ) : ( + Download + ) + } + +

    +

    + +

    Release notes

    + + +

    +
    +
    + + diff --git a/src/nimbus/components/cf/WARPReleases.astro b/src/nimbus/components/cf/WARPReleases.astro new file mode 100644 index 00000000000..cff9bffc340 --- /dev/null +++ b/src/nimbus/components/cf/WARPReleases.astro @@ -0,0 +1,66 @@ +--- +// CF source: src/components/WARPReleases.astro (verbatim; upstream's +// backtick-attribute `header=`...`` written as the `header={`...`}` form). +import WARPRelease from "./WARPRelease.astro"; +import Details from "./Details.astro"; +import { getCollection } from "astro:content"; +import { z } from "astro/zod"; +import { sub } from "date-fns"; + +type Props = z.infer; + +const props = z.object({ + track: z.enum([ + "windows/ga", + "windows/beta", + "macos/ga", + "macos/beta", + "linux/ga", + ]), +}); + +const { track } = props.parse(Astro.props); + +const sortByDate = (a: any, b: any) => + b.releaseDate.getTime() - a.releaseDate.getTime(); + +const entries = await getCollection("warp-releases", (release) => { + if (!release.id.startsWith(track)) return false; + + const oneYearAgo = sub(new Date(), { + years: 1, + }); + + if (release.data.releaseDate.getTime() < oneYearAgo.getTime()) return false; + + return true; +}); + +const releases = entries.map((x) => x.data); + +releases.sort(sortByDate); + +const latestRelease = releases.at(0); + +if (!latestRelease) { + throw new Error(); +} + +const platform = latestRelease.platformName; +--- + + + +
    + { + releases + .slice(1) + .sort(sortByDate) + .map((release) => ( + + )) + } +
    diff --git a/src/nimbus/components/cf/Width.astro b/src/nimbus/components/cf/Width.astro new file mode 100644 index 00000000000..a87bbcda0d5 --- /dev/null +++ b/src/nimbus/components/cf/Width.astro @@ -0,0 +1,27 @@ +--- +/** + * Width — constrain slotted content to a fractional width, optionally centered. + * + * CF source: cloudflare-docs/src/components/Width.astro (ported verbatim). + */ +import { z } from "astro/zod"; + +type Props = z.infer; + +const props = z.object({ + size: z.enum(["large", "medium", "small"]), + center: z.boolean().default(false), +}); + +const { size, center } = props.parse(Astro.props); + +const widthClasses = { + large: "w-3/4", + medium: "w-1/2", + small: "w-1/4", +}; +--- + +
    + +
    diff --git a/src/nimbus/components/cf/WorkersArchitectureDiagram.astro b/src/nimbus/components/cf/WorkersArchitectureDiagram.astro new file mode 100644 index 00000000000..f86a3909840 --- /dev/null +++ b/src/nimbus/components/cf/WorkersArchitectureDiagram.astro @@ -0,0 +1,1064 @@ + + + + + + + + + + + + + + + + + + +
    +
    + Scheduling and routing +
    +
    +
    + Scheduling and routing +
    + + + + + +
    +
    + HTTP client +
    +
    +
    + HTTP client +
    + + + + + +
    +
    + HTTP server +
    +
    +
    + HTTP server +
    + + + + + + + +
    +
    + Inbound +
    + HTTP proxy +
    +
    +
    +
    + [Not supported by viewer] +
    + + + + + +
    +
    + Outbound +
    + HTTP proxy +
    +
    +
    +
    + [Not supported by viewer] +
    + + + + + + + +
    +
    + Supervisor +
    +
    +
    +
    + [Not supported by viewer] +
    + + +
    +
    + Main Runtime Process +
    +
    +
    + Main Runtime Process +
    + + +
    +
    + Outer Sandbox +
    +
    +
    + Outer Sandbox +
    + + + + +
    +
    + Disk +
    +
    +
    + Disk +
    + + + +
    +
    + Control plane +
    +
    +
    +
    + [Not supported by viewer] +
    + + + + +
    +
    +
    + HTTP +
    +
    +
    +
    + [Not supported by viewer] +
    + + + + +
    +
    +
    + Cap'n Proto RPC +
    +
    +
    +
    + [Not supported by viewer] +
    + + + + +
    +
    +
    + In-process calls +
    +
    +
    +
    + [Not supported by viewer] +
    + + + + +
    +
    +
    + Other +
    +
    +
    +
    + [Not supported by viewer] +
    + + + +
    +
    +
    + V8 Isolate +
    +
    +
    +
    + [Not supported by viewer] +
    + + + +
    +
    +
    + V8 Isolate +
    +
    +
    +
    + [Not supported by viewer] +
    + + + +
    +
    +
    + V8 Isolate +
    +
    +
    +
    + [Not supported by viewer] +
    + + + +
    +
    +
    + V8 Isolate +
    +
    +
    +
    + [Not supported by viewer] +
    + + + +
    +
    + Process +
    + Sandbox +
    +
    +
    +
    + [Not supported by viewer] +
    + + + +
    +
    +
    + V8 Isolate +
    +
    +
    +
    + [Not supported by viewer] +
    + + + + + +
    +
    + Scheduling and routing +
    +
    +
    + Scheduling and routing +
    + + + +
    +
    + Process +
    + Sandbox +
    +
    +
    +
    + [Not supported by viewer] +
    + + + +
    +
    +
    + V8 Isolate +
    +
    +
    +
    + [Not supported by viewer] +
    + + + + + +
    +
    + Scheduling and routing +
    +
    +
    + Scheduling and routing +
    + + + + +
    diff --git a/src/nimbus/components/cf/WorkersIsolateDiagram.astro b/src/nimbus/components/cf/WorkersIsolateDiagram.astro new file mode 100644 index 00000000000..7a143ff40f3 --- /dev/null +++ b/src/nimbus/components/cf/WorkersIsolateDiagram.astro @@ -0,0 +1,310 @@ +--- +let range = (n: number) => [...Array(n).keys()]; +--- + +
    +
    +
    + { + range(9).map(() => ( +
    +
    +
    +
    + )) + } +
    +
    Traditional architecture
    +
    +
    +
    + { + range(8).map(() => ( +
    + {range(9).map(() => ( +
    + ))} +
    +
    + )) + } +
    +
    +
    +
    +
    +
    Workers V8 isolates
    +
    +
    +
    +
    +
    +
    User code
    +
    +
    +
    +
    +
    +
    Process overhead
    +
    +
    +
    +
    + + diff --git a/src/nimbus/components/cf/WorkersTemplates.astro b/src/nimbus/components/cf/WorkersTemplates.astro new file mode 100644 index 00000000000..da15d515159 --- /dev/null +++ b/src/nimbus/components/cf/WorkersTemplates.astro @@ -0,0 +1,72 @@ +--- +import { AnchorHeading, PackageManagers } from "~/components"; +import { fetchWithToken } from "~/util/github"; + +const REPO = "cloudflare/templates"; + +const latestCommit = await fetchWithToken( + `https://api.github.com/repos/${REPO}/commits?sha=main&per_page=1`, +) + .then((r) => r.json()) + .then((r) => r[0].sha); + +const contents = await fetchWithToken( + `https://api.github.com/repos/${REPO}/contents/?ref=${latestCommit}`, +).then((r) => r.json()); + +const dirs = contents.filter((ent: any) => ent.type === "dir"); +--- + +{ + dirs + .filter((dir: any) => dir.name !== ".github") + .map(async (dir: any) => { + const packageJson = await fetch( + `https://gh-code.developers.cloudflare.com/${REPO}/${latestCommit}/${dir.path}/package.json`, + ) + .then((r) => r.json()) + .catch((reason) => { + console.warn( + `[WorkersTemplates] Failed to parse JSON for ${dir.path}`, + reason, + ); + }); + + if (!packageJson) return; + + return ( + <> +
    +
    + + + Deploy to Cloudflare + +
    +

    {packageJson.description}

    +

    + Explore on{" "} + + GitHub ↗ + +

    + +
    +
    +
    +
    + + ); + }) +} diff --git a/src/nimbus/components/cf/WorkersVPCEgressDiagram.astro b/src/nimbus/components/cf/WorkersVPCEgressDiagram.astro new file mode 100644 index 00000000000..da07f86dc75 --- /dev/null +++ b/src/nimbus/components/cf/WorkersVPCEgressDiagram.astro @@ -0,0 +1,406 @@ +--- +import { Icon as AstroIcon } from "astro-icon/components"; + +const policies = [ + { label: "DNS", href: "/cloudflare-one/traffic-policies/dns-policies/" }, + { label: "HTTP", href: "/cloudflare-one/traffic-policies/http-policies/" }, + { + label: "Network", + href: "/cloudflare-one/traffic-policies/network-policies/", + }, +]; + +const logsHref = "/cloudflare-one/insights/logs/dashboard-logs/gateway-logs/"; +--- + +
    +
      +
    1. +
      +
      + + Worker +
      +

      Calls env.EGRESS.fetch()

      +
      +
    2. + +
    3. + VPC binding + +
    4. + +
    5. +
      + +

      + Bind via cf1:network +

      +
      +
    6. + + + +
    7. +
      + +

      Policies applied:

      +
      + { + policies.map((p) => ( + + {p.label} + + )) + } +
      +
      +
    8. + + + +
    9. +
      +
      + + Public Internet +
      +

      Any public hostname or IP

      +
      +
    10. +
    + + + Gateway logs + + DNS + HTTP + Network + + +
    + + diff --git a/src/nimbus/components/cf/WorkersVPCOverviewDiagram.astro b/src/nimbus/components/cf/WorkersVPCOverviewDiagram.astro new file mode 100644 index 00000000000..74ed413897a --- /dev/null +++ b/src/nimbus/components/cf/WorkersVPCOverviewDiagram.astro @@ -0,0 +1,505 @@ +--- +import { Icon as AstroIcon } from "astro-icon/components"; + +const policies = [ + { label: "DNS", href: "/cloudflare-one/traffic-policies/dns-policies/" }, + { label: "HTTP", href: "/cloudflare-one/traffic-policies/http-policies/" }, + { + label: "Network", + href: "/cloudflare-one/traffic-policies/network-policies/", + }, +]; + +const logsHref = "/cloudflare-one/insights/logs/dashboard-logs/gateway-logs/"; +--- + +
    +
    +
    +
    +
    + + Worker +
    +

    + Bind via vpc_services + or + vpc_networks +

    +
    +
    + +
    +
    + +

    + Reach private applications or networks through cloudflared +

    +
    + +
    + +

    + Reach the full account through cf1:network +

    +
    + +
    + +

    + Reach destinations through GRE, IPsec, or CNI on-ramps +

    +
    +
    + +
    +
    + +

    + Mesh and WAN traffic flows through Gateway, with policies enforced and + traffic logged. +

    +
    + { + policies.map((p) => ( + + {p.label} + + )) + } +
    +
    +
    + +
    +
    +
    + + Private services +
    +

    + APIs, databases, hosts in your network — reachable via any on-ramp +

    +
    + +
    +
    + + Public Internet +
    +

    Reachable via Mesh or WAN

    +
    +
    +
    +
    + + diff --git a/src/nimbus/components/cf/WranglerArg.astro b/src/nimbus/components/cf/WranglerArg.astro new file mode 100644 index 00000000000..d0d2a932bc8 --- /dev/null +++ b/src/nimbus/components/cf/WranglerArg.astro @@ -0,0 +1,55 @@ +--- +/** + * WranglerArg — one flag/positional row in a wrangler command's arg list. + * + * CF source: cloudflare-docs/src/components/WranglerArg.astro + * Mapping notes: extra-flag-details slot machinery dropped — it's only + * fed by upstream's ExtraFlagDetails wrapper, which Queues content + * never uses. + */ +import { marked } from "marked"; +import Type from "./Type.astro"; +import MetaInfo from "./MetaInfo.astro"; + +interface Props { + key: string; + definition: any; +} + +const { key, definition } = Astro.props; + +const type = definition.type ?? definition.choices; +const description = definition.description ?? definition.describe; + +// Descriptions may contain placeholders; escape them so +// marked doesn't treat them as HTML. +const sanitizedDescription = description + ? description.replace(//g, ">") + : ""; + +const required = definition.demandOption; +const defaultValue = definition.default; +const alias = definition.alias; + +const name = definition.positional ? `[${key.toUpperCase()}]` : `--${key}`; + +const typeText = Array.isArray(type) + ? type.map((t) => `"${t}"`).join(" | ") + : type; + +let aliasText; +if (alias) { + aliasText = Array.isArray(alias) + ? `aliases: --${alias.join(", --")}` + : `alias: --${alias}`; +} +--- + +
  • + {name} + {" "} + {aliasText && } + {required && } + {defaultValue !== undefined && } + +
  • diff --git a/src/nimbus/components/cf/WranglerCommand.astro b/src/nimbus/components/cf/WranglerCommand.astro new file mode 100644 index 00000000000..d03ed910f3f --- /dev/null +++ b/src/nimbus/components/cf/WranglerCommand.astro @@ -0,0 +1,112 @@ +--- +/** + * WranglerCommand — full reference block for one wrangler command: + * heading, description, runnable command (per package manager), arg + * list, and collapsible global flags. + * + * CF source: cloudflare-docs/src/components/WranglerCommand.astro + * Mapping notes: command data comes from the `wrangler` package's + * `experimental_getWranglerCommands()` registry (same as upstream). + * AnchorHeading → plain heading with a slugified id; Starlight Badge → + * ui/badge; starlight-package-managers → our PackageManagers port. + * ExtraFlagDetails slot machinery dropped (unused by Queues content). + */ +import { marked } from "marked"; +import { experimental_getWranglerCommands } from "wrangler"; +import { Badge } from "../ui/badge"; +import { PackageManagers } from "../ui/package-managers"; +import Details from "./Details.astro"; +import WranglerArg from "./WranglerArg.astro"; + +interface Props { + command: string; + headingLevel?: number; + description?: string; +} + +const { command, headingLevel = 2 } = Astro.props; + +const commands = experimental_getWranglerCommands(); +const { registry, globalFlags } = commands; + +let node = registry.subtree; +let definition; +for (const segment of command.trim().split(/\s+/)) { + const next = node.get(segment); + if (!next) break; + if (next.subtree.size === 0 && next.definition?.type === "command") { + definition = next.definition; + break; + } + node = next.subtree; +} + +if (!definition) { + throw new Error(`[WranglerCommand] Command "${command}" not found`); +} + +if (definition.metadata.hidden) { + throw new Error( + `[WranglerCommand] "${command}" is marked as hidden. If you want to publish, fix upstream in workers-sdk repo.`, + ); +} + +const description = Astro.props.description ?? definition.metadata.description; +const epilogue = definition.metadata.epilogue; +const experimental = definition.metadata.status === "experimental"; + +const positionals = definition.positionalArgs + ?.map((p: string) => `[${p.toUpperCase()}]`) + .join(" "); + +const positionalSet = new Set(definition.positionalArgs); + +const headingId = command.toLowerCase().replace(/[^a-z0-9]+/g, "-"); +const Heading = `h${headingLevel}` as "h2"; +--- + +{command} + +{ + experimental && ( + <> +
    + + + ) +} + + +{epilogue && } + + + +{ + definition.args && ( +
      + {Object.entries(definition.args) + .filter(([, value]: [string, any]) => !value.hidden) + .map(([key, value]: [string, any]) => ( + + ))} +
    + ) +} + +
    +
      + { + Object.entries(globalFlags).map(([key, value]) => ( + + )) + } +
    +
    diff --git a/src/nimbus/components/cf/WranglerConfig.astro b/src/nimbus/components/cf/WranglerConfig.astro new file mode 100644 index 00000000000..206613f6bea --- /dev/null +++ b/src/nimbus/components/cf/WranglerConfig.astro @@ -0,0 +1,86 @@ +--- +/** + * WranglerConfig — author one wrangler config block (TOML or JSONC), + * render synced wrangler.jsonc + wrangler.toml tabs. The other format is + * generated at build time, so the MDX stays single-source. + * + * CF source: cloudflare-docs/src/components/WranglerConfig.astro + * Mapping notes: same Expressive-Code → Shiki adaptation as + * TypeScriptExample — raw code and language come from the rendered + * `
    ` instead of the copy button's `data-code` attribute.
    + */
    +import TOML from "@iarna/toml";
    +import { parse as jsoncParse } from "jsonc-parser";
    +import { parse } from "node-html-parser";
    +import { Code } from "../ui/code";
    +import { TabItem, Tabs } from "../ui/tabs";
    +
    +interface Props {
    +  removeSchema?: boolean;
    +}
    +
    +const { removeSchema = false } = Astro.props;
    +
    +const slot = await Astro.slots.render("default");
    +// `blockTextElements: {}` — by default node-html-parser keeps 
     content
    +// as an unparsed raw-text blob, which makes `pre code` unmatchable.
    +const html = parse(slot, { blockTextElements: {} });
    +const codeEl = html.querySelector("pre code");
    +
    +if (!codeEl) {
    +  throw new Error(
    +    `[WranglerConfig] Expected a fenced toml/jsonc code block as the only child (on "${Astro.url.pathname}").`,
    +  );
    +}
    +
    +let code = codeEl.text;
    +
    +const hadTodayMagicString = /\$today/i.test(code);
    +code = code.replaceAll(/\$today/gi, new Date().toISOString().split("T")[0]!);
    +
    +const language = html.querySelector("[data-language]")?.attributes["data-language"];
    +
    +if (!language) {
    +  throw new Error(`[WranglerConfig] Unable to find data-language.`);
    +}
    +
    +/**
    + * When the author used `$today` for `compatibility_date`, inject a helpful
    + * comment above that line so readers know they should keep it current.
    + */
    +function injectCompatDateComment(src: string, commentPrefix: string): string {
    +  return src.replace(
    +    /^(\s*)(.*compatibility_date.*)/m,
    +    `$1${commentPrefix} Set this to today's date\n$1$2`,
    +  );
    +}
    +
    +let toml: string;
    +let json: string;
    +
    +if (language === "toml") {
    +  toml = code;
    +  const parsedToml = TOML.parse(code);
    +  const jsonObj = removeSchema
    +    ? parsedToml
    +    : { $schema: "./node_modules/wrangler/config-schema.json", ...parsedToml };
    +  json = JSON.stringify(jsonObj, null, 2);
    +} else {
    +  json = code;
    +  toml = TOML.stringify(jsoncParse(code));
    +}
    +
    +if (hadTodayMagicString) {
    +  toml = injectCompatDateComment(toml, "#");
    +  json = injectCompatDateComment(json, "//");
    +}
    +---
    +
    +
    +  
    +    
    +  
    +  
    +    
    +  
    +
    diff --git a/src/nimbus/components/cf/WranglerNamespace.astro b/src/nimbus/components/cf/WranglerNamespace.astro
    new file mode 100644
    index 00000000000..7be5c29a58d
    --- /dev/null
    +++ b/src/nimbus/components/cf/WranglerNamespace.astro
    @@ -0,0 +1,59 @@
    +---
    +/**
    + * WranglerNamespace — renders every visible command under a wrangler
    + * namespace (e.g. `wrangler queues *`) as WranglerCommand blocks.
    + *
    + * CF source: cloudflare-docs/src/components/WranglerNamespace.astro
    + * Mapping notes: identical logic; the command registry comes from the
    + * `wrangler` package already in this app's dependencies.
    + */
    +import { experimental_getWranglerCommands } from "wrangler";
    +import WranglerCommand from "./WranglerCommand.astro";
    +
    +interface Props {
    +  namespace: string;
    +  headingLevel?: number;
    +}
    +
    +const { namespace, headingLevel = 2 } = Astro.props;
    +
    +const { registry } = experimental_getWranglerCommands();
    +
    +const node = registry.subtree.get(namespace);
    +
    +if (!node || node.subtree.size === 0) {
    +  throw new Error(`[WranglerNamespace] Namespace "${namespace}" not found`);
    +}
    +
    +type Subtree = (typeof registry)["subtree"];
    +const definitions: NonNullable<(typeof node)["definition"]>[] = [];
    +
    +function flattenSubtree(subtree: Subtree) {
    +  for (const value of subtree.values()) {
    +    if (value.definition?.metadata?.hidden) continue;
    +    if (value.definition?.type === "command") {
    +      definitions.push(value.definition);
    +    } else {
    +      flattenSubtree(value.subtree);
    +    }
    +  }
    +}
    +
    +flattenSubtree(node.subtree);
    +---
    +
    +{
    +  definitions.map((definition) => {
    +    if (definition.type !== "command") {
    +      throw new Error(
    +        `[WranglerNamespace] Expected "command" but got "${definition.type}" for "${definition.command}"`,
    +      );
    +    }
    +    return (
    +      
    +    );
    +  })
    +}
    diff --git a/src/nimbus/components/cf/YouTube.astro b/src/nimbus/components/cf/YouTube.astro
    new file mode 100644
    index 00000000000..d17bd04b814
    --- /dev/null
    +++ b/src/nimbus/components/cf/YouTube.astro
    @@ -0,0 +1,33 @@
    +---
    +/**
    + * YouTube — responsive privacy-friendly YouTube embed.
    + *
    + * CF source: cloudflare-docs/src/components/YouTube.astro (ported verbatim).
    + */
    +import { z } from "astro/zod";
    +
    +type Props = z.infer;
    +
    +const props = z
    +  .object({
    +    id: z.string(),
    +    title: z.string().optional(),
    +  })
    +  .strict();
    +
    +let { id, title } = props.parse(Astro.props);
    +
    +if (!title) {
    +  title = `YouTube video with ID ${id}`;
    +}
    +---
    +
    +
    + +
    diff --git a/src/nimbus/components/changelog/ChangelogEntry.astro b/src/nimbus/components/changelog/ChangelogEntry.astro new file mode 100644 index 00000000000..e91f06d996e --- /dev/null +++ b/src/nimbus/components/changelog/ChangelogEntry.astro @@ -0,0 +1,59 @@ +--- +/** + * ChangelogEntry — one entry in the feed. Renders the date marker, a + * permalink title, product pills, and the entry's full MDX body inline + * (rendered with the site's MDX globals). + * + * Design: Nimbus changelog feature (registry/features/changelog.md, 5c). + * Adapted to the kept product model: product pills (links to product views) + * instead of opaque tag pills, and the `/changelog/post//` permalink. + */ +import { render, type CollectionEntry } from "astro:content"; +import DateRail from "./DateRail.astro"; +import ProductPills from "./ProductPills.astro"; +import { components } from "@/mdx-components"; + +interface Props { + entry: CollectionEntry<"changelog">; + /** Last visible entry — drops its trailing rail segment so the timeline + * closes at the final dot. */ + railEnd?: boolean; +} + +const { entry, railEnd = false } = Astro.props; +const { title, date, products } = entry.data; +const href = `/changelog/post/${entry.id}/`; +const { Content } = await render(entry); +--- +
    + + + {/* `nb-cl-title-link` is kept only as the `:has()` hover hook for the dot. */} +
    + +

    + {title} +

    +
    + + { + products.length > 0 && ( +
    + +
    + ) + } + +
    + +
    +
    +
    diff --git a/src/nimbus/components/changelog/ChangelogFeed.astro b/src/nimbus/components/changelog/ChangelogFeed.astro new file mode 100644 index 00000000000..b6ab72fe41f --- /dev/null +++ b/src/nimbus/components/changelog/ChangelogFeed.astro @@ -0,0 +1,135 @@ +--- +/** + * ChangelogFeed — the reverse-chronological timeline. Renders every (already + * paginated) entry on one continuous rail. Each entry's date carries its year, + * so there are no year dividers. + * + * The rail co-opts the page's decorative LEFT margin rule (drawn by + * ChangelogLayout) as its spine: nodes + a solid spine overlay sit on the + * column's left edge (left:0), exactly where the backdrop's dashed rule runs — + * so there is one continuous left line, dashed above the feed and solid through + * it. The date lives in the cleared left margin (right-aligned against the node) + * on wide screens, and stacks above the title where the margin is too tight. + * + * Design: Nimbus changelog feature (registry/features/changelog.md, 5d). + */ +import type { CollectionEntry } from "astro:content"; +import ChangelogEntry from "./ChangelogEntry.astro"; + +interface Props { + entries: CollectionEntry<"changelog">[]; +} + +const { entries } = Astro.props; + +const sorted = [...entries].sort( + (a, b) => b.data.date.getTime() - a.data.date.getTime(), +); + +const lastIndex = sorted.length - 1; +--- + +
    + { + sorted.length === 0 && ( +

    + No entries yet. +

    + ) + } + { + sorted.map((entry, i) => ( + + )) + } +
    + + diff --git a/src/nimbus/components/changelog/DateRail.astro b/src/nimbus/components/changelog/DateRail.astro new file mode 100644 index 00000000000..f4ab35a153f --- /dev/null +++ b/src/nimbus/components/changelog/DateRail.astro @@ -0,0 +1,30 @@ +--- +/** + * DateRail — the date marker on the timeline. Renders the full date + * ("Jun 12, 2026"); positioning is handled by the parent .nb-cl-entry + * (see ChangelogFeed). The year is inline so the feed needs no year dividers. + * + * Design: Nimbus changelog feature (registry/features/changelog.md, 5b). + */ +interface Props { + date: Date; +} + +const { date } = Astro.props; +const iso = date.toISOString().slice(0, 10); +const label = date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + timeZone: "UTC", +}); +--- + +
    + + +
    diff --git a/src/nimbus/components/changelog/Header.astro b/src/nimbus/components/changelog/Header.astro new file mode 100644 index 00000000000..a21e67a5b42 --- /dev/null +++ b/src/nimbus/components/changelog/Header.astro @@ -0,0 +1,180 @@ +--- +/** + * Header — the changelog hero: title, RSS button, tagline, and a product + * filter. + * + * Design: Nimbus changelog feature (registry/features/changelog.md, 5g). + * Adapted to the kept product model — the filter is the composable + * whose options navigate to the per-product / per-group + * views (and "All products" → /changelog/). The RSS href is contextual. + */ +import { Icon } from "astro-icon/components"; +import { directory, directoryByGroup, groups } from "~/util/directory"; +import { changelogProductIds } from "~/util/changelog"; +import { LinkButton } from "~/components/ui/link-button"; +import { + Combobox, + ComboboxOption, + ComboboxGroup, + ComboboxGroupLabel, +} from "~/components/ui/combobox"; + +interface Props { + heading?: string; + tagline?: string; + /** RSS feed for the current view (defaults to the all-products feed). */ + rssHref?: string; + /** Show the product filter. */ + showFilter?: boolean; +} + +const { + heading = "Changelog", + tagline = "New updates and improvements at Cloudflare.", + rssHref = "/changelog/rss/index.xml", + showFilter = true, +} = Astro.props; + +// Products that have at least one visible changelog entry, sorted by title. +const products = directory + .filter((entry) => changelogProductIds.includes(entry.id)) + .sort((a, b) => a.data.entry.title.localeCompare(b.data.entry.title)); + +// Groups that have at least one visible changelog entry. +const filteredGroups = groups + .filter((group) => { + const groupProducts = + directoryByGroup.find(([name]) => name === group)?.[1] ?? []; + return groupProducts.some((p) => changelogProductIds.includes(p.id)); + }) + .map((group) => ({ + label: group, + href: `/changelog/product-group/${group.replaceAll(" ", "-").toLowerCase()}/`, + })); + +// The option value is the destination URL; on change the filter navigates +// there. Match the current view so the right option shows as selected. +const path = Astro.url.pathname; +const productMatch = path.match(/^\/changelog\/product\/([^/]+)\//); +const groupMatch = path.match(/^\/changelog\/product-group\/([^/]+)\//); +const selectedHref = productMatch + ? `/changelog/product/${productMatch[1]}/` + : groupMatch + ? `/changelog/product-group/${groupMatch[1]}/` + : "/changelog/"; + +// The option labels live in slotted children, which the component can't read +// at render time — so it shows the placeholder until JS resolves the selected +// label. Seed the placeholder with the current label so the trigger reads +// correctly on first paint (and with no JS). +const selectedLabel = productMatch + ? (products.find((p) => p.id === productMatch[1])?.data.entry.title ?? + productMatch[1]) + : groupMatch + ? (filteredGroups.find((g) => g.href === selectedHref)?.label ?? + groupMatch[1]) + : "All products"; +--- + +
    + {/* Centered hero — type scale matches the directory page (Hero.astro clamp). */} +

    + {heading} +

    +

    + {tagline} +

    +
    + +{/* Controls toolbar — horizontal padding matches the feed's --body-x (the + entry text indent) so the controls line up with the entry titles/body: + 0 below md, 3.5rem at md, 4rem at lg. */} +
    + { + showFilter && products.length > 0 && ( +
    + + All products + {filteredGroups.length > 0 && ( + + Product groups + {filteredGroups.map((group) => ( + {group.label} + ))} + + )} + + Products + {products.map((product) => ( + + {product.data.entry.title} + + ))} + + +
    + ) + } + +
    + + All feeds + + + + RSS + +
    +
    + + diff --git a/src/nimbus/components/changelog/Pagination.astro b/src/nimbus/components/changelog/Pagination.astro new file mode 100644 index 00000000000..f98fd94bc36 --- /dev/null +++ b/src/nimbus/components/changelog/Pagination.astro @@ -0,0 +1,103 @@ +--- +/** + * Pagination — page-number strip for the paginated changelog views. + * CF source: cloudflare-docs/src/components/changelog/Pagination.astro + * Restyled with Nimbus theme tokens (neutral active state) so it auto-flips + * in dark mode — no bespoke CSS. + */ +import { cn } from "@/lib/cn"; + +interface Props { + currentPage: number; + lastPage: number; + /** Base URL for this paginated route, e.g. "/changelog/" or + * "/changelog/product/workflows/". Must end with a trailing slash. + * Page 1 = baseUrl, page N = baseUrl + N + "/". */ + baseUrl: string; +} + +const { currentPage, lastPage, baseUrl } = Astro.props; + +function pageUrl(n: number): string { + return n === 1 ? baseUrl : `${baseUrl}${n}/`; +} + +/** + * Always shows: first, last, current, one before current, one after current. + * Gaps between non-adjacent numbers become null (rendered as "…"). + */ +function buildPageItems(current: number, last: number): (number | null)[] { + if (last <= 1) return []; + + const visible = new Set(); + visible.add(1); + visible.add(last); + visible.add(current); + if (current - 1 >= 1) visible.add(current - 1); + if (current + 1 <= last) visible.add(current + 1); + + const sorted = [...visible].sort((a, b) => a - b); + + const items: (number | null)[] = []; + for (let i = 0; i < sorted.length; i++) { + if (i > 0 && sorted[i] - sorted[i - 1] > 1) { + items.push(null); // ellipsis gap + } + items.push(sorted[i]); + } + return items; +} + +const items = buildPageItems(currentPage, lastPage); +const prevUrl = currentPage > 1 ? pageUrl(currentPage - 1) : null; +const nextUrl = currentPage < lastPage ? pageUrl(currentPage + 1) : null; + +const btn = + "flex h-9 min-w-9 items-center justify-center rounded px-3 text-sm font-medium"; +const link = cn(btn, "no-underline hover:bg-accent"); +const disabled = cn(btn, "opacity-40"); +const active = cn(btn, "bg-foreground font-bold text-background"); +--- + +{ + lastPage > 1 && ( + + ) +} diff --git a/src/nimbus/components/changelog/ProductPills.astro b/src/nimbus/components/changelog/ProductPills.astro new file mode 100644 index 00000000000..3a232ae51d9 --- /dev/null +++ b/src/nimbus/components/changelog/ProductPills.astro @@ -0,0 +1,31 @@ +--- +/** + * ProductPills — badge links for the products a changelog entry is tagged + * with. CF source: cloudflare-docs/src/components/changelog/ProductPills.astro + * Adapted to this app's Badge (Starlight's Badge isn't available here). + */ +import { getEntries, reference } from "astro:content"; +import { z } from "astro/zod"; + +import { Badge } from "~/components/ui/badge"; + +type Props = z.infer; + +const props = z.object({ + products: z.array(reference("directory")), +}); + +const { products } = await props.parseAsync(Astro.props); + +const data = await getEntries(products); +--- + +
    + { + data.map((product) => ( + + + + )) + } +
    diff --git a/src/nimbus/components/directory/Directory.astro b/src/nimbus/components/directory/Directory.astro new file mode 100644 index 00000000000..4fc1ee0fde6 --- /dev/null +++ b/src/nimbus/components/directory/Directory.astro @@ -0,0 +1,212 @@ +--- +/** + * Directory — filterable catalog of documentation product folders (modelled on + * cloudflare.com/products). Astro + a vanilla controller (directory.client.ts); + * no framework island, so it can use the Astro primitive for the + * category filter. + * + * Layout: a centered, search-first hero drawn on the homepage's structural + * blueprint (FullPageLines + StructuralGrid), an always-present category + * checklist (sticky on desktop), and ONE flat grid of every product — no + * per-group sections, so each product appears exactly once even when it + * belongs to several groups. Cells are flat (defined only by the shared 1px + * lines) with rounded corner-marks on every intersection; cells close their + * own right/bottom edge (the container draws top/left), so a ragged final row + * needs no filler cells. The controller scopes by category and re-flows the + * single grid as search filters it, reusing ./grid.ts so build and runtime + * agree. + * + * Typography matches the homepage: the hero uses the Hero.astro clamp at + * font-medium. + */ +import { Icon } from "astro-icon/components"; +import { Checkbox, CheckboxGroup } from "~/components/ui/checkbox"; +import BackgroundLines from "../BackgroundLines.astro"; +import { + type ProductData, + UNCATEGORIZED, + LG_GRID_CLASS, + resolveCols, + cornersFor, + cellClass, + cornerSpansHTML, +} from "./grid"; + +interface Props { + products: ProductData[]; +} + +const { products } = Astro.props; + +// A product's effective categories (its declared groups, or `Other`). +const groupsOf = (product: ProductData): string[] => + product.groups.length ? product.groups : [UNCATEGORIZED]; + +// One flat list of unique products, sorted by name. A multi-group product +// still renders once; the category filter matches it via its `data-groups`. +const items = products + .slice() + .sort((a, b) => a.data.name.localeCompare(b.data.name)); +const cols = resolveCols(items.length); + +// Group list for the filter checklist — alphabetical, `Other` last. +const groupSet = new Set(); +for (const product of products) + for (const group of groupsOf(product)) groupSet.add(group); +const groups = [...groupSet].sort((a, b) => { + if (a === UNCATEGORIZED) return 1; + if (b === UNCATEGORIZED) return -1; + return a.localeCompare(b); +}); +--- + +
    + + +
    + {/* ---- hero: title + tagline only; type matches Hero.astro ---- */} +
    +

    + Directory +

    +

    + Everything you need to build, deploy, and scale applications on Cloudflare's global network. +

    +
    + + {/* ---- working area: rail (search + checklist) + results ---- */} +
    + + +
    + + +
    +
    + { + items.map((product, i) => { + const description = + product.data.meta?.description ?? product.data.entry.title; + return ( + +
    +
    +
    +
    +
    + + diff --git a/src/nimbus/components/directory/directory.client.ts b/src/nimbus/components/directory/directory.client.ts new file mode 100644 index 00000000000..9ee6ed1c342 --- /dev/null +++ b/src/nimbus/components/directory/directory.client.ts @@ -0,0 +1,179 @@ +/** + * Directory controller — progressive enhancement for . Vanilla DOM + * (no framework); re-inits on Astro view transitions via `mount`, which also + * tears down listeners on swap. + * + * Layout is the "blueprint console": a centered command search in the hero, an + * always-present category checklist, and ONE flat corner-mark grid of every + * product (geometry from ./grid.ts — the single source of truth shared with + * the build-time render in Directory.astro). No per-group sections, so each + * product appears once even when it belongs to several groups. + * + * Owns: + * - category filter: checked checkboxes show only products in those + * categories (none checked = all); a product matches if any of its + * `data-groups` is checked. + * - search: filters products by name. Combined with the category filter, the + * single grid is re-flowed (columns + corner marks) using the shared + * geometry in ./grid.ts. Cells close their own edges, so no fillers. + * ⌘K / "/" focus the search, Esc clears it. + * - the empty state, a "Clear filters" affordance, and URL sync + * (`?search=…&group=…&group=…`). + */ +import { mount } from "nimbus-docs/client"; +import { setSearchParams } from "~/util/url"; +import { + LG_GRID_CLASS, + resolveCols, + cornersFor, + cellClass, + cornerSpansHTML, +} from "./grid"; + +function initDirectory(root: HTMLElement): () => void { + const search = root.querySelector( + "[data-directory-search]", + ); + const categories = Array.from( + root.querySelectorAll('input[name="category"]'), + ); + const grid = root.querySelector("[data-directory-grid]"); + const empty = root.querySelector("[data-directory-empty]"); + const clearBtn = root.querySelector( + "[data-directory-clear]", + ); + const searchClear = root.querySelector( + "[data-directory-search-clear]", + ); + + const cells = grid + ? Array.from(grid.querySelectorAll("[data-directory-cell]")) + : []; + + // Below `lg` the grid collapses to a single column (grid-cols-1), so the + // corner-marks must be computed for 1 column there to stay aligned. + const lgQuery = window.matchMedia("(min-width: 1024px)"); + const isLg = (): boolean => lgQuery.matches; + + const selectedGroups = (): string[] => + categories.filter((c) => c.checked).map((c) => c.value); + + // Re-flow the grid to the products matching `query` and `groups`. Returns + // the number of visible products. + function relayout(query: string, groups: string[]): number { + if (!grid) return 0; + + const matches = cells.filter((cell) => { + const nameOk = !query || (cell.dataset.name ?? "").includes(query); + const cellGroups = (cell.dataset.groups ?? "").split("|"); + const groupOk = + groups.length === 0 || groups.some((g) => cellGroups.includes(g)); + return nameOk && groupOk; + }); + + const lgCols = resolveCols(matches.length); + grid.className = LG_GRID_CLASS[lgCols] ?? LG_GRID_CLASS[1]; + + // Corner-marks track the actually-rendered column count (1 below `lg`). + const cols = isLg() ? lgCols : 1; + + for (const cell of cells) cell.style.display = "none"; + matches.forEach((cell, i) => { + cell.style.display = ""; + cell.className = cellClass; + const marks = cell.querySelector("[data-corner-marks]"); + if (marks) marks.innerHTML = cornerSpansHTML(cornersFor(i, cols)); + }); + + return matches.length; + } + + function update(syncUrl = true): void { + const raw = search?.value ?? ""; + const query = raw.trim().toLowerCase(); + const groups = selectedGroups(); + + const count = relayout(query, groups); + + if (empty) empty.hidden = count > 0; + if (clearBtn) clearBtn.hidden = groups.length === 0 && query === ""; + if (searchClear) searchClear.hidden = query === ""; + + if (syncUrl) { + const params = new URLSearchParams(); + if (raw.trim()) params.set("search", raw); + groups.forEach((group) => params.append("group", group)); + setSearchParams(params); + } + } + + // Seed state from the URL (deep links / back-forward), then render once + // without writing the URL back. + function applyFromUrl(): void { + const params = new URLSearchParams(window.location.search); + if (search) search.value = params.get("search") ?? ""; + const selected = params + .getAll("group") + .flatMap((value) => value.split(",")); + for (const cb of categories) cb.checked = selected.includes(cb.value); + } + + function onSearch(): void { + update(); + } + function onCategory(): void { + update(); + } + function onClear(): void { + if (search) search.value = ""; + for (const cb of categories) cb.checked = false; + update(); + search?.focus(); + } + function onSearchClear(): void { + if (search) search.value = ""; + update(); + search?.focus(); + } + function onKeydown(event: KeyboardEvent): void { + const slash = event.key === "/" && document.activeElement !== search; + const cmdK = + event.key.toLowerCase() === "k" && (event.metaKey || event.ctrlKey); + if (slash || cmdK) { + event.preventDefault(); + search?.focus(); + search?.select(); + } else if ( + event.key === "Escape" && + document.activeElement === search && + search?.value + ) { + search.value = ""; + update(); + } + } + + // Re-flow corner-marks when crossing the lg breakpoint (cols 1 ↔ N). + const onBreakpoint = (): void => update(false); + + search?.addEventListener("input", onSearch); + for (const cb of categories) cb.addEventListener("change", onCategory); + clearBtn?.addEventListener("click", onClear); + searchClear?.addEventListener("click", onSearchClear); + document.addEventListener("keydown", onKeydown); + lgQuery.addEventListener("change", onBreakpoint); + + applyFromUrl(); + update(false); + + return () => { + search?.removeEventListener("input", onSearch); + for (const cb of categories) cb.removeEventListener("change", onCategory); + clearBtn?.removeEventListener("click", onClear); + searchClear?.removeEventListener("click", onSearchClear); + document.removeEventListener("keydown", onKeydown); + lgQuery.removeEventListener("change", onBreakpoint); + }; +} + +mount("[data-directory]", initDirectory); diff --git a/src/nimbus/components/directory/grid.ts b/src/nimbus/components/directory/grid.ts new file mode 100644 index 00000000000..2321e0e3a78 --- /dev/null +++ b/src/nimbus/components/directory/grid.ts @@ -0,0 +1,79 @@ +/** + * Directory grid layout — the pure geometry shared by the build-time render + * (`Directory.astro`) and the client controller (`directory.client.ts`), so + * the corner-mark + border bookkeeping has a single source of truth. + * + * Layout is modelled on cloudflare.com/products: one bordered grid per + * category. Cells are flat (defined only by the shared 1px lines) with small + * rounded "corner marks" sitting on every line intersection. A section uses + * as many columns as it has products (capped at LG_COLS), so a short category + * never shows empty trailing columns. + */ +import type { CollectionEntry } from "astro:content"; +import type { IconifyIconBuildResult } from "@iconify/utils"; + +export type ProductData = CollectionEntry<"directory"> & { + icon?: IconifyIconBuildResult; + groups: string[]; +}; + +/** Max columns at `lg`. Below `lg` every grid is a single column. */ +export const LG_COLS = 3; + +/** Bucket for products that declare no group. */ +export const UNCATEGORIZED = "Other"; + +// Static `grid-cols` strings, indexed by resolved column count, so Tailwind's +// scanner sees them as literals. +export const LG_GRID_CLASS: Record = { + 1: "grid grid-cols-1 lg:grid-cols-1", + 2: "grid grid-cols-1 lg:grid-cols-2", + 3: "grid grid-cols-1 lg:grid-cols-3", +}; + +export type Corner = "tl" | "tr" | "bl" | "br"; + +const CORNER_OFFSET: Record = { + tl: "top:-7px;left:-7px", + tr: "top:-7px;right:-7px", + bl: "left:-7px;bottom:-7px", + br: "right:-7px;bottom:-7px", +}; + +/** Columns a section of `count` products should use (1..LG_COLS). */ +export function resolveCols(count: number): number { + return Math.min(LG_COLS, count) || 1; +} + +// One mark per line intersection, deduplicated by ownership so adjacent cells +// don't stack squares on the same point: +// - top-left cell owns all four of its corners +// - the rest of the top row owns its top-right + bottom-right +// - the rest of the first column owns its bottom-left + bottom-right +// - every other cell owns only its bottom-right +export function cornersFor(index: number, cols: number): Corner[] { + const row = Math.floor(index / cols); + const col = index % cols; + if (row === 0 && col === 0) return ["tl", "tr", "bl", "br"]; + if (row === 0) return ["tr", "br"]; + if (col === 0) return ["bl", "br"]; + return ["br"]; +} + +// Each cell closes its own right + bottom edge; the grid container draws the +// top + left edges (see Directory.astro). This "self-closing" model means a +// ragged final row needs no filler cells — the populated cells still draw +// clean single-width lines, and there are no empty boxes. Uniform across +// columns, so it takes no index/cols. +export const cellClass = + "border-border bg-background relative flex h-full flex-col border-r border-b"; + +/** Inner HTML for a cell's corner-mark container (the small squares). */ +export function cornerSpansHTML(corners: Corner[]): string { + return corners + .map( + (c) => + ``, + ) + .join(""); +} diff --git a/src/nimbus/components/fields/FieldBadges.tsx b/src/nimbus/components/fields/FieldBadges.tsx new file mode 100644 index 00000000000..16cc2c0c503 --- /dev/null +++ b/src/nimbus/components/fields/FieldBadges.tsx @@ -0,0 +1,13 @@ +const FieldBadges = ({ badges }: { badges: string[] }) => { + return ( +
      + {badges.map((badge) => ( +
    • + {badge} +
    • + ))} +
    + ); +}; + +export default FieldBadges; diff --git a/src/nimbus/components/index.ts b/src/nimbus/components/index.ts new file mode 100644 index 00000000000..54b64625722 --- /dev/null +++ b/src/nimbus/components/index.ts @@ -0,0 +1 @@ +export * from "../components"; diff --git a/src/nimbus/components/landing/AccelerateSection.astro b/src/nimbus/components/landing/AccelerateSection.astro new file mode 100644 index 00000000000..c6a5f5ef587 --- /dev/null +++ b/src/nimbus/components/landing/AccelerateSection.astro @@ -0,0 +1,46 @@ +--- +import ProductCard from "@/components/ProductCard.astro"; +import ProductCardGrid from "@/components/ProductCardGrid.astro"; +import SectionHeading from "./SectionHeading.astro"; +import ViewAllLink from "./ViewAllLink.astro"; +import { products } from "./accelerate-data"; +--- + +
    +
    +
    + + Faster web performance + +

    + Accelerate websites and applications with Cloudflare CDN caching, image + optimization, smart routing, load balancing, and web analytics. +

    +
    + + Explore Directory + +
    + + + { + products.map((p) => ( + + )) + } + +
    diff --git a/src/nimbus/components/landing/AgentSetup.astro b/src/nimbus/components/landing/AgentSetup.astro new file mode 100644 index 00000000000..643159d1acc --- /dev/null +++ b/src/nimbus/components/landing/AgentSetup.astro @@ -0,0 +1,61 @@ +--- +import { Icon } from "astro-icon/components"; +import CornerMarks from "@/components/CornerMarks.astro"; +import HalftoneBackground from "./HalftoneBackground.astro"; +import SectionHeading from "./SectionHeading.astro"; +import CopyPromptButton from "@/components/CopyPromptButton.astro"; +import { LinkButton } from "@/components/ui/link-button"; + +const ALL_GUIDES_HREF = "/agent-setup/"; +--- + +
    +
    + + +
    +
    + + Build with your favorite AI agent + + +

    + Paste into any AI coding agent to install Cloudflare agent tooling: +

    + +
    +
    + +
    + + Browse all agent setup guides + +
    + + + All agents + + +
    +
    +
    +
    diff --git a/src/nimbus/components/landing/BuildFromScratch.astro b/src/nimbus/components/landing/BuildFromScratch.astro new file mode 100644 index 00000000000..534769524ed --- /dev/null +++ b/src/nimbus/components/landing/BuildFromScratch.astro @@ -0,0 +1,185 @@ +--- +import { Icon } from "astro-icon/components"; +import CommandBlock from "./CommandBlock.astro"; +import CornerMarks from "@/components/CornerMarks.astro"; +import { LinkButton } from "@/components/ui/link-button"; +import SectionHeading from "./SectionHeading.astro"; +import TabBar from "./TabBar.astro"; + +const primitives = [ + { + id: "compute", + label: "Compute", + icon: "workers-vpc", + iconColor: "#0a95ff", + iconBg: "rgba(10, 149, 255, 0.1)", + iconBgDark: "rgba(10, 149, 255, 0.16)", + heading: "Deploy with one command", + description: + "Build and deploy serverless functions and full-stack apps on Cloudflare's global network. No servers to manage. No cold starts or region complexity.", + command: "npm create cloudflare@latest my-app", + cta: { + label: "Create your first Worker", + href: "/workers/get-started/guide/", + }, + tags: [ + { label: "Workers", href: "/workers/" }, + { label: "Containers", href: "/containers/" }, + { label: "Durable Objects", href: "/durable-objects/" }, + { label: "Queues", href: "/queues/" }, + ], + }, + { + id: "ai", + label: "AI", + icon: "workers-ai", + iconColor: "#19e306", + iconBg: "#f2f5e1", + iconBgDark: "rgba(25, 227, 6, 0.16)", + heading: "The AI inference platform", + description: + "Run AI inference globally with one API call, build agents, and search across your data — no GPUs to manage, no capacity planning.", + command: "npx wrangler ai models", + cta: { label: "Browse available models", href: "/workers-ai/models/" }, + tags: [ + { label: "Workers AI", href: "/workers-ai/" }, + { label: "AI Gateway", href: "/ai-gateway/" }, + { label: "Agents", href: "/agents/" }, + { label: "Vectorize", href: "/vectorize/" }, + { label: "Browser Run", href: "/browser-run/" }, + ], + }, + { + id: "storage", + label: "Storage & Databases", + icon: "d1", + iconColor: "#ee0ddb", + iconBg: "rgba(238, 13, 219, 0.1)", + iconBgDark: "rgba(238, 13, 219, 0.16)", + heading: "Make your database feel instant, everywhere", + description: + "Serverless SQL, globally distributed key-value, and global database acceleration — query directly from Workers with no connection management.", + command: "npx wrangler d1 create my-database", + cta: { label: "Get started with D1", href: "/d1/get-started/" }, + tags: [ + { label: "R2", href: "/r2/" }, + { label: "Pipelines", href: "/pipelines/" }, + { label: "D1", href: "/d1/" }, + { label: "KV", href: "/kv/" }, + { label: "Hyperdrive", href: "/hyperdrive/" }, + ], + }, + { + id: "media", + label: "Media", + icon: "images", + iconColor: "#9616ff", + iconBg: "rgba(150, 22, 255, 0.1)", + iconBgDark: "rgba(150, 22, 255, 0.22)", + heading: "Build media pipelines without infrastructure headaches", + description: + "Cloudflare Images helps teams build scalable, reliable media pipelines to store, optimize, and deliver images.", + command: + "curl --request POST https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/images/v1", + cta: { + label: "Get started with Images", + href: "/images/get-started/introduction/", + }, + tags: [ + { label: "Images", href: "/images/" }, + { label: "Stream", href: "/stream/" }, + { label: "Realtime", href: "/realtime/" }, + ], + }, +]; +--- + +
    + + Powerful primitives, seamlessly integrated + + +
    + + + ({ id: p.id, label: p.label }))} + /> + + { + primitives.map((p, i) => ( + + )) + } +
    +
    + + diff --git a/src/nimbus/components/landing/ChangelogSection.astro b/src/nimbus/components/landing/ChangelogSection.astro new file mode 100644 index 00000000000..446289bbd3d --- /dev/null +++ b/src/nimbus/components/landing/ChangelogSection.astro @@ -0,0 +1,177 @@ +--- +import { getChangelogs } from "~/util/changelog"; +import { getEntries } from "astro:content"; +import { Icon } from "astro-icon/components"; +import CornerMarks from "@/components/CornerMarks.astro"; + +const fmtLong = new Intl.DateTimeFormat("en-US", { + month: "short", + day: "2-digit", + year: "numeric", + timeZone: "UTC", +}); +const fmtShort = new Intl.DateTimeFormat("en-US", { + month: "short", + day: "2-digit", + timeZone: "UTC", +}); +import HalftoneBackground from "./HalftoneBackground.astro"; +import SectionHeading from "./SectionHeading.astro"; +import ViewAllLink from "./ViewAllLink.astro"; + +const COMPUTE = new Set([ + "workers", + "pages", + "containers", + "workflows", + "durable-objects", + "queues", + "pipelines", + "cloudflare-for-platforms", + "workers-vpc", +]); +const STORAGE = new Set(["r2", "d1", "kv", "hyperdrive"]); +const AI = new Set([ + "workers-ai", + "ai-gateway", + "ai-search", + "agents", + "vectorize", + "browser-rendering", + "ai-crawl-control", +]); +const MEDIA = new Set(["images", "stream", "realtime"]); + +function catColor(id: string) { + if (COMPUTE.has(id)) return "#0a95ff"; + if (STORAGE.has(id)) return "#ee0ddb"; + if (AI.has(id)) return "#19e306"; + if (MEDIA.has(id)) return "#9616ff"; + return "#f56500"; +} + +const all = await getChangelogs({ + filter: (e) => !e.data.hidden, +}); + +const entries = await Promise.all( + all.slice(0, 8).map(async (entry) => { + const products = await getEntries(entry.data.products); + return { + title: entry.data.title, + description: entry.data.description, + dateFormatted: fmtLong.format(entry.data.date), + dateShort: fmtShort.format(entry.data.date), + products: products.map((p) => ({ + name: p.data.entry.title, + id: p.id, + color: catColor(p.id), + })), + url: `/changelog/post/${entry.id}/`, + }; + }), +); + +const featured = entries[0]; +const grid = entries.slice(1); +--- + +{ + entries.length > 0 && ( +
    +
    +
    + + What's new + +

    + The latest features and improvements shipping across Cloudflare. +

    +
    + View Changelog +
    + +
    + ) +} diff --git a/src/nimbus/components/landing/CommandBlock.astro b/src/nimbus/components/landing/CommandBlock.astro new file mode 100644 index 00000000000..3fa03880385 --- /dev/null +++ b/src/nimbus/components/landing/CommandBlock.astro @@ -0,0 +1,126 @@ +--- +interface Props { + id: string; + command: string; + showPrompt?: boolean; + rounded?: boolean; + class?: string; +} + +const { + id, + command, + showPrompt = false, + rounded = false, + class: className = "", +} = Astro.props; +--- + +
    + { + showPrompt && ( + + ) + } + + + {command} + + + +
    + + + + diff --git a/src/nimbus/components/landing/CommunityGrid.astro b/src/nimbus/components/landing/CommunityGrid.astro new file mode 100644 index 00000000000..71f18a01aeb --- /dev/null +++ b/src/nimbus/components/landing/CommunityGrid.astro @@ -0,0 +1,85 @@ +--- +import CornerMarks from "@/components/CornerMarks.astro"; +import SectionHeading from "./SectionHeading.astro"; + +const cards = [ + { + label: "Community", + heading: "Join the conversation", + desc: "Share ideas, answers, and code with the Cloudflare community.", + links: [ + { label: "Discord", href: "https://discord.cloudflare.com/" }, + { label: "X", href: "https://x.com/cloudflare" }, + { label: "Forum", href: "https://community.cloudflare.com/" }, + ], + }, + { + label: "Open Source", + heading: "View the source", + desc: "Cloudflare contributes to the open-source ecosystem in a variety of ways, including:", + links: [ + { label: "GitHub", href: "https://github.com/cloudflare" }, + { label: "Sponsors", href: "https://github.com/sponsors/cloudflare" }, + { label: "Style guide", href: "/style-guide/" }, + ], + }, + { + label: "Blog", + heading: "Read the latest", + desc: "Get the latest news on Cloudflare products, technologies, and culture.", + links: [ + { label: "blog.cloudflare.com", href: "https://blog.cloudflare.com/" }, + ], + }, +]; +--- + +
    +
    + + Connect with Cloudflare + +

    + Find community, read the blog, and explore open source projects. +

    +
    + +
    + + + { + cards.map((card) => ( +
    + + {card.label} + +

    + {card.heading} +

    +

    + {card.desc} +

    + +
    + {card.links.map((link) => ( + + {link.label} + + ))} +
    +
    + )) + } +
    +
    diff --git a/src/nimbus/components/landing/FullPageLines.astro b/src/nimbus/components/landing/FullPageLines.astro new file mode 100644 index 00000000000..8f67a492aa0 --- /dev/null +++ b/src/nimbus/components/landing/FullPageLines.astro @@ -0,0 +1,61 @@ +--- +/** + * FullPageLines — right-side dashed gutter for the landing page. + * + * Two 1px vertical dashed rules bracket a dot-grid column on the RIGHT side of + * the content. The inner rule sits on the cards' right edge (the content + * column's edge); the outer rule sits at the container boundary, with the dot + * grid filling the padding gutter between the two rules. Desktop-only, purely + * decorative. + * + * Shares the blueprint treatment with /directory (BackgroundLines) and + * /changelog (ChangelogLayout): dashed rules use --nb-border; dots use + * --nb-grid-line. + */ + +// 1px vertical dashed rule: 16px dash + 16px gap (32px period). +const dashed = + "background-image:linear-gradient(to bottom,var(--nb-border) 50%,transparent 50%);background-size:1px 32px;background-repeat:repeat-y"; + +// Matches the content container's horizontal padding in index.astro, so the +// gutter tracks the cards' right edge responsively. +const pad = "max(2rem, 4vw)"; +--- + + diff --git a/src/nimbus/components/landing/HalftoneBackground.astro b/src/nimbus/components/landing/HalftoneBackground.astro new file mode 100644 index 00000000000..cd43b4f798b --- /dev/null +++ b/src/nimbus/components/landing/HalftoneBackground.astro @@ -0,0 +1,44 @@ +--- +interface Props { + class?: string; +} + +const { class: className = "" } = Astro.props; +--- + + + + diff --git a/src/nimbus/components/landing/Hero.astro b/src/nimbus/components/landing/Hero.astro new file mode 100644 index 00000000000..4469ce56df2 --- /dev/null +++ b/src/nimbus/components/landing/Hero.astro @@ -0,0 +1,29 @@ +--- +import { Icon } from "astro-icon/components"; +import { stagger } from "./animation"; +import { LinkButton } from "@/components/ui/link-button"; +import CopyPromptButton from "@/components/CopyPromptButton.astro"; +--- + +
    +

    + Cloudflare Developer Docs +

    + +

    + Explore guides and tutorials to start building on Cloudflare's platform +

    + +
    + + Get started + + + + +
    +
    diff --git a/src/nimbus/components/landing/SectionHeading.astro b/src/nimbus/components/landing/SectionHeading.astro new file mode 100644 index 00000000000..d39eea68d8a --- /dev/null +++ b/src/nimbus/components/landing/SectionHeading.astro @@ -0,0 +1,18 @@ +--- +interface Props { + id?: string; + class?: string; +} + +const { id, class: className = "" } = Astro.props; +--- + +

    + +

    diff --git a/src/nimbus/components/landing/SecureSection.astro b/src/nimbus/components/landing/SecureSection.astro new file mode 100644 index 00000000000..4b97daf1f61 --- /dev/null +++ b/src/nimbus/components/landing/SecureSection.astro @@ -0,0 +1,107 @@ +--- +import ProductCard from "@/components/ProductCard.astro"; +import ProductCardGrid from "@/components/ProductCardGrid.astro"; +import SectionHeading from "./SectionHeading.astro"; + +const categories = [ + { + label: "Public websites & apps", + threats: [ + { + scenario: "Protect your applications without sacrificing performance", + product: "WAF", + href: "/waf/", + icon: "ddos-protection", + detail: + "Identify and block malicious payloads before they can compromise your application.", + cta: "Harden your app with WAF", + }, + { + scenario: "Encrypt your site in minutes", + product: "SSL/TLS", + href: "/ssl/", + icon: "ssl", + detail: "Streamline TLS Certificate Management.", + cta: "Set up SSL/TLS", + }, + { + scenario: "Verify visitors without CAPTCHA", + product: "Turnstile", + href: "/turnstile/", + icon: "turnstile", + detail: + "Confirm web visitors are real and block unwanted bots without slowing down web experiences for real users.", + cta: "Add Turnstile protection", + }, + ], + }, + { + label: "Corporate and home networks", + category: "zero-trust" as const, + threats: [ + { + scenario: + "Securely connect origins with post-quantum encrypted tunnels", + product: "Tunnel", + href: "/cloudflare-one/networks/connectors/cloudflare-tunnel/", + icon: "tunnel", + detail: "Outbound-only encrypted tunnels, no open ports.", + cta: "Create a secure Tunnel", + }, + { + scenario: "Secure internal applications with Cloudflare Access", + product: "Access", + href: "/cloudflare-one/access-controls/", + icon: "access", + detail: + "Identity-first, quantum-safe access to private applications and infrastructure.", + cta: "Set up Cloudflare Access", + }, + { + scenario: "Secure Internet browsing without disruptions", + product: "Gateway", + href: "/cloudflare-one/traffic-policies/", + icon: "gateway", + detail: + "Cloud-native Secure Web Gateway (SWG) that inspects browser traffic without disruption.", + cta: "Create Gateway policies", + }, + ], + }, +]; +--- + +
    + + Security that scales + +

    + Everything you need to secure applications, APIs, and infrastructure. +

    + +
    + { + categories.map((cat) => ( + + {cat.threats.map((t) => ( + + ))} + + )) + } +
    +
    diff --git a/src/nimbus/components/landing/Sidebar.astro b/src/nimbus/components/landing/Sidebar.astro new file mode 100644 index 00000000000..8d8dd8e27c6 --- /dev/null +++ b/src/nimbus/components/landing/Sidebar.astro @@ -0,0 +1,82 @@ +--- +// Landing mega-nav: static section headings over the shared ui/sidebar +// primitives (SidebarGroup + SidebarLink). +import { SidebarGroup, SidebarLink } from "@/components/ui/sidebar"; +import { sidebarSections, type SidebarNode } from "./sidebar-data"; +import type { SidebarItem } from "nimbus-docs/types"; + +interface Props { + currentPath: string; + class?: string; +} + +const { currentPath, class: className = "" } = Astro.props; + +const normalize = (p: string) => (p.length > 1 ? p.replace(/\/$/, "") : p); +const current = normalize(currentPath); +const isActive = (href: string) => normalize(href) === current; + +function toItem(node: SidebarNode, order: number): SidebarItem { + if (node.type === "link") { + return { + type: "link", + label: node.label, + href: node.href, + isCurrent: isActive(node.href), + order, + }; + } + return { + type: "group", + label: node.label, + collapsed: node.collapsed ?? true, + children: node.nodes.map(toItem), + order, + }; +} +--- + +
    + { + sidebarSections.map((section, i) => ( +
    0 && "mt-3"]}> +

    + {section.heading} +

    +
      + {section.nodes.map((node, idx) => ( +
    • + {node.type === "link" ? ( + + ) : ( + + )} +
    • + ))} +
    +
    + )) + } +
    + + + + diff --git a/src/nimbus/components/landing/StructuralGrid.astro b/src/nimbus/components/landing/StructuralGrid.astro new file mode 100644 index 00000000000..4c546e71fad --- /dev/null +++ b/src/nimbus/components/landing/StructuralGrid.astro @@ -0,0 +1,83 @@ +--- +const LINE_TIERS = [ + { width: "min(1400px, 100%)", accent: false }, + { width: "min(1280px, calc(100% - 48px))", accent: true }, + { width: "min(1216px, calc(100% - 112px))", accent: false }, + { width: "min(720px, calc(100% - 120px))", accent: false }, +]; + +const marks = + "absolute -top-0 left-1/2 -translate-x-1/2 h-full pointer-events-none"; +--- + + diff --git a/src/nimbus/components/landing/TabBar.astro b/src/nimbus/components/landing/TabBar.astro new file mode 100644 index 00000000000..a841c59dfc1 --- /dev/null +++ b/src/nimbus/components/landing/TabBar.astro @@ -0,0 +1,287 @@ +--- +import { Icon } from "astro-icon/components"; + +export interface Tab { + id: string; + label: string; + icon?: string; +} + +export type TabBarVariant = "pill" | "underline"; + +interface Props { + name: string; + tabs: Tab[]; + activeId?: string; + variant?: TabBarVariant; + "aria-label"?: string; + class?: string; +} + +const { + name, + tabs, + activeId = tabs[0]?.id, + variant = "pill", + "aria-label": ariaLabel, + class: className = "", +} = Astro.props; + +interface VariantStyles { + tablist: string; + indicator: string; + tab: { base: string; active: string; inactive: string }; +} + +const variantStyles: Record = { + pill: { + tablist: [ + "pointer-events-auto relative z-20 inline-flex items-center gap-1", + "border-border bg-background shrink-0 overflow-x-auto rounded-full border p-1", + "scrollbar-hide max-w-full md:max-w-none md:overflow-x-visible", + ].join(" "), + indicator: [ + "bg-primary pointer-events-none absolute top-1", + "h-[calc(100%-8px)] rounded-full", + "shadow-[0_1px_2px_rgba(0,0,0,0.06),0_4px_12px_rgba(0,0,0,0.08)]", + "opacity-0", + ].join(" "), + tab: { + base: [ + "relative z-10 flex h-10 cursor-pointer items-center gap-2", + "rounded-full px-3.5 py-2.5 font-medium -tracking-[0.01em]", + "whitespace-nowrap transition-colors duration-300 ease-out", + "focus-visible:outline-primary focus-visible:outline-2 focus-visible:outline-offset-2", + ].join(" "), + active: "text-primary-foreground", + inactive: + "text-muted-foreground hover:bg-foreground/5 hover:text-foreground", + }, + }, + underline: { + tablist: [ + "border-border bg-muted relative flex border-b", + "scrollbar-hide overflow-x-auto md:overflow-x-visible", + ].join(" "), + indicator: [ + "bg-primary pointer-events-none absolute bottom-[-1px] h-px", + "opacity-0", + ].join(" "), + tab: { + base: [ + "relative z-10 flex shrink-0 cursor-pointer items-center gap-2", + "px-5 py-3 text-sm font-medium -tracking-[0.01em] whitespace-nowrap", + "border-r border-border last:border-r-0", + "transition-colors duration-200 ease-out", + "focus-visible:outline-primary focus-visible:outline-2 focus-visible:-outline-offset-2", + ].join(" "), + active: "text-foreground bg-background", + inactive: + "text-muted-foreground hover:bg-background hover:text-foreground", + }, + }, +}; + +const v = variantStyles[variant]; +--- + +
    + + + { + tabs.map((t) => { + const active = t.id === activeId; + return ( + + ); + }) + } +
    + + diff --git a/src/nimbus/components/landing/ViewAllLink.astro b/src/nimbus/components/landing/ViewAllLink.astro new file mode 100644 index 00000000000..ae983c7a487 --- /dev/null +++ b/src/nimbus/components/landing/ViewAllLink.astro @@ -0,0 +1,24 @@ +--- +import { Icon } from "astro-icon/components"; + +interface Props { + href: string; + class?: string; +} + +const { href, class: className = "" } = Astro.props; +--- + + diff --git a/src/nimbus/components/landing/accelerate-data.ts b/src/nimbus/components/landing/accelerate-data.ts new file mode 100644 index 00000000000..19d45001f32 --- /dev/null +++ b/src/nimbus/components/landing/accelerate-data.ts @@ -0,0 +1,79 @@ +// Product data for the Accelerate section. + +export type ProductCategory = "compute" | "ai" | "storage" | "media"; + +export interface AccelerateProduct { + id: string; + product: string; + icon: string; + href: string; + outcome: string; + detail: string; + cta: string; + category?: ProductCategory; +} + +export const products: readonly AccelerateProduct[] = [ + { + id: "dns", + product: "DNS", + icon: "dns", + href: "/dns/", + outcome: "Fast, reliable and resilient DNS queries", + detail: + "World's fastest authoritative DNS, consistently ranked #1 by DNSPerf; free, fully API-managed, DNSSEC supported.", + cta: "Set up Authoritative DNS", + }, + { + id: "smart-shield", + product: "Smart Shield", + icon: "smart-shield", + href: "/smart-shield/", + outcome: "Minimize origin load and accelerate dynamic content", + detail: + "Intelligently manage traffic, optimize content delivery, and safeguard origin infrastructure.", + cta: "Enable Smart Shield", + }, + { + id: "cdn", + product: "CDN", + icon: "cache", + href: "/cache/get-started/", + outcome: + "Default caching for static assets, with cache rules for full control", + detail: + "Caches content in 330+ cities worldwide, with instant purging and granular Cache Rules.", + cta: "Set up Cache Rules", + }, + { + id: "speed", + product: "Speed", + icon: "speed", + href: "/speed/", + outcome: "Assess your site speed and apply recommended optimizations", + detail: + "Application delivery optimizations including minification, Brotli compression, Early Hints, and HTTP/3.", + cta: "Improve your site speed", + }, + { + id: "images", + product: "Images", + icon: "images", + href: "/images/", + outcome: "Transform, optimize, and deliver images worldwide", + detail: + "Cloudflare Images handles format conversion, responsive sizing, and intelligent caching.", + cta: "Optimize image delivery", + category: "media", + }, + { + id: "web-analytics", + product: "Web Analytics", + icon: "web-analytics", + href: "/web-analytics/", + outcome: "Understand the performance of your web pages", + detail: + "Cloudflare Web Analytics collects Core Web Vitals and performance data from 100% of page views without cookies or sampling.", + cta: "Track real user metrics", + }, +]; diff --git a/src/nimbus/components/landing/animation.ts b/src/nimbus/components/landing/animation.ts new file mode 100644 index 00000000000..45d5b6c2dc1 --- /dev/null +++ b/src/nimbus/components/landing/animation.ts @@ -0,0 +1,4 @@ +// Staggered `fade-rise` entrance classes (keyframes in globals.css). +export function stagger(delay: number) { + return `animate-[fade-rise_0.6s_cubic-bezier(0.16,1,0.3,1)_${delay}s_both] motion-reduce:animate-none`; +} diff --git a/src/nimbus/components/landing/sidebar-data.ts b/src/nimbus/components/landing/sidebar-data.ts new file mode 100644 index 00000000000..c29ac84a779 --- /dev/null +++ b/src/nimbus/components/landing/sidebar-data.ts @@ -0,0 +1,239 @@ +// Landing mega-nav structure (sections → links / collapsible groups). + +export interface SidebarLink { + type: "link"; + label: string; + href: string; +} + +export interface SidebarGroup { + type: "group"; + label: string; + /** Product icon name (src/icons/.svg). */ + icon?: string; + collapsed?: boolean; + nodes: SidebarNode[]; +} + +export type SidebarNode = SidebarLink | SidebarGroup; + +export interface SidebarSection { + heading: string; + nodes: SidebarNode[]; +} + +const link = (label: string, href: string): SidebarLink => ({ + type: "link", + label, + href, +}); + +export const sidebarSections: SidebarSection[] = [ + { + heading: "Get started", + nodes: [ + link("Overview", "/"), + link("API", "/api/"), + link("Agent Setup", "/agent-setup/"), + link("Directory", "/directory/"), + link("Changelog", "/changelog/"), + { + type: "group", + label: "Resources", + collapsed: true, + nodes: [ + link( + "Learning paths", + "/resources/?filter-pcx_content_type=learning-path", + ), + link("Use cases", "/use-cases/"), + link("Reference architecture", "/reference-architecture/"), + ], + }, + { + type: "group", + label: "API & IaC", + collapsed: true, + nodes: [ + link("API reference", "/api/"), + link("Terraform", "/terraform/"), + link("Pulumi", "/pulumi/"), + link("SDKs", "/fundamentals/api/reference/sdks/"), + ], + }, + link("Support", "/support/"), + ], + }, + { + heading: "Build", + nodes: [ + { + type: "group", + label: "Compute", + icon: "workers", + collapsed: true, + nodes: [ + link("Workers", "/workers/"), + link("Containers", "/containers/"), + link("Durable Objects", "/durable-objects/"), + link("Queues", "/queues/"), + link("Workflows", "/workflows/"), + link("Browser Run", "/browser-run/"), + link("Workers VPC", "/workers-vpc/"), + link("Cloudflare for Platforms", "/cloudflare-for-platforms/"), + link("Email Service", "/email-service/"), + ], + }, + { + type: "group", + label: "AI", + icon: "workers-ai", + collapsed: true, + nodes: [ + link("Models", "/ai/models/"), + link("Workers AI", "/workers-ai/"), + link("AI Gateway", "/ai-gateway/"), + link("Agents", "/agents/"), + link("Agent Memory", "/agent-memory/"), + link("Sandbox SDK", "/sandbox/"), + link("Vectorize", "/vectorize/"), + link("AI Search", "/ai-search/"), + link("AI Crawl Control", "/ai-crawl-control/"), + ], + }, + { + type: "group", + label: "Storage & Database", + icon: "kv", + collapsed: true, + nodes: [ + link("R2", "/r2/"), + link("R2 Data Catalog", "/r2/data-catalog/"), + link("R2 SQL", "/r2-sql/"), + link("Pipelines", "/pipelines/"), + link("D1", "/d1/"), + link("KV", "/kv/"), + link("Hyperdrive", "/hyperdrive/"), + ], + }, + { + type: "group", + label: "Media", + icon: "images", + collapsed: true, + nodes: [ + link("Images", "/images/"), + link("Stream", "/stream/"), + link("Realtime", "/realtime/"), + ], + }, + ], + }, + { + heading: "Protect & Connect", + nodes: [ + { + type: "group", + label: "Application Security", + icon: "waf", + collapsed: true, + nodes: [ + link("WAF", "/waf/"), + link("DDoS Protection", "/ddos-protection/"), + link("SSL/TLS", "/ssl/"), + link("Bots", "/bots/"), + link("API Shield", "/api-shield/"), + link("Page Shield", "/page-shield/"), + link("Turnstile", "/turnstile/"), + link("Security Center", "/security-center/"), + ], + }, + { + type: "group", + label: "Cloudflare One", + icon: "cloudflare-one", + collapsed: true, + nodes: [ + link("Overview", "/cloudflare-one/"), + link("Insights & Logs", "/cloudflare-one/insights/"), + link("Team & Resources", "/cloudflare-one/team-and-resources/"), + link("Networks", "/cloudflare-one/networks/"), + link("Access controls", "/cloudflare-one/access-controls/"), + link("Traffic policies", "/cloudflare-one/traffic-policies/"), + link( + "Cloud & SaaS findings", + "/cloudflare-one/cloud-and-saas-findings/", + ), + link("Email security", "/cloudflare-one/email-security/"), + link( + "Data loss prevention", + "/cloudflare-one/data-loss-prevention/", + ), + link("Browser isolation", "/cloudflare-one/remote-browser-isolation/"), + link("Reusable components", "/cloudflare-one/reusable-components/"), + link("Integrations", "/cloudflare-one/integrations/"), + ], + }, + { + type: "group", + label: "Domains & DNS", + icon: "dns", + collapsed: true, + nodes: [ + link("DNS", "/dns/"), + link("1.1.1.1", "/1.1.1.1/"), + link("Registrar", "/registrar/"), + link("Email Routing", "/email-service/"), + link("DMARC Management", "/dmarc-management/"), + ], + }, + { + type: "group", + label: "Networking", + icon: "network", + collapsed: true, + nodes: [ + link("Tunnel", "/tunnel/"), + link("Mesh", "/mesh/"), + link("Magic Transit", "/magic-transit/"), + link("Magic WAN", "/cloudflare-wan/"), + link("Network Interconnect", "/network-interconnect/"), + link("Spectrum", "/spectrum/"), + link("BYOIP", "/byoip/"), + ], + }, + { + type: "group", + label: "Delivery & Performance", + icon: "speed", + collapsed: true, + nodes: [ + link("Cache", "/cache/"), + link("Speed", "/speed/"), + link("Load Balancing", "/load-balancing/"), + link("Waiting Room", "/waiting-room/"), + link("Argo Smart Routing", "/argo-smart-routing/"), + link("Zaraz", "/zaraz/"), + ], + }, + ], + }, + { + heading: "Manage & Observe", + nodes: [ + { + type: "group", + label: "Observe", + icon: "analytics", + collapsed: true, + nodes: [ + link("Analytics", "/analytics/"), + link("Web Analytics", "/web-analytics/"), + link("Logs", "/logs/"), + link("Log Explorer", "/log-explorer/"), + link("Health Checks", "/health-checks/"), + ], + }, + ], + }, +]; diff --git a/src/nimbus/components/models/CopyableCode.astro b/src/nimbus/components/models/CopyableCode.astro new file mode 100644 index 00000000000..b06cbaa2e89 --- /dev/null +++ b/src/nimbus/components/models/CopyableCode.astro @@ -0,0 +1,75 @@ +--- +/** + * CopyableCode — copy affordance for the model ID: the id is shown as `` + * inside the button, the label is a `title`/`aria-label` tooltip, and success + * is shown by swapping the copy icon for a checkmark. + */ +interface Props { + text: string; + label?: string; +} + +const { text, label = "Copy" } = Astro.props; +--- + + + + + + diff --git a/src/nimbus/components/models/ExampleCard.astro b/src/nimbus/components/models/ExampleCard.astro new file mode 100644 index 00000000000..2a9fbf8ea21 --- /dev/null +++ b/src/nimbus/components/models/ExampleCard.astro @@ -0,0 +1,215 @@ +--- +/** + * ExampleCard — renders one example: a code snippet block (single `` or + * `` of languages) plus its rendered output (text / media) and/or raw + * JSON response. + */ +import { Code } from "~/components/ui/code"; +import { Tabs, TabItem } from "~/components/ui/tabs"; +import OutputBody from "~/components/models/OutputBody.astro"; +import type { ModelExample, CodeSnippet } from "~/util/models"; + +interface Props { + example: ModelExample; + fallbackSnippets?: CodeSnippet[]; + layout?: "stack" | "side-by-side"; +} + +const { example, fallbackSnippets, layout = "side-by-side" } = Astro.props; + +const snippets = example.code_snippets ?? fallbackSnippets ?? []; +const hasSnippets = snippets.length > 0; + +const output = example.output; + +// Text output (e.g. LLM responses): { text: "..." } or { response: "..." }. +// Some examples provide streaming-style chunks as a string array. +function toTextOutput(value: unknown): string | undefined { + if (typeof value === "string") return value; + if (Array.isArray(value)) { + return value.filter((v): v is string => typeof v === "string").join(""); + } + return undefined; +} + +const textOutput = toTextOutput(output?.text ?? output?.response); + +const rawImageUrl = output?.image ?? output?.image_url; +const imageUrl = typeof rawImageUrl === "string" ? rawImageUrl : undefined; + +// Some models return an `images` array of URLs. +const rawImages = output?.images; +const imageUrls = Array.isArray(rawImages) + ? rawImages.filter((v): v is string => typeof v === "string") + : []; + +const rawVideoUrl = output?.video ?? output?.video_url; +const videoUrl = typeof rawVideoUrl === "string" ? rawVideoUrl : undefined; + +const rawAudioUrl = output?.audio ?? output?.audio_url; +const audioUrl = typeof rawAudioUrl === "string" ? rawAudioUrl : undefined; + +const hasImage = !!imageUrl || imageUrls.length > 0; +const hasVideo = !!videoUrl; +const hasAudio = !!audioUrl; +const hasText = !!textOutput; +const hasMedia = hasImage || hasVideo || hasAudio; + +// The Output tab has renderable content if we detected any known shape above. +const hasOutput = hasText || hasMedia; +const hasRawResponse = !!example.raw_response; +const hasResponse = hasOutput || hasRawResponse; + +const rawResponseJson = hasRawResponse + ? JSON.stringify(example.raw_response, null, 2) + : ""; + +// Only enable the side-by-side layout when a code block and renderable media +// are both present — text-only or JSON-only responses stack better vertically. +const sideBySide = + layout === "side-by-side" && hasSnippets && hasResponse && hasMedia; + +const languageMap: Record = { + typescript: "ts", + javascript: "js", + python: "python", + curl: "bash", + bash: "bash", + shell: "bash", + cURL: "bash", +}; + +const displayNames: Record = { + typescript: "TypeScript", + javascript: "JavaScript", + python: "Python", + curl: "cURL", + cURL: "cURL", + bash: "Bash", +}; + +// Returns a Shiki language id. Cast to `any` at call sites because Astro's +// `Code` types `lang` as the literal `CodeLanguage` union, while these labels +// are resolved dynamically from the catalog data. +function getLang(label: string): any { + return languageMap[label] ?? languageMap[label.toLowerCase()] ?? "txt"; +} + +function getLabel(label: string): string { + return displayNames[label] ?? displayNames[label.toLowerCase()] ?? label; +} +--- + +
    + { + hasSnippets && ( +
    + {snippets.length === 1 ? ( + + ) : ( + + {snippets.map((snippet) => ( + + + + ))} + + )} +
    + ) + } + + { + hasResponse && ( +
    + {hasOutput && hasRawResponse ? ( + + + + + +
    + +
    +
    +
    + ) : hasOutput ? ( + + ) : ( +
    + +
    + )} +
    + ) + } + + { + !hasResponse && !hasSnippets && example.input && ( + + ) + } +
    + + diff --git a/src/nimbus/components/models/ModelBadges.astro b/src/nimbus/components/models/ModelBadges.astro new file mode 100644 index 00000000000..37df5f4b881 --- /dev/null +++ b/src/nimbus/components/models/ModelBadges.astro @@ -0,0 +1,66 @@ +--- +/** + * ModelBadges — provider + capability + deprecation badges for a model: + * - provider badge (Cloudflare-hosted / Third-party) — `default` + * - capability badges — `default` (model) / `caution` (platform) + * - deprecation — `danger` (Deprecated / Planned deprecation) + */ +import { Badge } from "~/components/ui/badge"; +import { CAPABILITY_PROPERTIES, type ModelCardData } from "~/util/models"; + +interface Props { + model: ModelCardData; +} + +const { model } = Astro.props; + +type BadgeDef = { + variant: "default" | "caution" | "danger"; + text: string; + /** Supplementary tooltip — only the ZDR badge carries one (zdr_comment). */ + title?: string; +}; + +// Provider badge — always first. +const providerBadge: BadgeDef = { + variant: "default", + text: model.hosting === "proxied" ? "Third-party" : "Cloudflare-hosted", +}; + +// Property badges — iterate the ordered property list so capability + +// deprecation badges appear in data-array order (deprecation interleaved where +// it appears, not forced first). +const propertyBadges: BadgeDef[] = []; +for (const { property_id, value } of model.propertiesList) { + if (property_id in CAPABILITY_PROPERTIES && value === "true") { + const def = CAPABILITY_PROPERTIES[property_id]; + const badge: BadgeDef = { + variant: def.category === "platform" ? "caution" : "default", + text: def.label, + }; + // ZDR badge carries the catalog `zdr_comment` as a tooltip. + if (property_id === "zdr" && model.zdrComment) { + badge.title = model.zdrComment; + } + propertyBadges.push(badge); + } else if (property_id === "planned_deprecation_date") { + const ts = Math.floor(new Date(value as string).getTime()); + propertyBadges.push({ + variant: "danger", + text: Date.now() > ts ? "Deprecated" : "Planned deprecation", + }); + } +} + +const badges: BadgeDef[] = [providerBadge, ...propertyBadges]; +--- + +
      + { + badges.map((badge) => ( +
    • + +
    • + )) + } +
    diff --git a/src/nimbus/components/models/ModelCard.astro b/src/nimbus/components/models/ModelCard.astro new file mode 100644 index 00000000000..c77a8913369 --- /dev/null +++ b/src/nimbus/components/models/ModelCard.astro @@ -0,0 +1,129 @@ +--- +/** + * ModelCard — one model as a flat grid cell, sharing the Directory shell + * (grid.ts): bg-background, 1px self-closing borders, rounded corner-mark + * squares at line intersections. Content hierarchy: logo + 18px title anchor, + * a 12px meta row (author • model type), 14px muted description, badges. + * + * The cell carries the `data-facet-*` / `data-name` / `data-date` attributes the + * catalog controller (models.client.ts) reads to filter, sort, and re-flow the + * corner marks. `index`/`cols` seed the build-time corner marks. + */ +import { Badge } from "~/components/ui/badge"; +import ModelBadges from "./ModelBadges.astro"; +import { authorData } from "~/components/models/data"; +import { facetValues, pinnedModelNames, type ModelCardData } from "~/util/models"; +import { + cellClass, + cornersFor, + cornerSpansHTML, +} from "~/components/directory/grid"; + +interface Props { + model: ModelCardData; + index: number; + cols: number; + basePath?: string; +} + +const { model, index, cols, basePath = "/ai/models" } = Astro.props; +// Card href: short last-path-segment slug under /workers-ai (legacy URLs), +// else the full model id/slug (multi-segment under /ai). +const slugSeg = basePath === "/workers-ai/models" ? model.shortName : model.name; +const href = `${basePath}/${slugSeg}/`; +const dateValue = model.createdAt ? new Date(model.createdAt).getTime() : 0; +// Pinned-first sort tier: -1 when not pinned. +const pinnedIndex = pinnedModelNames.indexOf(model.name); +const isPinned = pinnedIndex >= 0; +// Author logo; initial-letter tile when the author id is unmapped. +const authorLogo = authorData[model.author]?.logo; +--- + +
    + diff --git a/src/nimbus/components/models/ModelCatalog.astro b/src/nimbus/components/models/ModelCatalog.astro new file mode 100644 index 00000000000..0b67687c119 --- /dev/null +++ b/src/nimbus/components/models/ModelCatalog.astro @@ -0,0 +1,152 @@ +--- +/** + * ModelCatalog — filterable AI model catalog, rendered inside the docs layout. + * A top toolbar (search + Task Types / Capabilities / Providers? / Authors + * multi-select dropdowns + sort), a "We found N models" count with clear, and a + * responsive card grid. A vanilla controller (models.client.ts) drives it. + * + * The host page resolves the model slice (`getLegacyModels` / + * `getResolvedModels` → `toModelCardData`) and passes it in. `basePath` selects + * the per-model URL prefix and the card href slug form. The Providers facet is + * computed internally — it appears only when the slice spans more than one + * `hosting` value. + */ +import { Icon } from "astro-icon/components"; +import ModelCard from "./ModelCard.astro"; +import { getFacets, type ModelCardData } from "~/util/models"; +import { resolveCols, LG_GRID_CLASS } from "~/components/directory/grid"; + +interface Props { + models: ModelCardData[]; + basePath?: string; +} + +const { models, basePath = "/ai/models" } = Astro.props; +const facets = getFacets(models); +const cols = resolveCols(models.length); +--- + +
    + {/* Toolbar */} +
    + {/* Search */} +
    + + +
    + + {/* Filter dropdowns + sort */} +
    + { + facets.map((facet) => ( +
    + + {facet.label} + +
    + {/* Within-facet search */} + +
    + {facet.options.map((option) => ( + + ))} + +
    +
    +
    + )) + } + + + +
    +
    + + {/* Count + clear */} +
    + + We found {models.length} + {models.length === 1 ? " model" : " models"} + + +
    + + {/* Grid */} + + +
    +
    + {models.map((model, i) => )} +
    +
    +
    + + diff --git a/src/nimbus/components/models/ModelDetailPage.astro b/src/nimbus/components/models/ModelDetailPage.astro new file mode 100644 index 00000000000..223aac61357 --- /dev/null +++ b/src/nimbus/components/models/ModelDetailPage.astro @@ -0,0 +1,607 @@ +--- +/** + * ModelDetailPage — a single AI model page, shared by the `/workers-ai/models` + * and `/ai/models` per-model routes, rendered through DocsLayout. Sections: + * author logo + heading, copyable model ID, banner, the Model Info table, + * Playground, Usage (ExampleCard for catalog / per-task `code/*` for legacy), + * the Examples list, the recursive Parameters (multi-mode loop + top-level + * variant tabs driving both Input/Output), and raw schema links. + * + * Props: + * - model: the resolved ModelView (full, with schema). + * - basePath: "/workers-ai/models" | "/ai/models" — URL prefix. + * - slug: the URL slug segment(s) under basePath. + */ +import type { MarkdownHeading } from "astro"; +import { getRouteNavigation } from "nimbus-docs"; +import DocsLayout from "~/layouts/DocsLayout.astro"; +import { Badge } from "~/components/ui/badge"; +import { Code } from "~/components/ui/code"; +import { Tabs, TabItem } from "~/components/ui/tabs"; +import { LinkButton } from "~/components/ui/link-button"; +import { Aside } from "~/components/ui/aside"; +import { PageActions } from "~/components/ui/page-actions"; +import ModelBadges from "~/components/models/ModelBadges.astro"; +import ModelFeatures from "~/components/models/ModelFeatures.astro"; +import CopyableCode from "~/components/models/CopyableCode.astro"; +import ExampleCard from "~/components/models/ExampleCard.astro"; +import SchemaDisplay from "~/components/models/SchemaDisplay.astro"; +import SchemaVariantTabs from "~/components/models/SchemaVariantTabs.tsx"; +import { authorData } from "~/components/models/data"; +import { + detectApiModes, + getTopLevelVariants, + type ModelView, +} from "~/util/models"; + +// Legacy per-task code-example components (only the ones our slice needs). +import TextGenerationCode from "~/components/models/code/TextGenerationCode.astro"; +import SummarizationCode from "~/components/models/code/SummarizationCode.astro"; +import TextEmbeddingCode from "~/components/models/code/TextEmbeddingCode.astro"; +import ObjectDetectionCode from "~/components/models/code/ObjectDetectionCode.astro"; +import TextClassificationCode from "~/components/models/code/TextClassificationCode.astro"; +import TranslationCode from "~/components/models/code/TranslationCode.astro"; +import DeepgramAura from "~/components/models/code/DeepgramAura.astro"; +import BgeRerankerBase from "~/components/models/code/Bge-Reranker-Base.astro"; +import Flux1Schnell from "~/components/models/code/Flux-1-Schnell.astro"; + +interface Props { + model: ModelView; + basePath: string; + slug: string; +} + +const { model, basePath, slug } = Astro.props; + +// Display name: catalog displayName, or fall back to last segment of model ID. +const displayName = + model.displayName !== model.slug + ? model.displayName + : (model.name.split("/").at(-1) ?? model.name); + +// For catalog models, the first example is the "Usage" section; the rest go +// into the Examples disclosure list. +const firstExample = model.examples?.[0]; +const remainingExamples = model.examples?.slice(1) ?? []; +const hasCatalogUsage = !!firstExample?.code_snippets?.length; + +// Legacy code-example overrides (only used when there is no catalog usage). +let CodeExamples: any = null; +if (!hasCatalogUsage) { + switch (model.task) { + case "Text Generation": + CodeExamples = TextGenerationCode; + break; + case "Object Detection": + CodeExamples = ObjectDetectionCode; + break; + case "Translation": + CodeExamples = TranslationCode; + break; + case "Summarization": + CodeExamples = SummarizationCode; + break; + case "Text Embeddings": + CodeExamples = TextEmbeddingCode; + break; + case "Text Classification": + CodeExamples = TextClassificationCode; + break; + } + + // Model-specific overrides (by exact model id). + if (model.name === "@cf/black-forest-labs/flux-1-schnell") { + CodeExamples = Flux1Schnell; + } + if (model.name === "@cf/baai/bge-reranker-base") { + CodeExamples = BgeRerankerBase; + } + if (model.name === "@cf/deepgram/aura-1") { + CodeExamples = DeepgramAura; + } +} + +const hasCodeExamples = hasCatalogUsage || !!CodeExamples; +const hasRemainingExamples = remainingExamples.length > 0; + +const description = model.description; +const isBeta = model.beta; + +// Map the banner `severity` onto the four Aside types; unknown severities fall +// back to neutral "note". +const banner = model.banner ?? null; +function bannerAsideType( + severity: string, +): "note" | "tip" | "caution" | "danger" { + switch (severity.toLowerCase()) { + case "info": + return "tip"; + case "warning": + return "caution"; + case "danger": + case "error": + return "danger"; + default: + return "note"; + } +} + +// Playground: Text Generation + Cloudflare-hosted, with a defensive gpt-oss +// exclusion (gpt-oss is not in our slice — kept as a documented dead branch). +let hasPlayground = model.task === "Text Generation" && model.hosting === "hosted"; +if (model.name.includes("@cf/openai/gpt-oss")) { + hasPlayground = false; +} + +const author = authorData[model.author]; +const authorName = model.authorName; + +// `hasSchema`: input must have at least one key. +const hasSchema = Object.keys(model.schema.input).length > 0; +const apiModes = detectApiModes(model.schema); +const allInputsIdentical = + apiModes && + apiModes.every( + (mode) => JSON.stringify(mode.input) === JSON.stringify(apiModes[0].input), + ); + +// Top-level request-format variants (single-mode branch). Hoist a union of +// titled branches so a single selector drives both Input + Output panes. +const inputVariants = getTopLevelVariants(model.schema.input) ?? []; +const outputVariants = + model.schema.output && model.schema.output.format !== "binary" + ? (getTopLevelVariants(model.schema.output) ?? []) + : []; +const variantTitles: string[] = []; +for (const v of [...inputVariants, ...outputVariants]) { + if (!variantTitles.includes(v.title)) variantTitles.push(v.title); +} +const activeVariant = variantTitles[0]; + +const outputIsBinary = model.schema.output.format === "binary"; +const outputContentType = model.schema.output.contentType as string | undefined; + +// Raw-schema endpoint links, matching the [...schema].json.ts route: per-mode +// `{mode.id}-input/-output` when modes exist, else `schema-input/-output`. +const schemaBasePath = `${basePath}/${slug}`; +const rawLinks = apiModes + ? apiModes.flatMap((mode) => [ + { name: mode.name, kind: "Input", file: `${mode.id}-input` }, + { name: mode.name, kind: "Output", file: `${mode.id}-output` }, + ]) + : [ + { name: "Input", kind: null, file: "schema-input" }, + { name: "Output", kind: null, file: "schema-output" }, + ]; + +// Navigation for this catalog leaf: the sidebar marks the Models section +// active and the breadcrumb resolves to the section (the model name is the +//

    , not a crumb). The leaf is never injected into the nav tree. +// +// The `/ai` section lands on `/ai/models/`, so its ancestry collapses to +// "AI"; append a "Models" crumb so the trail reads Home › AI › Models. The +// `/workers-ai/models/` section already resolves to "Workers AI › Models". +const trail = basePath === "/ai/models" ? [{ label: "Models" }] : []; +const { sidebar, breadcrumbs } = await getRouteNavigation({ + path: `${basePath}/${slug}/`, + section: `${basePath}/`, + collection: "docs", + prevNext: false, + trail, +}); + +const markdownPath = `${basePath}/${slug}.md`; +const markdownUrl = Astro.site + ? new URL(markdownPath, Astro.site).href + : markdownPath; + +// `` carries displayName + author; the layout appends the site suffix. +const pageTitle = `${displayName} (${authorName})`; + +const headings: MarkdownHeading[] = [ + hasPlayground && { depth: 2, slug: "Playground", text: "Playground" }, + hasCodeExamples && { depth: 2, slug: "Usage", text: "Usage" }, + hasRemainingExamples && { depth: 2, slug: "Examples", text: "Examples" }, + hasSchema && { depth: 2, slug: "Parameters", text: "Parameters" }, + hasSchema && { depth: 2, slug: "API-Schemas", text: "API Schemas (Raw)" }, +].filter(Boolean) as MarkdownHeading[]; +--- + +<DocsLayout + title={pageTitle} + description={description} + sidebar={sidebar} + headings={headings} + breadcrumbs={breadcrumbs} + markdownUrl={markdownUrl} + prevNext={{}} + collection="docs" +> + <Fragment slot="pagination" /> + + <div slot="page-title"> + <div class="not-prose mt-6 flex items-center gap-4"> + { + author ? ( + <img + class="h-14 w-14 shrink-0 rounded-sm bg-card object-contain dark:bg-gray-200 dark:p-1" + src={author.logo} + alt={`${authorName} logo`} + /> + ) : ( + <span + aria-hidden="true" + class="flex h-14 w-14 shrink-0 items-center justify-center rounded-md bg-accent text-2xl font-black text-muted-foreground uppercase" + > + {model.author.charAt(0)} + </span> + ) + } + <div class="min-w-0"> + <div class="flex flex-wrap items-center gap-3"> + <h1 + class="mb-0 leading-tight text-foreground" + style={`font-size:var(--nb-h1-size);font-weight:var(--nb-h1-weight);letter-spacing:var(--nb-h1-tracking)`} + > + {displayName} + </h1> + {isBeta && <Badge text="Beta" variant="caution" size="medium" />} + </div> + <p class="mt-1 mb-0 text-sm text-muted-foreground"> + {model.task} • {authorName} + </p> + </div> + </div> + <PageActions markdownUrl={markdownUrl} class="mt-3" /> + <div class="mt-3 mb-6 h-px w-full bg-border" role="none"></div> + </div> + + <div class="not-prose mb-4"> + <CopyableCode text={model.name} label="Copy model ID" /> + </div> + + <div class="not-prose mb-4"><ModelBadges model={model} /></div> + + <p>{description}</p> + + { + banner && ( + <Aside type={bannerAsideType(banner.severity)}> + <p> + {banner.title && <strong>{banner.title}: </strong>} + {banner.text} + {banner.link && ( + <Fragment> + {" "} + <a + href={banner.link.url} + target="_blank" + rel="noopener noreferrer" + aria-label={`${banner.link.label} (${displayName})`} + > + {banner.link.label} + <span class="external-link"> ↗</span> + </a> + </Fragment> + )} + </p> + </Aside> + ) + } + + { + /* Defensive dead branch (model not in slice): Llama 3.2 11b license note. */ + model.name === "@cf/meta/llama-3.2-11b-vision-instruct" && ( + <Aside> + <p> + To use Llama 3.2 11b Vision Instruct, you need to agree to the{" "} + <a href="https://github.com/meta-llama/llama-models/blob/main/models/llama3_2/LICENSE"> + Meta License + </a>{" "} + and{" "} + <a href="https://github.com/meta-llama/llama-models/blob/main/models/llama3_2/USE_POLICY.md"> + Acceptable Use Policy + </a> + . To do so, please send an initial request to + <code>@cf/meta/llama-3.2-11b-vision-instruct</code> with + <code>"prompt" : "agree"</code>. After that, you'll be able to use the + model as normal. + </p> + <Code + code={`curl https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/ai/run/@cf/meta/llama-3.2-11b-vision-instruct \\ + -X POST \\ + -H "Authorization: Bearer $CLOUDFLARE_AUTH_TOKEN" \\ + -d '{ "prompt": "agree"}'`} + lang="sh" + /> + </Aside> + ) + } + + <ModelFeatures model={model} /> + + { + /* Defensive dead branch (model not in slice): nova-3 transport pricing. */ + model.name === "@cf/deepgram/nova-3" && ( + <Aside> + <p> + The <a href="/workers-ai/platform/pricing">pricing of this model</a> is + different based on transport. Transport-based pricing does not apply to + all models. + </p> + <ul> + <li> + WebSocket: $0.0092 per audio minute output (836.36 neurons per audio + minute output) + </li> + <li> + Regular HTTP: $0.0052 per audio minute output (472.73 neurons per + audio minute output) + </li> + </ul> + </Aside> + ) + } + + { + hasPlayground && ( + <Fragment> + <h2 id="Playground">Playground</h2> + <p> + Try out this model with Workers AI LLM Playground. It does not require + any setup or authentication and is an instant way to preview and test a + model directly in the browser. + </p> + <LinkButton href={`https://playground.ai.cloudflare.com/?model=${model.name}`}> + Launch the LLM Playground + </LinkButton> + </Fragment> + ) + } + + { + hasCatalogUsage && firstExample && ( + <Fragment> + <h2 id="Usage">Usage</h2> + <ExampleCard example={firstExample} layout="stack" /> + </Fragment> + ) + } + + { + !hasCatalogUsage && CodeExamples && ( + <Fragment> + <h2 id="Usage">Usage</h2> + <CodeExamples name={model.name} lora={false} /> + </Fragment> + ) + } + + { + hasRemainingExamples && ( + <Fragment> + <h2 id="Examples">Examples</h2> + {remainingExamples.map((example) => ( + <details> + <summary> + <strong>{example.name}</strong> + {example.description && <span> — {example.description}</span>} + </summary> + <ExampleCard example={example} /> + </details> + ))} + </Fragment> + ) + } + + { + hasSchema && apiModes && apiModes.length > 1 ? ( + <Fragment> + <h2 id="Parameters">Parameters</h2> + + {allInputsIdentical ? ( + <Fragment> + <h3 class="mt-4! mb-2! text-base font-semibold">Input</h3> + <SchemaDisplay schema={apiModes[0].input} title="Input" /> + + <h3 class="mt-6! mb-2! text-base font-semibold">Output</h3> + <div class="not-prose space-y-3"> + {apiModes.map((mode) => ( + <details class="group rounded-lg border border-border py-3"> + <summary class="cursor-pointer px-8 font-medium text-foreground"> + <span>{mode.name}</span> + {mode.description && ( + <span class="text-sm font-normal text-muted-foreground"> + {" "} + — {mode.description} + </span> + )} + </summary> + <div class="border-t border-border p-4 pb-1"> + <SchemaDisplay schema={mode.output} title="Output" /> + </div> + </details> + ))} + </div> + </Fragment> + ) : ( + <div class="not-prose space-y-3"> + {apiModes.map((mode) => ( + <details class="group rounded-lg border border-border py-3"> + <summary class="cursor-pointer px-8 font-medium text-foreground"> + <span>{mode.name}</span> + {mode.description && ( + <span class="text-sm font-normal text-muted-foreground"> + {" "} + — {mode.description} + </span> + )} + </summary> + <div class="border-t border-border p-4 pb-1"> + <Tabs syncKey={`schema-${mode.id}`}> + <TabItem label="Input"> + <SchemaDisplay schema={mode.input} title="Input" /> + </TabItem> + <TabItem label="Output"> + <SchemaDisplay schema={mode.output} title="Output" /> + </TabItem> + </Tabs> + </div> + </details> + ))} + </div> + )} + </Fragment> + ) : ( + hasSchema && ( + <Fragment> + <h2 id="Parameters">Parameters</h2> + {variantTitles.length > 0 ? ( + <div data-variant-section data-active-variant={activeVariant}> + <SchemaVariantTabs titles={variantTitles} client:visible /> + <Tabs syncKey="schema-tab"> + <TabItem label="Input"> + {variantTitles.map((title) => ( + <div data-variant-pane={title} hidden={title !== activeVariant}> + <SchemaDisplay + schema={model.schema.input} + title="Input" + variantTitle={title} + /> + </div> + ))} + </TabItem> + <TabItem label="Output"> + {outputIsBinary ? ( + <p> + {outputContentType?.startsWith("audio/") ? ( + <Fragment> + The binding returns a <code>ReadableStream</code> with + the audio in{" "} + {outputContentType.replace("audio/", "").toUpperCase()}{" "} + format (check the model's output schema). + </Fragment> + ) : ( + <Fragment> + The binding returns a <code>ReadableStream</code> with + the output (check the model's output schema). + </Fragment> + )} + </p> + ) : ( + variantTitles.map((title) => ( + <div + data-variant-pane={title} + hidden={title !== activeVariant} + > + <SchemaDisplay + schema={model.schema.output} + title="Output" + variantTitle={title} + /> + </div> + )) + )} + </TabItem> + </Tabs> + </div> + ) : ( + <Tabs syncKey="schema-tab"> + <TabItem label="Input"> + <SchemaDisplay schema={model.schema.input} title="Input" /> + </TabItem> + <TabItem label="Output"> + {outputIsBinary ? ( + <p> + {outputContentType?.startsWith("audio/") ? ( + <Fragment> + The binding returns a <code>ReadableStream</code> with the + audio in{" "} + {outputContentType.replace("audio/", "").toUpperCase()}{" "} + format (check the model's output schema). + </Fragment> + ) : ( + <Fragment> + The binding returns a <code>ReadableStream</code> with the + output (check the model's output schema). + </Fragment> + )} + </p> + ) : ( + <SchemaDisplay schema={model.schema.output} title="Output" /> + )} + </TabItem> + </Tabs> + )} + </Fragment> + ) + ) + } + + { + hasSchema && ( + <Fragment> + <h2 id="API-Schemas">API Schemas (Raw)</h2> + <div class="not-prose grid grid-cols-1 gap-2 sm:grid-cols-2"> + {rawLinks.map((link) => ( + <div class="flex items-center justify-between rounded-lg border border-border px-4 py-1.5"> + <span class="text-sm"> + <span class="font-medium text-foreground">{link.name}</span> + {link.kind && ( + <span class="ml-2 text-muted-foreground">{link.kind}</span> + )} + </span> + <span class="flex items-center gap-1"> + <a + href={`${schemaBasePath}/${link.file}.json`} + target="_blank" + rel="noopener noreferrer" + title="Open" + class="flex h-7 w-7 items-center justify-center rounded text-muted-foreground no-underline hover:bg-accent hover:text-foreground" + > + <svg + xmlns="http://www.w3.org/2000/svg" + width="14" + height="14" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + > + <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /> + <polyline points="15 3 21 3 21 9" /> + <line x1="10" y1="14" x2="21" y2="3" /> + </svg> + </a> + <a + href={`${schemaBasePath}/${link.file}.json`} + download={`${link.file}.json`} + title="Download" + class="flex h-7 w-7 items-center justify-center rounded text-muted-foreground no-underline hover:bg-accent hover:text-foreground" + > + <svg + xmlns="http://www.w3.org/2000/svg" + width="14" + height="14" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + > + <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> + <polyline points="7 10 12 15 17 10" /> + <line x1="12" y1="15" x2="12" y2="3" /> + </svg> + </a> + </span> + </div> + ))} + </div> + </Fragment> + ) + } +</DocsLayout> diff --git a/src/nimbus/components/models/ModelFeatures.astro b/src/nimbus/components/models/ModelFeatures.astro new file mode 100644 index 00000000000..1ad3b87ca6a --- /dev/null +++ b/src/nimbus/components/models/ModelFeatures.astro @@ -0,0 +1,213 @@ +--- +/** + * ModelFeatures — the per-model "Model Info" table. Rows render in a fixed + * order: deprecation → context window → terms → info → max input tokens → + * output dimensions → function calling → reasoning → vision → zdr → lora → + * beta → Batch → request formats → partner → realtime → unit price → + * dashboard pricing link. + */ +import type { ModelView } from "~/util/models"; + +interface Props { + model: ModelView; +} + +const { model } = Astro.props; + +const nf = new Intl.NumberFormat("en-US"); +const currencyFormatter = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + maximumFractionDigits: 10, +}); + +// Flatten the property list into a lookup (values are strings). +const properties: Record<string, any> = {}; +for (const { property_id, value } of model.propertiesList) { + properties[property_id] = value; +} + +const isCatalog = model.dataSource === "catalog"; +// Catalog dashboard pricing deep link; model_id is "provider/name". +const dashPricingUrl = + isCatalog && model.modelId + ? `https://dash.cloudflare.com/?to=/:account/ai/models/${model.modelId}` + : null; + +// `requestFormats` lives on the resolved model (catalog-only) rather than the +// properties array because Property.value is string-only. Identifiers like +// "chat-completions" are title-cased for display. +const requestFormats = isCatalog ? (model.requestFormats ?? null) : null; +const formatRequestFormat = (format: string): string => + format + .split(/[-_]/) + .map((part) => (part ? part[0].toUpperCase() + part.slice(1) : part)) + .join(" "); + +const hasProps = Object.keys(properties).length > 0; + +const price = Array.isArray(properties.price) + ? (properties.price as { price: number; unit: string }[]) + : null; +--- + +{ + hasProps && ( + <table> + <thead> + <tr> + <th>Model Info</th> + <th /> + </tr> + </thead> + <tbody> + {properties.planned_deprecation_date && ( + <tr> + <td> + {Date.now() > + Math.floor( + new Date(properties.planned_deprecation_date).getTime(), + ) + ? "Deprecated" + : "Planned Deprecation"} + </td> + <td> + {new Date( + properties.planned_deprecation_date, + ).toLocaleDateString("en-US")} + </td> + </tr> + )} + {properties.context_window && ( + <tr> + <td> + Context Window + <a href="/workers-ai/glossary/"> + <span class="external-link"> ↗</span> + </a> + </td> + <td>{nf.format(Number(properties.context_window))} tokens</td> + </tr> + )} + {properties.terms && ( + <tr> + <td>Terms and License</td> + <td> + <a href={properties.terms} target="_blank"> + link<span class="external-link"> ↗</span> + </a> + </td> + </tr> + )} + {properties.info && ( + <tr> + <td>More information</td> + <td> + <a href={properties.info} target="_blank"> + link<span class="external-link"> ↗</span> + </a> + </td> + </tr> + )} + {properties.max_input_tokens && ( + <tr> + <td>Maximum Input Tokens</td> + <td>{nf.format(Number(properties.max_input_tokens))}</td> + </tr> + )} + {properties.output_dimensions && ( + <tr> + <td>Output Dimensions</td> + <td>{nf.format(Number(properties.output_dimensions))}</td> + </tr> + )} + {properties.function_calling && ( + <tr> + <td> + Function calling{" "} + <a href="/workers-ai/function-calling"> + <span class="external-link"> ↗</span> + </a> + </td> + <td>Yes</td> + </tr> + )} + {properties.reasoning && ( + <tr> + <td>Reasoning</td> + <td>Yes</td> + </tr> + )} + {properties.vision && ( + <tr> + <td>Vision</td> + <td>Yes</td> + </tr> + )} + {properties.zdr && ( + <tr> + <td>Zero data retention</td> + <td>Yes</td> + </tr> + )} + {properties.lora && ( + <tr> + <td>LoRA</td> + <td>Yes</td> + </tr> + )} + {properties.beta && ( + <tr> + <td>Beta</td> + <td>Yes</td> + </tr> + )} + {properties.async_queue && ( + <tr> + <td>Batch</td> + <td>Yes</td> + </tr> + )} + {requestFormats && requestFormats.length > 0 && ( + <tr> + <td>Request formats</td> + <td>{requestFormats.map(formatRequestFormat).join(", ")}</td> + </tr> + )} + {properties.partner && ( + <tr> + <td>Partner</td> + <td>Yes</td> + </tr> + )} + {properties.realtime && ( + <tr> + <td>Real-time</td> + <td>Yes</td> + </tr> + )} + {price && price.length > 0 && ( + <tr> + <td>Unit Pricing</td> + <td> + {price + .map((p) => `${currencyFormatter.format(p.price)} ${p.unit}`) + .join(", ")} + </td> + </tr> + )} + {dashPricingUrl && ( + <tr> + <td>Pricing</td> + <td> + <a href={dashPricingUrl} target="_blank" rel="noreferrer"> + View pricing in the Cloudflare dashboard + <span class="external-link"> ↗</span> + </a> + </td> + </tr> + )} + </tbody> + </table> + ) +} diff --git a/src/nimbus/components/models/OutputBody.astro b/src/nimbus/components/models/OutputBody.astro new file mode 100644 index 00000000000..56404da9f7d --- /dev/null +++ b/src/nimbus/components/models/OutputBody.astro @@ -0,0 +1,66 @@ +--- +/** OutputBody — renders an example's output (text / image(s) / video / audio). */ +interface Props { + alt: string; + textOutput?: string; + imageUrl?: string; + imageUrls?: string[]; + videoUrl?: string; + audioUrl?: string; +} + +const { + alt, + textOutput, + imageUrl, + imageUrls = [], + videoUrl, + audioUrl, +} = Astro.props; +--- + +<div class="example-output-body"> + {textOutput && <pre class="example-text">{textOutput}</pre>} + {imageUrl && <img src={imageUrl} alt={alt} loading="lazy" />} + {imageUrls.map((url) => <img src={url} alt={alt} loading="lazy" />)} + { + /* AI-generated outputs do not ship caption tracks. */ + videoUrl && <video src={videoUrl} controls preload="metadata" /> + } + {audioUrl && <audio src={audioUrl} controls preload="metadata" />} +</div> + +<style> + .example-output-body { + display: flex; + flex-direction: column; + gap: 0.75rem; + } + + .example-text { + margin: 0; + padding: 0.75rem; + border: 1px solid var(--nb-border); + border-radius: 0.5rem; + background: var(--nb-muted); + color: var(--nb-foreground); + font-family: var(--nb-font-mono); + font-size: 0.875rem; + line-height: 1.5; + white-space: pre-wrap; + word-break: break-word; + max-height: 32rem; + overflow: auto; + } + + .example-output-body :global(img), + .example-output-body :global(video) { + width: 100%; + border-radius: 0.5rem; + border: 1px solid var(--nb-border); + } + + .example-output-body :global(audio) { + width: 100%; + } +</style> diff --git a/src/nimbus/components/models/SchemaDisplay.astro b/src/nimbus/components/models/SchemaDisplay.astro new file mode 100644 index 00000000000..4d0d909ba5b --- /dev/null +++ b/src/nimbus/components/models/SchemaDisplay.astro @@ -0,0 +1,429 @@ +--- +/** + * SchemaDisplay — schema display for model parameters. Walks the JSON Schema at + * build time (allOf merge, nullable collapse, $ref resolve, flat-schema rows) + * into `SchemaRowData`, then hands off to the interactive `SchemaTree` / + * `SchemaVariantSelector` islands. Handles top-level anyOf/oneOf variants. + */ +import { + SchemaTree, + SchemaCombinerName, + type SchemaNode, + type RegularNode, + type SchemaTreeRefDereferenceFn, +} from "@stoplight/json-schema-tree"; +import { type Dictionary } from "@stoplight/types"; +import { resolveInlineRef } from "@stoplight/json"; +import SchemaTreeView from "./SchemaTree.tsx"; +import SchemaVariantSelector from "./SchemaVariantSelector.tsx"; +import type { SchemaRowData } from "./types"; +import { getTopLevelVariants } from "~/util/models"; + +interface Props { + schema: Record<string, unknown>; + title: "Input" | "Output"; + /** + * When set, render only the variant whose `title` matches (no embedded + * selector). If the schema has no matching variant (e.g. the other side of + * a unified selector doesn't define this branch), the full schema renders + * unchanged. Used by ModelDetailPage to drive both Input and Output panels + * from a single top-level variant selector. + */ + variantTitle?: string; +} + +const { schema, title, variantTitle } = Astro.props; +const hideRequired = title === "Output"; + +// Check if schema is a flat/simple schema (no properties, no oneOf/anyOf with properties) +// These are things like streaming output: { type: "string", contentType: "...", format: "binary" } +function isFlatSchema(schemaObj: Record<string, unknown>): boolean { + // Has properties = not flat + if (schemaObj.properties) return false; + // Has oneOf/anyOf with properties inside = not flat + const variants = (schemaObj.oneOf || schemaObj.anyOf) as + | Record<string, unknown>[] + | undefined; + if (variants?.some((v) => v.properties)) return false; + // Has type and some other attributes but no nested structure + return !!(schemaObj.type || schemaObj.contentType || schemaObj.format); +} + +const isFlat = isFlatSchema(schema); + +// Convert flat schema keys into rows for display +// e.g. { type: "string", format: "binary" } => rows with name=type, type=string +function flatSchemaToRows(schemaObj: Record<string, unknown>): SchemaRowData[] { + const keys = Object.keys(schemaObj); + return keys.map((key, i) => ({ + id: key, + name: key, + type: String(schemaObj[key]), + isArray: false, + isObject: false, + isOneOf: false, + isOneOfChild: false, + isFirstOneOfChild: false, + isLastOneOfChild: false, + required: false, + defaultValue: undefined, + description: undefined, + enumValues: undefined, + metadata: undefined, + depth: 0, + isLast: i === keys.length - 1, + ancestorIsLast: [], + children: undefined, + })); +} + +// Generate unique ID for sessionStorage keys +const schemaId = `schema-${title.toLowerCase()}-${Date.now()}`; + +type ReferenceInfo = { + source: string | null; + pointer: string | null; +}; +type ReferenceResolver = ( + ref: ReferenceInfo, + propertyPath: string[] | null, + currentObject?: object, +) => unknown; + +const defaultResolver: SchemaTreeRefDereferenceFn = + (contextObject: object): ReferenceResolver => + ({ pointer }, _, currentObject) => { + const activeObject = contextObject ?? currentObject; + if (pointer === null) return null; + if (pointer === "#") return activeObject; + const resolved = resolveInlineRef( + activeObject as Dictionary<string>, + pointer, + ); + if (resolved) return resolved; + throw new ReferenceError(`Could not resolve '${pointer}'`); + }; + +// Skip structural JSON Schema keys that we handle separately +const SKIP_KEYS = new Set([ + "type", + "properties", + "items", + "required", + "description", + "default", + "enum", + "oneOf", + "anyOf", + "allOf", + "title", + "additionalProperties", +]); + +// Skip internal JSON Schema keywords ($-prefixed) and vendor extensions (x-prefixed) +const shouldSkipKey = (key: string) => + SKIP_KEYS.has(key) || key.startsWith("$") || key.startsWith("x-"); + +function collectRows( + nodes: SchemaNode[] | undefined, + depth: number, + ancestorIsLast: boolean[], + parentPath: string = "", +): SchemaRowData[] { + if (!nodes) return []; + const rows: SchemaRowData[] = []; + + // Filter to valid property nodes + const validNodes = nodes.filter((node) => { + const reg = node as RegularNode; + const rawName = reg.subpath?.[reg.subpath.length - 1]; + if (rawName === undefined || rawName === "") return false; + const parentProperties = reg.parent?.fragment?.properties; + if (parentProperties && !(String(rawName) in (parentProperties as object))) + return false; + return true; + }); + + for (let i = 0; i < validNodes.length; i++) { + const reg = validNodes[i] as RegularNode; + const name = String(reg.subpath?.[reg.subpath.length - 1] ?? ""); + const isLast = i === validNodes.length - 1; + const id = parentPath ? `${parentPath}.${name}` : name; + + const parentRequired = reg.parent?.fragment?.required; + const required = Array.isArray(parentRequired) + ? (parentRequired as string[]).includes(name) + : false; + + const isArray = reg.primaryType === "array"; + const isObject = reg.primaryType === "object"; + const hasOneOf = reg.combiners?.includes(SchemaCombinerName.OneOf) ?? false; + const hasAnyOf = reg.combiners?.includes(SchemaCombinerName.AnyOf) ?? false; + + // Check if this is a nullable type pattern (e.g., boolean | null, string | null) + // In this case, we should collapse it to just show the non-null type as nullable + let isNullable = false; + let nullableType: string | undefined; + let nullableChildDescription: string | undefined; + if ((hasOneOf || hasAnyOf) && reg.children?.length === 2) { + const childTypes = reg.children.map((c) => + String((c as RegularNode).primaryType ?? ""), + ); + const nullIndex = childTypes.findIndex((t) => t === "null"); + if (nullIndex !== -1) { + // This is a nullable type - get the non-null type and its description + const nonNullIndex = nullIndex === 0 ? 1 : 0; + const nonNullChild = reg.children[nonNullIndex] as RegularNode; + nullableType = childTypes[nonNullIndex]; + nullableChildDescription = nonNullChild.annotations?.description as + | string + | undefined; + isNullable = true; + } + } + + const isOneOf = (hasOneOf || hasAnyOf) && !isNullable; + const type = + isNullable && nullableType + ? `${nullableType} | null` + : isOneOf + ? "one of" + : (reg.primaryType ?? ""); + + const defaultValue = + reg.annotations?.default !== undefined + ? String(reg.annotations.default) + : undefined; + + // For nullable types, prefer child description if parent doesn't have one + const description = + (reg.annotations?.description as string | undefined) ?? + nullableChildDescription; + // Check both validations and fragment for enum (different schema parsers put it in different places) + const enumValues = ( + (reg.validations?.enum as unknown[]) ?? + ((reg.fragment as Record<string, unknown>)?.enum as unknown[]) + )?.map(String); + + // Collect all metadata from fragment and validations + const metadata: Record<string, string | number | boolean> = {}; + + // Get metadata from fragment (format, etc.) + const fragment = reg.fragment as Record<string, unknown>; + if (fragment) { + for (const [key, value] of Object.entries(fragment)) { + if ( + !shouldSkipKey(key) && + (typeof value === "string" || + typeof value === "number" || + typeof value === "boolean") + ) { + metadata[key] = value; + } + } + } + + // Get metadata from validations (minimum, maximum, minLength, maxLength, minItems, maxItems, etc.) + if (reg.validations) { + for (const [key, value] of Object.entries(reg.validations)) { + if ( + !shouldSkipKey(key) && + key !== "enum" && + (typeof value === "string" || + typeof value === "number" || + typeof value === "boolean") + ) { + metadata[key] = value; + } + } + } + + // For arrays, also get format from items + if (isArray && reg.children?.length === 1) { + const itemsNode = reg.children[0] as RegularNode; + const itemsFragment = itemsNode.fragment as Record<string, unknown>; + if (itemsFragment?.format && typeof itemsFragment.format === "string") { + metadata["format"] = itemsFragment.format; + } + } + + // Collect children recursively + // For arrays, skip the "items" intermediate node and lift its children + // For nullable types, don't show children (they're just the type | null options) + let children: SchemaRowData[] | undefined; + if (reg.children?.length && !isNullable) { + if (isArray && reg.children.length === 1) { + const itemsNode = reg.children[0] as RegularNode; + const itemsName = String( + itemsNode.subpath?.[itemsNode.subpath.length - 1] ?? "", + ); + if (itemsName === "items" && itemsNode.children?.length) { + // Skip "items" node, use its children directly + children = collectRows( + itemsNode.children, + depth + 1, + [...ancestorIsLast, isLast], + id, + ); + } else { + children = collectRows( + reg.children, + depth + 1, + [...ancestorIsLast, isLast], + id, + ); + } + } else { + children = collectRows( + reg.children, + depth + 1, + [...ancestorIsLast, isLast], + id, + ); + } + } + + // For oneOf/anyOf items, try to extract a meaningful label + let displayName = name; + const parentReg = reg.parent as RegularNode | undefined; + const isOneOfChild = parentReg?.combiners?.includes( + SchemaCombinerName.OneOf, + ); + const isAnyOfChild = parentReg?.combiners?.includes( + SchemaCombinerName.AnyOf, + ); + if (isOneOfChild || isAnyOfChild) { + // Try node.title, annotations.title, or fragment.title + const fragment = reg.fragment as Record<string, unknown>; + const title = + reg.title ?? + ((reg.annotations as Record<string, unknown>)?.title as + | string + | undefined) ?? + (fragment?.title as string | undefined); + + if (title) { + displayName = title; + } else { + // Try to infer from role property's enum (for message types) + const properties = fragment?.properties as + | Record<string, Record<string, unknown>> + | undefined; + const roleEnum = properties?.role?.enum as string[] | undefined; + if (roleEnum && roleEnum.length === 1) { + // Single role value - use it as the name (e.g., "user", "assistant", "tool") + displayName = `${roleEnum[0]} message`; + } else { + // Try other heuristics + const nestedOneOf = fragment?.oneOf as + | Record<string, unknown>[] + | undefined; + if (nestedOneOf && nestedOneOf.length > 0 && nestedOneOf[0]?.title) { + displayName = `${nestedOneOf[0].title as string} format`; + } else if (properties) { + const props = Object.keys(properties); + if (props.includes("requests")) { + displayName = "Batch request"; + } else { + displayName = `Option ${Number(name) + 1}`; + } + } else { + displayName = `Option ${Number(name) + 1}`; + } + } + } + } + + rows.push({ + id, + name: displayName, + type, + isArray, + isObject, + isOneOf, + isOneOfChild: !!(isOneOfChild || isAnyOfChild), + isFirstOneOfChild: !!(isOneOfChild || isAnyOfChild) && i === 0, + isLastOneOfChild: !!(isOneOfChild || isAnyOfChild) && isLast, + required, + defaultValue, + description, + enumValues, + metadata: Object.keys(metadata).length > 0 ? metadata : undefined, + depth, + isLast, + ancestorIsLast, + children, + }); + } + + return rows; +} + +const allVariants = getTopLevelVariants(schema); + +// When the parent drives variant selection (variantTitle is set), pick that +// branch's schema if it exists. If the schema has no matching variant for the +// requested title, fall back to rendering the full schema so this side stays +// readable. This is the "only one side has titled variants" case. +const matchedVariant = variantTitle + ? allVariants?.find((v) => v.title === variantTitle) + : undefined; + +// Only show the inline variant selector when the parent isn't driving +// selection. If variantTitle is set, we never render the inline selector. +const topLevelVariants = variantTitle ? null : allVariants; + +const schemaForRendering = matchedVariant?.schema ?? schema; + +// Process variants or full schema +function processSchema(schemaToProcess: Record<string, unknown>) { + const tree = new SchemaTree(schemaToProcess, { + mergeAllOf: true, + refResolver: defaultResolver, + maxRefDepth: 3, + }); + tree.populate(); + + const topNodes = + (tree.root.children[0] as RegularNode)?.children ?? undefined; + return collectRows(topNodes, 0, []); +} + +// For flat schemas, convert keys to rows directly +// For non-variant schemas, process with tree parser +// For variant schemas (inline selector), defer to variantRows +const rows = isFlat + ? flatSchemaToRows(schemaForRendering) + : topLevelVariants + ? [] + : processSchema(schemaForRendering); + +// For variant schemas, process each variant +const variantRows = + topLevelVariants?.map((v) => ({ + title: v.title, + rows: processSchema(v.schema), + })) ?? []; +--- + +<div class="schema-display"> + { + topLevelVariants && variantRows.length > 0 ? ( + <SchemaVariantSelector + variants={variantRows} + schemaId={schemaId} + hideRequired={hideRequired} + client:visible + /> + ) : rows.length === 0 ? ( + <p class="text-sm text-muted-foreground">No parameters defined.</p> + ) : ( + <SchemaTreeView + rows={rows} + schemaId={schemaId} + hideRequired={hideRequired} + client:visible + /> + ) + } +</div> diff --git a/src/nimbus/components/models/SchemaTree.tsx b/src/nimbus/components/models/SchemaTree.tsx new file mode 100644 index 00000000000..0c15e9e1bac --- /dev/null +++ b/src/nimbus/components/models/SchemaTree.tsx @@ -0,0 +1,441 @@ +import { useState, useEffect, useCallback, useMemo } from "react"; +import type { SchemaRowData } from "./types"; + +/** + * SchemaTree — interactive view for schema parameters: collapsible nested + * objects/arrays with lazy child rendering, within-tree search (filter + + * auto-expand + highlight), sessionStorage collapse persistence, and + * oneOf/anyOf OR-dividers. + */ + +interface SchemaTreeProps { + rows: SchemaRowData[]; + schemaId: string; + hideRequired?: boolean; +} + +interface SchemaNodeProps { + row: SchemaRowData; + schemaId: string; + searchTerm: string; + forceExpand: boolean; + expandedNodes: Set<string>; + onToggle: (id: string) => void; + hideRequired?: boolean; +} + +// Highlight matching text in search +function highlightMatch(text: string, searchTerm: string): React.ReactNode { + if (!searchTerm || !text) return text; + + const lowerText = text.toLowerCase(); + const lowerSearch = searchTerm.toLowerCase(); + const index = lowerText.indexOf(lowerSearch); + + if (index === -1) return text; + + return ( + <> + {text.slice(0, index)} + <mark className="rounded bg-warning-muted px-0.5 text-warning"> + {text.slice(index, index + searchTerm.length)} + </mark> + {text.slice(index + searchTerm.length)} + </> + ); +} + +// Check if a row or its children match the search +function matchesSearch(row: SchemaRowData, searchTerm: string): boolean { + if (!searchTerm) return true; + + const lower = searchTerm.toLowerCase(); + if (row.name.toLowerCase().includes(lower)) return true; + if (row.description?.toLowerCase().includes(lower)) return true; + if (row.type.toLowerCase().includes(lower)) return true; + + if (row.children) { + return row.children.some((child) => matchesSearch(child, searchTerm)); + } + + return false; +} + +// Get all node IDs that should be expanded due to search match in children +function getExpandedForSearch( + rows: SchemaRowData[], + searchTerm: string, +): Set<string> { + const expanded = new Set<string>(); + + function traverse(row: SchemaRowData, ancestors: string[]) { + const lower = searchTerm.toLowerCase(); + const directMatch = + row.name.toLowerCase().includes(lower) || + row.description?.toLowerCase().includes(lower); + + if (row.children) { + for (const child of row.children) { + traverse(child, [...ancestors, row.id]); + } + } + + if (directMatch || (row.children && matchesSearch(row, searchTerm))) { + ancestors.forEach((id) => expanded.add(id)); + if (row.children) expanded.add(row.id); + } + } + + rows.forEach((row) => traverse(row, [])); + return expanded; +} + +// OR divider component for oneOf children +function OrDivider({ depth }: { depth: number }) { + const indentPx = depth * 24; + return ( + <div + className="flex items-center gap-2 py-0 text-xs text-muted-foreground" + style={{ paddingLeft: indentPx + 16 }} + > + <span className="h-px flex-1 border-t border-dashed border-border" /> + <span className="font-medium tracking-wider uppercase">or</span> + <span className="h-px flex-1 border-t border-dashed border-border" /> + </div> + ); +} + +// Individual schema node component +function SchemaNode({ + row, + schemaId, + searchTerm, + forceExpand, + expandedNodes, + onToggle, + hideRequired, +}: SchemaNodeProps) { + const hasChildren = row.children && row.children.length > 0; + const isExpanded = forceExpand || expandedNodes.has(row.id); + const [hasRenderedChildren, setHasRenderedChildren] = useState(false); + + useEffect(() => { + if (isExpanded && !hasRenderedChildren) { + setHasRenderedChildren(true); + } + }, [isExpanded, hasRenderedChildren]); + + const handleToggle = () => { + onToggle(row.id); + }; + + const indentPx = 16 + row.depth * 24; // 16px base padding + depth indent + + // For oneOf children, use different styling + // Skip OR divider for top-level oneOf (depth 0) - those use the variant selector instead + if (row.isOneOfChild) { + // Only show outer border when: last oneOf child AND (no children OR not expanded) + const showOneOfBorder = + row.isLastOneOfChild && (!hasChildren || !isExpanded); + return ( + <> + {/* Show OR divider before non-first options, but not at top level */} + {!row.isFirstOneOfChild && row.depth > 0 && ( + <OrDivider depth={row.depth} /> + )} + + {/* Hide bottom border if OR divider will follow, or if expanded (children provide border) */} + <div className={showOneOfBorder ? "border-b border-border" : ""}> + <div + className={[ + "py-3", + hasChildren && "cursor-pointer hover:bg-muted/50", + hasChildren && isExpanded && "border-b border-border", + ] + .filter(Boolean) + .join(" ")} + style={{ paddingLeft: indentPx }} + onClick={hasChildren ? handleToggle : undefined} + role={hasChildren ? "button" : undefined} + aria-expanded={hasChildren ? isExpanded : undefined} + > + {/* Line 1: Name (non-mono for oneOf options) */} + <div className="flex items-center gap-1 text-sm"> + <span className="w-4 flex-shrink-0 text-center"> + {hasChildren && ( + <span + className={[ + "inline-block text-xs text-muted-foreground transition-transform", + isExpanded && "rotate-90", + ] + .filter(Boolean) + .join(" ")} + > + ▶ + </span> + )} + </span> + <span className="font-medium text-foreground"> + {highlightMatch(row.name, searchTerm)} + </span> + {row.isArray && ( + <span className="font-mono text-muted-foreground">[]</span> + )} + {row.isObject && !row.isOneOf && ( + <span className="font-mono text-muted-foreground">{"{}"}</span> + )} + </div> + + {/* Line 2: Type + metadata */} + <div className="mt-0.5 flex flex-wrap items-center gap-2 pl-5 text-xs text-muted-foreground"> + <span>{row.type}</span> + {row.metadata && + Object.entries(row.metadata).map(([key, value]) => ( + <span key={key}> + <span className="font-medium">{key}:</span> {String(value)} + </span> + ))} + {row.enumValues && row.enumValues.length > 0 && ( + <span> + <span className="font-medium">enum:</span>{" "} + {row.enumValues.join(", ")} + </span> + )} + </div> + + {/* Line 3: Description if present */} + {row.description && ( + <div className="mt-1 pl-5 text-sm text-muted-foreground"> + {highlightMatch(row.description, searchTerm)} + </div> + )} + </div> + + {/* Children */} + {hasChildren && hasRenderedChildren && isExpanded && ( + <div> + {row.children!.map((child) => ( + <SchemaNode + key={child.id} + row={child} + schemaId={schemaId} + searchTerm={searchTerm} + forceExpand={forceExpand} + expandedNodes={expandedNodes} + onToggle={onToggle} + hideRequired={hideRequired} + /> + ))} + </div> + )} + </div> + </> + ); + } + + // Regular node styling + // Only show outer border when collapsed or no children - expanded children provide their own closing border + const showOuterBorder = !hasChildren || !isExpanded; + return ( + <div className={showOuterBorder ? "border-b border-border" : ""}> + {/* Clickable row */} + <div + className={[ + "py-3", + hasChildren && "cursor-pointer hover:bg-muted/50", + hasChildren && isExpanded && "border-b border-border", + ] + .filter(Boolean) + .join(" ")} + style={{ paddingLeft: indentPx }} + onClick={hasChildren ? handleToggle : undefined} + role={hasChildren ? "button" : undefined} + aria-expanded={hasChildren ? isExpanded : undefined} + > + {/* Line 1: Name with expand arrow and type indicators */} + <div className="flex items-center gap-1 font-mono text-sm"> + <span className="w-4 flex-shrink-0 text-center"> + {hasChildren && ( + <span + className={[ + "inline-block text-xs text-muted-foreground transition-transform", + isExpanded && "rotate-90", + ] + .filter(Boolean) + .join(" ")} + > + ▶ + </span> + )} + </span> + <span className="font-medium text-foreground"> + {highlightMatch(row.name, searchTerm)} + </span> + {row.isArray && <span className="text-muted-foreground">[]</span>} + {row.isObject && !row.isOneOf && ( + <span className="text-muted-foreground">{"{}"}</span> + )} + </div> + + {/* Line 2: Metadata and description */} + <div className="mt-1 flex flex-wrap items-baseline gap-x-3 gap-y-1 pl-5 text-sm"> + {/* Type and badges */} + <span className="flex flex-wrap items-center gap-3"> + <code className="text-xs text-muted-foreground">{row.type}</code> + {row.required && !hideRequired && ( + <span className="rounded border border-warning/30 bg-warning-muted px-1 py-0.5 text-xs leading-none text-warning"> + required + </span> + )} + {row.defaultValue !== undefined && ( + <span className="text-xs text-muted-foreground"> + <span className="font-medium">default:</span> {row.defaultValue} + </span> + )} + {/* Render all metadata */} + {row.metadata && + Object.entries(row.metadata).map(([key, value]) => ( + <span key={key} className="text-xs text-muted-foreground"> + <span className="font-medium">{key}:</span> {String(value)} + </span> + ))} + {/* Enum values */} + {row.enumValues && row.enumValues.length > 0 && ( + <span className="text-xs text-muted-foreground"> + <span className="font-medium">enum:</span>{" "} + {row.enumValues.join(", ")} + </span> + )} + </span> + + {/* Description */} + {row.description && ( + <span className="text-muted-foreground"> + {highlightMatch(row.description, searchTerm)} + </span> + )} + </div> + </div> + + {/* Children */} + {hasChildren && hasRenderedChildren && isExpanded && ( + <div> + {row.children!.map((child) => ( + <SchemaNode + key={child.id} + row={child} + schemaId={schemaId} + searchTerm={searchTerm} + forceExpand={forceExpand} + expandedNodes={expandedNodes} + onToggle={onToggle} + hideRequired={hideRequired} + /> + ))} + </div> + )} + </div> + ); +} + +export default function SchemaTree({ + rows, + schemaId, + hideRequired, +}: SchemaTreeProps) { + const [searchTerm, setSearchTerm] = useState(""); + const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set()); + + useEffect(() => { + try { + const saved = sessionStorage.getItem(`schema-expanded-${schemaId}`); + if (saved) { + setExpandedNodes(new Set(JSON.parse(saved))); + } + } catch { + // Ignore errors + } + }, [schemaId]); + + const saveExpandedState = useCallback( + (nodes: Set<string>) => { + try { + sessionStorage.setItem( + `schema-expanded-${schemaId}`, + JSON.stringify([...nodes]), + ); + } catch { + // Ignore errors + } + }, + [schemaId], + ); + + const handleToggle = useCallback( + (id: string) => { + setExpandedNodes((prev) => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + saveExpandedState(next); + return next; + }); + }, + [saveExpandedState], + ); + + const searchExpandedNodes = useMemo(() => { + if (!searchTerm) return new Set<string>(); + return getExpandedForSearch(rows, searchTerm); + }, [rows, searchTerm]); + + const filteredRows = useMemo(() => { + if (!searchTerm) return rows; + return rows.filter((row) => matchesSearch(row, searchTerm)); + }, [rows, searchTerm]); + + const mergedExpandedNodes = useMemo(() => { + return new Set([...expandedNodes, ...searchExpandedNodes]); + }, [expandedNodes, searchExpandedNodes]); + + return ( + <div className="schema-tree"> + {/* Search filter */} + <div className="mb-4"> + <input + type="text" + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} + placeholder="Filter parameters..." + className="w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none" + /> + </div> + + {filteredRows.length === 0 ? ( + <p className="py-4 text-sm text-muted-foreground"> + No parameters match your search. + </p> + ) : ( + <div className="max-h-[500px] overflow-y-scroll rounded-md border border-border"> + <div className="not-prose divide-y divide-border"> + {filteredRows.map((row) => ( + <SchemaNode + key={row.id} + row={row} + schemaId={schemaId} + searchTerm={searchTerm} + forceExpand={searchTerm.length > 0} + expandedNodes={mergedExpandedNodes} + onToggle={handleToggle} + hideRequired={hideRequired} + /> + ))} + </div> + </div> + )} + </div> + ); +} diff --git a/src/nimbus/components/models/SchemaVariantSelector.tsx b/src/nimbus/components/models/SchemaVariantSelector.tsx new file mode 100644 index 00000000000..a1bdda02747 --- /dev/null +++ b/src/nimbus/components/models/SchemaVariantSelector.tsx @@ -0,0 +1,87 @@ +import { useState } from "react"; +import SchemaTree from "./SchemaTree.tsx"; +import type { SchemaRowData } from "./types"; + +/** + * SchemaVariantSelector — inline request-format picker (e.g. Prompt vs + * Messages) that owns its own tree rendering. + */ + +interface SchemaVariant { + title: string; + description?: string; + rows: SchemaRowData[]; +} + +interface SchemaVariantSelectorProps { + variants: SchemaVariant[]; + schemaId: string; + hideRequired?: boolean; +} + +// Map variant titles to descriptions +const variantDescriptions: Record<string, string> = { + Prompt: "Simple text input for single-turn interactions", + Messages: + "Structured conversation format with roles (user, assistant, system)", +}; + +export default function SchemaVariantSelector({ + variants, + schemaId, + hideRequired, +}: SchemaVariantSelectorProps) { + const [selectedIndex, setSelectedIndex] = useState(0); + const selectedVariant = variants[selectedIndex]; + + return ( + <div> + {/* Variant selector */} + <fieldset className="p-0"> + <legend className="sr-only">Input format</legend> + <div className="mt-0 flex gap-x-3"> + {variants.map((variant, index) => { + const isSelected = selectedIndex === index; + const description = variantDescriptions[variant.title]; + return ( + <button + key={variant.title} + type="button" + onClick={() => setSelectedIndex(index)} + className={`mt-0 flex flex-1 cursor-pointer flex-col justify-start rounded-lg border p-3 text-left transition-colors ${ + isSelected + ? "border-primary bg-primary/10" + : "border-border bg-card hover:border-muted-foreground/40" + }`} + > + <span + className={`text-sm font-medium ${isSelected ? "text-primary" : "text-foreground"}`} + > + {variant.title} + </span> + {description && ( + <p + className={`text-xs ${isSelected ? "text-primary/80" : "text-muted-foreground"}`} + > + {description} + </p> + )} + </button> + ); + })} + </div> + </fieldset> + + {/* Selected variant content */} + {selectedVariant.rows.length === 0 ? ( + <p className="text-sm text-muted-foreground">No parameters defined.</p> + ) : ( + <SchemaTree + rows={selectedVariant.rows} + schemaId={`${schemaId}-${selectedVariant.title.toLowerCase().replace(/\s+/g, "-")}`} + hideRequired={hideRequired} + /> + )} + </div> + ); +} diff --git a/src/nimbus/components/models/SchemaVariantTabs.tsx b/src/nimbus/components/models/SchemaVariantTabs.tsx new file mode 100644 index 00000000000..bb19b66c463 --- /dev/null +++ b/src/nimbus/components/models/SchemaVariantTabs.tsx @@ -0,0 +1,71 @@ +import { useEffect, useRef, useState } from "react"; + +interface Props { + titles: string[]; + /** + * Optional ID of an element whose `data-active-variant` attribute should + * be updated. Defaults to the closest ancestor with `data-variant-section`. + */ + targetId?: string; +} + +/** + * Top-level variant selector that drives both the Input and Output schema + * panels from a single control. Owns no rendering — it toggles + * `data-active-variant` on its scoping ancestor (or the element referenced by + * `targetId`) and flips `hidden` on every `[data-variant-pane]` under it. The + * server-rendered `hidden` attribute is the no-JS default. + */ +export default function SchemaVariantTabs({ titles, targetId }: Props) { + const [selected, setSelected] = useState(0); + const rootRef = useRef<HTMLDivElement>(null); + + useEffect(() => { + const title = titles[selected]; + if (!title) return; + const target = targetId + ? document.getElementById(targetId) + : rootRef.current?.closest<HTMLElement>("[data-variant-section]"); + if (!target) return; + target.dataset.activeVariant = title; + // Toggle visibility of every variant pane under this section. The + // server-rendered `hidden` attribute provides a correct default before + // hydration; this just keeps it in sync when the user switches. + const panes = target.querySelectorAll<HTMLElement>("[data-variant-pane]"); + panes.forEach((pane) => { + pane.hidden = pane.dataset.variantPane !== title; + }); + }, [selected, titles, targetId]); + + return ( + <div ref={rootRef}> + <fieldset className="p-0"> + <legend className="sr-only">Schema variant</legend> + <div className="mt-0 flex gap-x-3"> + {titles.map((title, index) => { + const isSelected = selected === index; + return ( + <button + key={title} + type="button" + onClick={() => setSelected(index)} + aria-pressed={isSelected} + className={`mt-0 flex flex-1 cursor-pointer flex-col justify-start rounded-lg border p-3 text-left transition-colors ${ + isSelected + ? "border-primary bg-primary/10" + : "border-border bg-card hover:border-muted-foreground/40" + }`} + > + <span + className={`text-sm font-medium ${isSelected ? "text-primary" : "text-foreground"}`} + > + {title} + </span> + </button> + ); + })} + </div> + </fieldset> + </div> + ); +} diff --git a/src/nimbus/components/models/code/Bge-Reranker-Base.astro b/src/nimbus/components/models/code/Bge-Reranker-Base.astro new file mode 100644 index 00000000000..4cd8003b5d0 --- /dev/null +++ b/src/nimbus/components/models/code/Bge-Reranker-Base.astro @@ -0,0 +1,79 @@ +--- +import { z } from "astro/zod"; +import { Code } from "~/components/ui/code"; +import { Tabs, TabItem } from "~/components/ui/tabs"; + +type Props = z.infer<typeof props>; + +const props = z.object({ + name: z.string(), +}); + +const { name } = props.parse(Astro.props); + +const worker = ` +export interface Env { + AI: Ai; +} + +export default { + async fetch(request, env): Promise<Response> { + const query = 'Which one is cooler?' + const contexts = [ + { + text: 'a cyberpunk lizzard' + }, + { + text: 'a cyberpunk cat' + } + ]; + + const response = await env.AI.run('${name}', { query, contexts }); + + return Response.json(response); + }, +} satisfies ExportedHandler<Env>; + +`; + +const python = ` +import os +import requests + +ACCOUNT_ID = "your-account-id" +AUTH_TOKEN = os.environ.get("CLOUDFLARE_AUTH_TOKEN") + +response = requests.post( + f"https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/run/${name}", + headers={"Authorization": f"Bearer {AUTH_TOKEN}"}, + json={ + "query": "Which one is better?", + "contexts": [ + {"text": "a cyberpunk lizzard"}, + {"text": "a cyberpunk car"}, + ] + } +) +result = response.json() +print(result) +`; + +const curl = ` +curl https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/ai/run/${name} \\ + -X POST \\ + -H "Authorization: Bearer $CLOUDFLARE_AUTH_TOKEN" \\ + -d '{ "query": "Which one is better?", "contexts": [{ "text": "a cyberpunk lizzard" }, {"text": "a cyberpunk cat"}]}' +`; +--- + +<Tabs syncKey="workersAiExamples"> + <TabItem label="TypeScript"> + <Code code={worker} lang="ts" title="" /> + </TabItem> + <TabItem label="Python"> + <Code code={python} lang="py" title="" /> + </TabItem> + <TabItem label="curl"> + <Code code={curl} lang="sh" /> + </TabItem> +</Tabs> diff --git a/src/nimbus/components/models/code/DeepgramAura.astro b/src/nimbus/components/models/code/DeepgramAura.astro new file mode 100644 index 00000000000..e22afbae250 --- /dev/null +++ b/src/nimbus/components/models/code/DeepgramAura.astro @@ -0,0 +1,47 @@ +--- +import { z } from "astro/zod"; +import { Code } from "~/components/ui/code"; +import { Tabs, TabItem } from "~/components/ui/tabs"; + +type Props = z.infer<typeof props>; + +const props = z.object({ + name: z.string(), + lora: z.boolean(), +}); + +const { name } = props.parse(Astro.props); + +const worker = ` +export default { + async fetch(request, env, ctx): Promise<Response> { + const resp = await env.AI.run("${name}", { + "text":"Hello World!" + }, { + returnRawResponse: true + }); + + return resp; + }, +} satisfies ExportedHandler<Env>; +`; + +const curl = ` +curl --request POST \ + --url 'https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/run/${name}' \ + --header 'Authorization: Bearer {TOKEN}' \ + --header 'Content-Type: application/json' \ + --data '{ + "text":"Hello world!" +}' +`; +--- + +<Tabs syncKey="workersAiExamples"> + <TabItem label="TypeScript"> + <Code code={worker} lang="ts" title="" /> + </TabItem> + <TabItem label="curl"> + <Code code={curl} lang="sh" /> + </TabItem> +</Tabs> diff --git a/src/nimbus/components/models/code/Flux-1-Schnell.astro b/src/nimbus/components/models/code/Flux-1-Schnell.astro new file mode 100644 index 00000000000..5051a5de1f9 --- /dev/null +++ b/src/nimbus/components/models/code/Flux-1-Schnell.astro @@ -0,0 +1,75 @@ +--- +import { z } from "astro/zod"; +import { Code } from "~/components/ui/code"; +import { Tabs, TabItem } from "~/components/ui/tabs"; + +type Props = z.infer<typeof props>; + +const props = z.object({ + name: z.string(), +}); + +const { name } = props.parse(Astro.props); + +const workerReturningDataURI = ` +export interface Env { + AI: Ai; +} + +export default { + async fetch(request, env): Promise<Response> { + const response = await env.AI.run('@cf/black-forest-labs/flux-1-schnell', { + prompt: 'a cyberpunk lizard', + seed: Math.floor(Math.random() * 10) + }); + // response.image is base64 encoded which can be used directly as an <img src=""> data URI + const dataURI = \`data:image/jpeg;charset=utf-8;base64,\${response.image}\`; + return Response.json({ dataURI }); + }, +} satisfies ExportedHandler<Env>; + +`; + +const workerReturningImage = ` +export interface Env { + AI: Ai; +} + +export default { + async fetch(request, env): Promise<Response> { + const response = await env.AI.run('@cf/black-forest-labs/flux-1-schnell', { + prompt: 'a cyberpunk lizard', + seed: Math.floor(Math.random() * 10) + }); + // Convert from base64 string + const binaryString = atob(response.image); + // Create byte representation + const img = Uint8Array.from(binaryString, (m) => m.codePointAt(0)); + return new Response(img, { + headers: { + 'Content-Type': 'image/jpeg', + }, + }); + }, +} satisfies ExportedHandler<Env>; +`; + +const curl = ` +curl https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/ai/run/${name} \\ + -X POST \\ + -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \\ + -d '{ "prompt": "cyberpunk cat", "seed": "Random positive integer" }' +`; +--- + +<Tabs syncKey="workersAiExamples"> + <TabItem label="Worker (Data URI)"> + <Code code={workerReturningDataURI} lang="ts" /> + </TabItem> + <TabItem label="Worker (Image)"> + <Code code={workerReturningImage} lang="ts" /> + </TabItem> + <TabItem label="curl"> + <Code code={curl} lang="sh" /> + </TabItem> +</Tabs> diff --git a/src/nimbus/components/models/code/ObjectDetectionCode.astro b/src/nimbus/components/models/code/ObjectDetectionCode.astro new file mode 100644 index 00000000000..d5f7c793192 --- /dev/null +++ b/src/nimbus/components/models/code/ObjectDetectionCode.astro @@ -0,0 +1,53 @@ +--- +import { z } from "astro/zod"; +import { Code } from "~/components/ui/code"; +import { Tabs, TabItem } from "~/components/ui/tabs"; + +type Props = z.infer<typeof props>; + +const props = z.object({ + name: z.string(), +}); + +const { name } = props.parse(Astro.props); + +const worker = ` +export interface Env { + AI: Ai; +} + +export default { + async fetch(request, env): Promise<Response> { + const res = await fetch("https://cataas.com/cat"); + const blob = await res.arrayBuffer(); + + const inputs = { + image: [...new Uint8Array(blob)], + }; + + const response = await env.AI.run( + "${name}", + inputs + ); + + return new Response(JSON.stringify({ inputs: { image: [] }, response })); + }, +} satisfies ExportedHandler<Env>; +`; + +const curl = ` +curl https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/ai/run/${name} \\ + -X POST \\ + -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \\ + --data-binary "@pedestrian-boulevard-manhattan-crossing.jpg" +`; +--- + +<Tabs syncKey="workersAiExamples"> + <TabItem label="TypeScript"> + <Code code={worker} lang="ts" title="" /> + </TabItem> + <TabItem label="curl"> + <Code code={curl} lang="sh" /> + </TabItem> +</Tabs> diff --git a/src/nimbus/components/models/code/SummarizationCode.astro b/src/nimbus/components/models/code/SummarizationCode.astro new file mode 100644 index 00000000000..64d89f3a591 --- /dev/null +++ b/src/nimbus/components/models/code/SummarizationCode.astro @@ -0,0 +1,47 @@ +--- +import { z } from "astro/zod"; +import { Code } from "~/components/ui/code"; +import { Tabs, TabItem } from "~/components/ui/tabs"; + +type Props = z.infer<typeof props>; + +const props = z.object({ + name: z.string(), +}); + +const { name } = props.parse(Astro.props); + +const worker = ` +export interface Env { + AI: Ai; +} + +export default { + async fetch(request, env): Promise<Response> { + const response = await env.AI.run("${name}", { + input_text: "Workers AI allows you to run machine learning models, on the Cloudflare network, from your own code – whether that be from Workers, Pages, or anywhere via the Cloudflare API. With the launch of Workers AI, Cloudflare is slowly rolling out GPUs to its global network. This enables you to build and deploy ambitious AI applications that run near your users, wherever they are.", + max_length: 14 + }); + return Response.json(response); + }, +} satisfies ExportedHandler<Env>; +`; + +const curl = ` +curl https://api.cloudflare.com/client/v4/accounts/{cf_account_id}/ai/run/${name} \\ + -H "Authorization: Bearer {cf_api_token}" \\ + -d '{ + "input_text": "Workers AI allows you to run machine learning models, on the Cloudflare network, from your own code – whether that be from Workers, Pages, or anywhere via the Cloudflare API. With the launch of Workers AI, Cloudflare is slowly rolling out GPUs to its global network. This enables you to build and deploy ambitious AI applications that run near your users, wherever they are.", + "max_length": 14 + }' +`; +--- + +<Tabs syncKey="workersAiExamples"> + <TabItem label="TypeScript"> + <Code code={worker} lang="ts" title="" /> + </TabItem> + <TabItem label="curl"> + <Code code={curl} lang="sh" /> + </TabItem> +</Tabs> diff --git a/src/nimbus/components/models/code/TextClassificationCode.astro b/src/nimbus/components/models/code/TextClassificationCode.astro new file mode 100644 index 00000000000..029b8d5e63e --- /dev/null +++ b/src/nimbus/components/models/code/TextClassificationCode.astro @@ -0,0 +1,64 @@ +--- +import { z } from "astro/zod"; +import { Code } from "~/components/ui/code"; +import { Tabs, TabItem } from "~/components/ui/tabs"; + +type Props = z.infer<typeof props>; + +const props = z.object({ + name: z.string(), +}); + +const { name } = props.parse(Astro.props); + +const worker = ` +export interface Env { + AI: Ai; +} + +export default { + async fetch(request, env): Promise<Response> { + + const response = await env.AI.run( + "${name}", + { + text: "This pizza is great!", + } + ); + + return Response.json(response); + }, +} satisfies ExportedHandler<Env>; +`; + +const python = ` +API_BASE_URL = "https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/run/" +headers = {"Authorization": "Bearer {API_KEY}"} + +def run(model, input): + response = requests.post(f"{API_BASE_URL}{model}", headers=headers, json=input) + return response.json() + +output = run("${name}", { "text": "This pizza is great!" }) +print(output) +`; + +const curl = ` +curl https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/ai/run/${name} \\ + -X POST \\ + -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \\ + -d '{ "text": "This pizza is great!" }' +`; +--- + +<Tabs syncKey="workersAiExamples"> + <TabItem label="TypeScript"> + <Code code={worker} lang="ts" title="" /> + </TabItem> + <TabItem label="Python"> + <Code code={python} lang="py" title="" /> + </TabItem> + <TabItem label="curl"> + <Code code={curl} lang="sh" /> + </TabItem> +</Tabs> diff --git a/src/nimbus/components/models/code/TextEmbeddingCode.astro b/src/nimbus/components/models/code/TextEmbeddingCode.astro new file mode 100644 index 00000000000..c3650fc95a1 --- /dev/null +++ b/src/nimbus/components/models/code/TextEmbeddingCode.astro @@ -0,0 +1,90 @@ +--- +import { z } from "astro/zod"; +import { Code } from "~/components/ui/code"; +import { Tabs, TabItem } from "~/components/ui/tabs"; +import { Aside } from "~/components/ui/aside"; + +type Props = z.infer<typeof props>; + +const props = z.object({ + name: z.string(), +}); + +const { name } = props.parse(Astro.props); + +const worker = ` +export interface Env { + AI: Ai; +} + +export default { + async fetch(request, env): Promise<Response> { + + // Can be a string or array of strings] + const stories = [ + "This is a story about an orange cloud", + "This is a story about a llama", + "This is a story about a hugging emoji", + ]; + + const embeddings = await env.AI.run( + "${name}", + { + text: stories, + } + ); + + return Response.json(embeddings); + }, +} satisfies ExportedHandler<Env>; +`; + +const python = ` +import os +import requests + + +ACCOUNT_ID = "your-account-id" +AUTH_TOKEN = os.environ.get("CLOUDFLARE_AUTH_TOKEN") + +stories = [ + 'This is a story about an orange cloud', + 'This is a story about a llama', + 'This is a story about a hugging emoji' +] + +response = requests.post( + f"https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/run/${name}", + headers={"Authorization": f"Bearer {AUTH_TOKEN}"}, + json={"text": stories} +) + +print(response.json()) +`; + +const curl = ` +curl https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/ai/run/${name} \\ + -X POST \\ + -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \\ + -d '{ "text": ["This is a story about an orange cloud", "This is a story about a llama", "This is a story about a hugging emoji"] }' +`; +--- + +<Tabs syncKey="workersAiExamples"> + <TabItem label="TypeScript"> + <Code code={worker} lang="ts" title="" /> + </TabItem> + <TabItem label="Python"> + <Code code={python} lang="py" title="" /> + </TabItem> + <TabItem label="curl"> + <Code code={curl} lang="sh" /> + </TabItem> +</Tabs> + +<Aside type="note" title="OpenAI compatible endpoints"> + Workers AI also supports OpenAI compatible API endpoints for{" "} + <code>/v1/chat/completions</code> and <code>/v1/embeddings</code>. For more + details, refer to{" "} + <a href="/workers-ai/configuration/open-ai-compatibility/">Configurations</a>. +</Aside> diff --git a/src/nimbus/components/models/code/TextGenerationCode.astro b/src/nimbus/components/models/code/TextGenerationCode.astro new file mode 100644 index 00000000000..5192b987af4 --- /dev/null +++ b/src/nimbus/components/models/code/TextGenerationCode.astro @@ -0,0 +1,162 @@ +--- +import { z } from "astro/zod"; +import { Code } from "~/components/ui/code"; +import { Tabs, TabItem } from "~/components/ui/tabs"; +import { Aside } from "~/components/ui/aside"; + +type Props = z.infer<typeof props>; + +const props = z.object({ + name: z.string(), + lora: z.boolean(), +}); + +const { name, lora } = props.parse(Astro.props); + +const loraWorker = ` +export interface Env { + AI: Ai; +} + +export default { + async fetch(request, env): Promise<Response> { + + const response = await env.AI.run("${name}", { + prompt: "tell me a story", + raw: true, //skip applying the default chat template + lora: "00000000-0000-0000-0000-000000000", //the finetune id OR name + }); + return Response.json(response); + }, +} satisfies ExportedHandler<Env>; +`; + +const loraCurl = ` +curl https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/ai/run/${name} \\ + -X POST \\ + -H "Authorization: Bearer $CLOUDFLARE_AUTH_TOKEN" \\ + -d '{ + "prompt": "tell me a story", + "raw": "true", + "lora": "00000000-0000-0000-0000-000000000" + }' +`; + +const streamingWorker = ` +export interface Env { + AI: Ai; +} + +export default { + async fetch(request, env): Promise<Response> { + + const messages = [ + { role: "system", content: "You are a friendly assistant" }, + { + role: "user", + content: "What is the origin of the phrase Hello, World", + }, + ]; + + const stream = await env.AI.run("${name}", { + messages, + stream: true, + }); + + return new Response(stream, { + headers: { "content-type": "text/event-stream" }, + }); + }, +} satisfies ExportedHandler<Env>; +`; +const worker = ` +export interface Env { + AI: Ai; +} + +export default { + async fetch(request, env): Promise<Response> { + + const messages = [ + { role: "system", content: "You are a friendly assistant" }, + { + role: "user", + content: "What is the origin of the phrase Hello, World", + }, + ]; + const response = await env.AI.run("${name}", { messages }); + + return Response.json(response); + }, +} satisfies ExportedHandler<Env>; +`; + +const python = ` +import os +import requests + +ACCOUNT_ID = "your-account-id" +AUTH_TOKEN = os.environ.get("CLOUDFLARE_AUTH_TOKEN") + +prompt = "Tell me all about PEP-8" +response = requests.post( + f"https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/run/${name}", + headers={"Authorization": f"Bearer {AUTH_TOKEN}"}, + json={ + "messages": [ + {"role": "system", "content": "You are a friendly assistant"}, + {"role": "user", "content": prompt} + ] + } +) +result = response.json() +print(result) +`; + +const curl = ` +curl https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/ai/run/${name} \\ + -X POST \\ + -H "Authorization: Bearer $CLOUDFLARE_AUTH_TOKEN" \\ + -d '{ "messages": [{ "role": "system", "content": "You are a friendly assistant" }, { "role": "user", "content": "Why is pizza so good" }]}' +`; +--- + +{ + lora ? ( + <Tabs syncKey="workersAiExamples"> + <TabItem label="TypeScript"> + <Code code={loraWorker} lang="ts" title="" /> + </TabItem> + <TabItem label="curl"> + <Code code={loraCurl} lang="sh" /> + </TabItem> + </Tabs> + ) : ( + <> + <Tabs syncKey="workersAiExamples"> + <TabItem label="Worker (Streaming)"> + <Code code={streamingWorker} lang="ts" /> + </TabItem> + <TabItem label="TypeScript"> + <Code code={worker} lang="ts" title="" /> + </TabItem> + <TabItem label="Python"> + <Code code={python} lang="py" title="" /> + </TabItem> + <TabItem label="curl"> + <Code code={curl} lang="sh" /> + </TabItem> + </Tabs> + + <Aside type="note" title="OpenAI compatible endpoints"> + Workers AI also supports OpenAI compatible API endpoints for{" "} + <code>/v1/chat/completions</code> and <code>/v1/embeddings</code>. For + more details, refer to{" "} + <a href="/workers-ai/configuration/open-ai-compatibility/"> + Configurations + </a> + . + </Aside> + </> + ) +} diff --git a/src/nimbus/components/models/code/TranslationCode.astro b/src/nimbus/components/models/code/TranslationCode.astro new file mode 100644 index 00000000000..abc233b8814 --- /dev/null +++ b/src/nimbus/components/models/code/TranslationCode.astro @@ -0,0 +1,73 @@ +--- +import { z } from "astro/zod"; +import { Code } from "~/components/ui/code"; +import { Tabs, TabItem } from "~/components/ui/tabs"; + +type Props = z.infer<typeof props>; + +const props = z.object({ + name: z.string(), +}); + +const { name } = props.parse(Astro.props); + +const worker = ` +export interface Env { + AI: Ai; +} + +export default { + async fetch(request, env): Promise<Response> { + + const response = await env.AI.run( + "${name}", + { + text: "I'll have an order of the moule frites", + source_lang: "english", // defaults to english + target_lang: "french", + } + ); + + return new Response(JSON.stringify(response)); + }, +} satisfies ExportedHandler<Env>; +`; + +const python = ` +import requests + +API_BASE_URL = "https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/run/" +headers = {"Authorization": "Bearer {API_TOKEN}"} + +def run(model, input): + response = requests.post(f"{API_BASE_URL}{model}", headers=headers, json=input) + return response.json() + +output = run('${name}', { + "text": "I'll have an order of the moule frites", + "source_lang": "english", + "target_lang": "french" +}) + +print(output) +`; + +const curl = ` +curl https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/ai/run/${name} \\ + -X POST \\ + -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \\ + -d '{ "text": "Ill have an order of the moule frites", "source_lang": "english", "target_lang": "french" }' +`; +--- + +<Tabs syncKey="workersAiExamples"> + <TabItem label="TypeScript"> + <Code code={worker} lang="ts" title="" /> + </TabItem> + <TabItem label="Python"> + <Code code={python} lang="py" title="" /> + </TabItem> + <TabItem label="curl"> + <Code code={curl} lang="sh" /> + </TabItem> +</Tabs> diff --git a/src/nimbus/components/models/data.ts b/src/nimbus/components/models/data.ts new file mode 100644 index 00000000000..225059ddf0b --- /dev/null +++ b/src/nimbus/components/models/data.ts @@ -0,0 +1,251 @@ +/** + * Author metadata (display name + logo). Each logo is imported from + * `../../assets/images/workers-ai/*.svg` and exposed via `.src`. Multiple ids + * can alias the same brand (e.g. `facebook`/`meta`/`meta-llama` → "Meta"). Ids + * with no entry fall back to the raw id + an initial-letter tile at the call + * sites via `authorData[id]?.name ?? id`. + */ +import alibaba from "~/assets/images/workers-ai/alibaba.svg"; +import anthropic from "~/assets/images/workers-ai/anthropic.svg"; +import assemblyai from "~/assets/images/workers-ai/assemblyai.svg"; +import aws from "~/assets/images/workers-ai/aws.svg"; +import baai from "~/assets/images/workers-ai/baai.svg"; +import blackforestlabs from "~/assets/images/workers-ai/blackforestlabs.svg"; +import bytedance from "~/assets/images/workers-ai/bytedance.svg"; +import cloudflare from "~/assets/images/workers-ai/cloudflare.svg"; +import deepgram from "~/assets/images/workers-ai/deepgram.svg"; +import deepseek from "~/assets/images/workers-ai/deepseek.svg"; +import defog from "~/assets/images/workers-ai/defog.svg"; +import elevenlabs from "~/assets/images/workers-ai/elevenlabs.svg"; +import fal from "~/assets/images/workers-ai/fal.svg"; +import fireworks from "~/assets/images/workers-ai/fireworks.svg"; +import google from "~/assets/images/workers-ai/google.svg"; +import huggingface from "~/assets/images/workers-ai/huggingface.svg"; +import ibm from "~/assets/images/workers-ai/ibm.svg"; +import ideogram from "~/assets/images/workers-ai/ideogram.svg"; +import inworld from "~/assets/images/workers-ai/inworld.svg"; +import kling from "~/assets/images/workers-ai/kling.svg"; +import leonardo from "~/assets/images/workers-ai/leonardo.svg"; +import luma from "~/assets/images/workers-ai/luma.svg"; +import meta from "~/assets/images/workers-ai/meta.svg"; +import microsoft from "~/assets/images/workers-ai/microsoft.svg"; +import minimax from "~/assets/images/workers-ai/minimax.svg"; +import mistralai from "~/assets/images/workers-ai/mistralai.svg"; +import moonshotai from "~/assets/images/workers-ai/moonshotai.svg"; +import myshell from "~/assets/images/workers-ai/myshell.svg"; +import nvidia from "~/assets/images/workers-ai/nvidia.svg"; +import openai from "~/assets/images/workers-ai/openai.svg"; +import pipecat from "~/assets/images/workers-ai/pipecat.svg"; +import pixverse from "~/assets/images/workers-ai/pixverse.svg"; +import prunaai from "~/assets/images/workers-ai/prunaai.svg"; +import qwen from "~/assets/images/workers-ai/qwen.svg"; +import recraft from "~/assets/images/workers-ai/recraft.svg"; +import replicate from "~/assets/images/workers-ai/replicate.svg"; +import resembleai from "~/assets/images/workers-ai/resemble-ai.svg"; +import runway from "~/assets/images/workers-ai/runway.svg"; +import stabilityai from "~/assets/images/workers-ai/stabilityai.svg"; +import tiiuae from "~/assets/images/workers-ai/tiiuae.svg"; +import unum from "~/assets/images/workers-ai/unum.svg"; +import vidu from "~/assets/images/workers-ai/vidu.svg"; +import xai from "~/assets/images/workers-ai/xai.svg"; +import zai from "~/assets/images/workers-ai/zai.svg"; +import zaiorg from "~/assets/images/workers-ai/zai-org.svg"; + +export const authorData: Record<string, { name: string; logo: string }> = { + alibaba: { + name: "Alibaba", + logo: alibaba.src, + }, + anthropic: { + name: "Anthropic", + logo: anthropic.src, + }, + assemblyai: { + name: "AssemblyAI", + logo: assemblyai.src, + }, + aws: { + name: "AWS", + logo: aws.src, + }, + baai: { + name: "BAAI", + logo: baai.src, + }, + "black-forest-labs": { + name: "Black Forest Labs", + logo: blackforestlabs.src, + }, + bytedance: { + name: "ByteDance", + logo: bytedance.src, + }, + cloudflare: { + name: "Cloudflare", + logo: cloudflare.src, + }, + deepgram: { + name: "Deepgram", + logo: deepgram.src, + }, + "deepseek-ai": { + name: "DeepSeek", + logo: deepseek.src, + }, + defog: { + name: "Defog", + logo: defog.src, + }, + elevenlabs: { + name: "ElevenLabs", + logo: elevenlabs.src, + }, + facebook: { + name: "Meta", + logo: meta.src, + }, + fal: { + name: "fal", + logo: fal.src, + }, + fireworks: { + name: "Fireworks", + logo: fireworks.src, + }, + google: { + name: "Google", + logo: google.src, + }, + huggingface: { + name: "HuggingFace", + logo: huggingface.src, + }, + "ibm-granite": { + name: "IBM", + logo: ibm.src, + }, + ideogram: { + name: "Ideogram", + logo: ideogram.src, + }, + inworld: { + name: "Inworld", + logo: inworld.src, + }, + klingai: { + name: "Kling AI", + logo: kling.src, + }, + leonardo: { + name: "Leonardo", + logo: leonardo.src, + }, + luma: { + name: "Luma", + logo: luma.src, + }, + meta: { + name: "Meta", + logo: meta.src, + }, + "meta-llama": { + name: "Meta", + logo: meta.src, + }, + microsoft: { + name: "Microsoft", + logo: microsoft.src, + }, + minimax: { + name: "MiniMax", + logo: minimax.src, + }, + mistral: { + name: "MistralAI", + logo: mistralai.src, + }, + mistralai: { + name: "MistralAI", + logo: mistralai.src, + }, + moonshotai: { + name: "Moonshot AI", + logo: moonshotai.src, + }, + "myshell-ai": { + name: "MyShell", + logo: myshell.src, + }, + nvidia: { + name: "NVIDIA", + logo: nvidia.src, + }, + openai: { + name: "OpenAI", + logo: openai.src, + }, + "pipecat-ai": { + name: "Pipecat", + logo: pipecat.src, + }, + pixverse: { + name: "PixVerse", + logo: pixverse.src, + }, + pruna: { + name: "Pruna AI", + logo: prunaai.src, + }, + prunaai: { + name: "Pruna AI", + logo: prunaai.src, + }, + qwen: { + name: "Qwen", + logo: qwen.src, + }, + recraft: { + name: "Recraft", + logo: recraft.src, + }, + replicate: { + name: "Replicate", + logo: replicate.src, + }, + "resemble-ai": { + name: "Resemble AI", + logo: resembleai.src, + }, + runwayml: { + name: "RunwayML", + logo: runway.src, + }, + stabilityai: { + name: "Stability.ai", + logo: stabilityai.src, + }, + tiiuae: { + name: "TII UAE", + logo: tiiuae.src, + }, + unum: { + name: "Unum", + logo: unum.src, + }, + vidu: { + name: "Vidu", + logo: vidu.src, + }, + xai: { + name: "xAI", + logo: xai.src, + }, + zai: { + name: "Zhipu AI", + logo: zai.src, + }, + "zai-org": { + name: "Zhipu AI", + logo: zaiorg.src, + }, +}; diff --git a/src/nimbus/components/models/models.client.ts b/src/nimbus/components/models/models.client.ts new file mode 100644 index 00000000000..189c7d2b317 --- /dev/null +++ b/src/nimbus/components/models/models.client.ts @@ -0,0 +1,229 @@ +/** + * Model catalog controller — progressive enhancement for <ModelCatalog>. + * Vanilla DOM (no framework island); re-inits on Astro view transitions via + * `mount`. Shares the Directory grid geometry (grid.ts) so the corner-mark + + * border bookkeeping has a single source of truth. + * + * Filters by N multi-select facets (Tasks / Capabilities / Providers / Authors) + * + name search, sorts pinned-first then newest/oldest, then re-flows the single + * grid: it resolves the column count from the visible cell count, reorders the + * DOM for sort, and recomputes each visible cell's corner marks for its new + * position (so the lined grid + rounded corner squares stay correct under any + * filter). Facet state reflects to the URL with plural repeated keys + * (`?tasks=a&tasks=b`); search persists too; sort is ephemeral. Each facet + * dropdown also has a within-facet search box. + */ +import { mount } from "nimbus-docs/client"; +import { setSearchParams } from "~/util/url"; +import { + LG_GRID_CLASS, + resolveCols, + cornersFor, + cellClass, + cornerSpansHTML, +} from "~/components/directory/grid"; + +function initModels(root: HTMLElement): () => void { + const search = root.querySelector<HTMLInputElement>("[data-models-search]"); + const sortSel = root.querySelector<HTMLSelectElement>("[data-models-sort]"); + const clearBtn = root.querySelector<HTMLButtonElement>("[data-models-clear]"); + const grid = root.querySelector<HTMLElement>("[data-models-grid]"); + const empty = root.querySelector<HTMLElement>("[data-models-empty]"); + const countEl = root.querySelector<HTMLElement>("[data-models-count]"); + const dropdowns = Array.from( + root.querySelectorAll<HTMLDetailsElement>("[data-facet-dropdown]"), + ); + + const checkboxes = Array.from( + root.querySelectorAll<HTMLInputElement>("input[data-facet]"), + ); + const cells = grid + ? Array.from(grid.querySelectorAll<HTMLElement>("[data-models-cell]")) + : []; + + const facetKeys = [ + ...new Set(checkboxes.map((c) => c.dataset.facet ?? "")), + ].filter(Boolean); + + // Below `lg` the grid collapses to a single column, so corner marks must be + // computed for 1 column there to stay aligned. + const lgQuery = window.matchMedia("(min-width: 1024px)"); + const isLg = (): boolean => lgQuery.matches; + + // For the URL: the checkbox `value` (author id / slug / label). + const selectedValues = (key: string): string[] => + checkboxes + .filter((c) => c.dataset.facet === key && c.checked) + .map((c) => c.value); + + // For filtering: the `data-match` value compared against a cell's stamp. + // Equals the value for every facet except Authors (value = id, match = name). + const selectedMatch = (key: string): string[] => + checkboxes + .filter((c) => c.dataset.facet === key && c.checked) + .map((c) => c.dataset.match ?? c.value); + + const cellValues = (cell: HTMLElement, key: string): string[] => + (cell.dataset[`facet${key.charAt(0).toUpperCase()}${key.slice(1)}`] ?? "") + .split("|") + .filter(Boolean); + + // Filter → sort → re-flow grid (columns + corner marks). Returns visible count. + function relayout(): number { + if (!grid) return 0; + + const raw = search?.value ?? ""; + const query = raw.trim().toLowerCase(); + const sel: Record<string, string[]> = {}; + for (const key of facetKeys) sel[key] = selectedMatch(key); + + const matches = cells.filter((cell) => { + const nameOk = !query || (cell.dataset.name ?? "").includes(query); + return ( + nameOk && + facetKeys.every((key) => { + const chosen = sel[key]; + if (chosen.length === 0) return true; + const vals = cellValues(cell, key); + return chosen.some((c) => vals.includes(c)); + }) + ); + }); + + // Pinned-first tier: pinned models sort to the top in BOTH directions, + // ordered by their pin index; the rest sort by date. + const dir = sortSel?.value === "oldest" ? 1 : -1; + matches.sort((a, b) => { + const pa = Number(a.dataset.pinnedIndex ?? -1); + const pb = Number(b.dataset.pinnedIndex ?? -1); + const aPinned = pa >= 0; + const bPinned = pb >= 0; + if (aPinned && !bPinned) return -1; + if (!aPinned && bPinned) return 1; + if (aPinned && bPinned) return pa - pb; + const da = Number(a.dataset.date ?? 0); + const db = Number(b.dataset.date ?? 0); + return da === db ? 0 : (da < db ? -1 : 1) * dir; + }); + + const lgCols = resolveCols(matches.length); + grid.className = LG_GRID_CLASS[lgCols] ?? LG_GRID_CLASS[1]; + const cols = isLg() ? lgCols : 1; + + for (const cell of cells) cell.style.display = "none"; + matches.forEach((cell, i) => { + cell.style.display = ""; + cell.className = cellClass; + grid.appendChild(cell); // reorder DOM so visual order = sorted order + const marks = cell.querySelector<HTMLElement>("[data-corner-marks]"); + if (marks) marks.innerHTML = cornerSpansHTML(cornersFor(i, cols)); + }); + + return matches.length; + } + + function update(syncUrl = true): void { + const visible = relayout(); + const raw = search?.value ?? ""; + // Selection counts (summary badges + clear/empty state) by checked count. + const counts: Record<string, number> = {}; + for (const key of facetKeys) + counts[key] = checkboxes.filter( + (c) => c.dataset.facet === key && c.checked, + ).length; + + for (const key of facetKeys) { + const badge = root.querySelector<HTMLElement>(`[data-facet-badge="${key}"]`); + if (badge) { + const n = counts[key]; + badge.hidden = n === 0; + badge.textContent = String(n); + } + } + + const anyFacet = facetKeys.some((k) => counts[k] > 0); + if (empty) empty.hidden = visible > 0; + if (clearBtn) clearBtn.hidden = !anyFacet; + if (countEl) + countEl.innerHTML = `We found <span class="font-semibold text-foreground">${visible}</span> ${ + visible === 1 ? "model" : "models" + }`; + + if (syncUrl) { + // URL shape: search + plural repeated facet keys (`?tasks=a&tasks=b`). + // Sort is ephemeral — never written. + const params = new URLSearchParams(); + if (raw.trim()) params.set("search", raw); + for (const key of facetKeys) + for (const v of selectedValues(key)) params.append(key, v); + setSearchParams(params); + } + } + + function applyFromUrl(): void { + const params = new URLSearchParams(window.location.search); + if (search) search.value = params.get("search") ?? ""; + for (const key of facetKeys) { + // Repeated keys via getAll. Unknown values (e.g. stale provider slugs) + // are dropped naturally: no matching checkbox exists for them. + const chosen = params.getAll(key); + for (const cb of checkboxes) + if (cb.dataset.facet === key) cb.checked = chosen.includes(cb.value); + } + } + + // Within-facet search: filter each dropdown's option labels + empty state. + function initFacetSearch(): void { + for (const d of dropdowns) { + const filter = d.querySelector<HTMLInputElement>("[data-facet-filter]"); + if (!filter) continue; + const options = Array.from( + d.querySelectorAll<HTMLElement>("[data-facet-option]"), + ); + const emptyEl = d.querySelector<HTMLElement>("[data-facet-empty]"); + filter.addEventListener("input", () => { + const q = filter.value.trim().toLowerCase(); + let any = false; + for (const o of options) { + const show = !q || (o.dataset.label ?? "").toLowerCase().includes(q); + o.style.display = show ? "" : "none"; + if (show) any = true; + } + if (emptyEl) emptyEl.hidden = any; + }); + } + } + + function onClear(): void { + for (const cb of checkboxes) cb.checked = false; + update(); + } + function onDocClick(event: MouseEvent): void { + for (const d of dropdowns) + if (d.open && !d.contains(event.target as Node)) d.open = false; + } + + const onInput = (): void => update(); + const onBreakpoint = (): void => update(false); + search?.addEventListener("input", onInput); + for (const cb of checkboxes) cb.addEventListener("change", onInput); + sortSel?.addEventListener("change", onInput); + clearBtn?.addEventListener("click", onClear); + document.addEventListener("click", onDocClick); + lgQuery.addEventListener("change", onBreakpoint); + + initFacetSearch(); + applyFromUrl(); + update(false); + + return () => { + search?.removeEventListener("input", onInput); + for (const cb of checkboxes) cb.removeEventListener("change", onInput); + sortSel?.removeEventListener("change", onInput); + clearBtn?.removeEventListener("click", onClear); + document.removeEventListener("click", onDocClick); + lgQuery.removeEventListener("change", onBreakpoint); + }; +} + +mount("[data-models]", initModels); diff --git a/src/nimbus/components/models/types.ts b/src/nimbus/components/models/types.ts new file mode 100644 index 00000000000..58aa3f414bb --- /dev/null +++ b/src/nimbus/components/models/types.ts @@ -0,0 +1,25 @@ +/** + * Schema row data, produced at build time by `SchemaDisplay.astro`'s tree walk + * and consumed by the `SchemaTree` island. + */ +export interface SchemaRowData { + id: string; + name: string; + type: string; + isArray: boolean; + isObject: boolean; + isOneOf: boolean; + isOneOfChild: boolean; // This item is a child of a oneOf/anyOf + isFirstOneOfChild: boolean; // First child (no OR divider before) + isLastOneOfChild: boolean; // Last child (no OR divider after, so show border) + required: boolean; + defaultValue?: string; + description?: string; + enumValues?: string[]; + // Generic metadata - all other schema properties (format, min, max, minItems, maxItems, etc.) + metadata?: Record<string, string | number | boolean>; + depth: number; + isLast: boolean; + ancestorIsLast: boolean[]; + children?: SchemaRowData[]; +} diff --git a/src/nimbus/components/react/SubtractIPCalculator.tsx b/src/nimbus/components/react/SubtractIPCalculator.tsx new file mode 100644 index 00000000000..be1b8cdd2c1 --- /dev/null +++ b/src/nimbus/components/react/SubtractIPCalculator.tsx @@ -0,0 +1,125 @@ +import { useState, useSyncExternalStore } from "react"; +import { excludeCidr, parseCidr } from "cidr-tools"; +import { track } from "~/util/zaraz"; +import { cn } from "@/lib/cn"; +import { buttonVariants } from "@/components/ui/button"; + +const inputClass = + "h-9 w-full rounded-md border border-border bg-background px-3 text-sm text-foreground shadow-xs transition-colors placeholder:text-muted-foreground focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-50"; + +const isValidCidr = (cidr: string) => { + try { + parseCidr(cidr); + return true; + } catch { + return false; + } +}; + +const parseList = (value: string) => + value + .split(",") + .map((cidr) => cidr.trim()) + .filter(Boolean); + +const exclude = (base: string, subtract: string[]) => + isValidCidr(base) && subtract.every(isValidCidr) + ? excludeCidr(base, subtract) + : []; + +const useIsHydrated = () => + useSyncExternalStore( + () => () => {}, + () => true, + () => false, + ); + +export default function SubtractIPCalculator({ + defaults, +}: { + defaults: { + base?: string; + subtract?: string[]; + }; +}) { + const [base, setBase] = useState(defaults?.base ?? ""); + // Raw input text (preserves spaces/commas as typed); the parsed CIDR list is + // derived from it during render. + const [subtractInput, setSubtractInput] = useState(() => + (defaults?.subtract ?? []).join(", "), + ); + // The inputs behind the last "Calculate" (seeded from defaults). Results are + // derived from this snapshot, never stored — so there's no state to keep in + // sync. + const [committed, setCommitted] = useState(() => ({ + base: defaults?.base ?? "", + subtract: defaults?.subtract ?? [], + })); + + const subtract = parseList(subtractInput); + const hydrated = useIsHydrated(); + const result = hydrated ? exclude(committed.base, committed.subtract) : []; + const canCalculate = isValidCidr(base) && subtract.every(isValidCidr); + + function calculate() { + setCommitted({ base, subtract }); + track("interacted with docs calculator", { value: "split ip calculator" }); + } + + return ( + <div className="my-4 space-y-5 rounded-lg bg-card p-6 text-foreground no-underline shadow-sm ring ring-border"> + <div className="grid gap-4 sm:grid-cols-2"> + <label className="block"> + <span className="mb-1.5 block text-sm font-medium text-foreground"> + Base CIDR + </span> + <input + type="text" + className={inputClass} + placeholder="10.0.0.0/8" + value={base} + onChange={(e) => setBase(e.target.value)} + /> + </label> + <label className="block"> + <span className="mb-1.5 block text-sm font-medium text-foreground"> + Subtracted CIDRs + </span> + <input + type="text" + className={inputClass} + placeholder="10.0.0.0/24, 10.1.0.0/16" + value={subtractInput} + onChange={(e) => setSubtractInput(e.target.value)} + /> + </label> + </div> + + <button + className={cn(buttonVariants({ variant: "primary" }))} + disabled={!canCalculate} + onClick={calculate} + > + Calculate + </button> + + {result.length > 0 && ( + <div className="space-y-2"> + <span className="block text-sm font-medium text-foreground"> + Results + </span> + <div className="flex flex-wrap gap-1.5"> + {result.map((cidr) => ( + <code + key={cidr} + className="rounded-md bg-muted px-2 py-1 font-mono text-sm text-foreground" + > + {cidr} + </code> + ))} + </div> + </div> + )} + </div> + ); +} diff --git a/src/nimbus/components/react/diagram-showcase/AgentPrimitivesDiagram.tsx b/src/nimbus/components/react/diagram-showcase/AgentPrimitivesDiagram.tsx new file mode 100644 index 00000000000..dbe83f497da --- /dev/null +++ b/src/nimbus/components/react/diagram-showcase/AgentPrimitivesDiagram.tsx @@ -0,0 +1,334 @@ +"use client"; + +import { useCallback, useLayoutEffect, useRef, useState } from "react"; +import { + Diagram, + useDiagram, + useMeasure, + edgePoint, + type EdgeRect, + type Point, +} from "nimbus-docs/react"; +import { DiagramStage, Tabs } from "@/components/react/diagram"; +import { RX, indentedRect, type NodeRect, type NotchConfig } from "@/components/react/diagram/welding"; +import { cn } from "@/lib/cn"; + +/** + * AgentPrimitivesDiagram — the four parts of a Cloudflare Agent compose + * into one card. + * + * Visually identical to www's PrimitivesDiagram (welded chrome, dashed + * boundary, orange wires, external stubs, compose toggle). Four parts: + * Channels in, Harness + SDK Runtime joined in the middle (together they + * are the agent), Tools out. "Composed" snaps the tiles together, fading + * the wires and chrome. + * + * Geometry leans on `nimbus-docs/react` (`edgePoint`, `EdgeRect`); wires + + * stubs + connector dots are derived once into a single `segments` list. + */ + +// ─── Data ────────────────────────────────────────────────── + +const PARTS = [ + { id: "channels", label: "Channels", href: "/agents/communication-channels/" }, + { id: "harness", label: "Harness", href: "/agents/harnesses/" }, + { id: "runtime", label: "SDK Runtime", href: "/agents/runtime/" }, + { id: "tools", label: "Tools", href: "/agents/tools/" }, +] as const; + +type PartId = (typeof PARTS)[number]["id"]; +type Side = "left" | "right" | "top" | "bottom"; + +type Anchor = readonly [PartId, Side, number]; + +/** Internal wires: [from] → [to], drawn at 0.3 opacity with dots both ends. */ +const WIRES: readonly [Anchor, Anchor][] = [ + // Channels fork to both middle tiles (the agent) + [["channels", "right", 0.3], ["harness", "left", 0.5]], + [["channels", "right", 0.7], ["runtime", "left", 0.5]], + // Harness sits on the runtime (stacked) + [["harness", "bottom", 0.5], ["runtime", "top", 0.5]], + // Both middle tiles reach out to the tools + [["harness", "right", 0.5], ["tools", "left", 0.3]], + [["runtime", "right", 0.5], ["tools", "left", 0.7]], +]; + +/** External stubs: a single anchor that runs out to the boundary. */ +const STUBS: readonly Anchor[] = [ + ["channels", "left", 0.5], // users/systems reach in + ["channels", "top", 0.5], + ["harness", "top", 0.5], // outbound model calls + ["runtime", "bottom", 0.5], // durable state / scheduling + ["tools", "right", 0.5], // outbound to MCP / browser / sandbox + ["tools", "bottom", 0.5], +]; + +/** Welded notches — one per wire/stub anchor on the card edge it docks to. */ +const NODE_NOTCHES: Record<PartId, NotchConfig> = (() => { + const map = {} as Record<PartId, Record<Side, number[]>>; + const add = ([id, side, frac]: Anchor) => { + (map[id] ??= { left: [], right: [], top: [], bottom: [] })[side].push(frac); + }; + for (const [from, to] of WIRES) { add(from); add(to); } + for (const s of STUBS) add(s); + const out = {} as Record<PartId, NotchConfig>; + for (const [id, sides] of Object.entries(map) as [PartId, Record<Side, number[]>][]) { + out[id] = Object.fromEntries( + (Object.entries(sides) as [Side, number[]][]).filter(([, f]) => f.length), + ); + } + return out; +})(); + +const BORDER_PAD = 20; +const BRAND = "#f56500"; // orange accent (matches the Queues diagram) + +/** Orthogonal (right-angle) connector; co-linear endpoints collapse to a line. */ +function orthPath(a: Point, b: Point, side: Side): string { + if (side === "left" || side === "right") { + if (Math.abs(a.y - b.y) < 1) return `M ${a.x},${a.y} L ${b.x},${b.y}`; + const mx = (a.x + b.x) / 2; + return `M ${a.x},${a.y} L ${mx},${a.y} L ${mx},${b.y} L ${b.x},${b.y}`; + } + if (Math.abs(a.x - b.x) < 1) return `M ${a.x},${a.y} L ${b.x},${b.y}`; + const my = (a.y + b.y) / 2; + return `M ${a.x},${a.y} L ${a.x},${my} L ${b.x},${my} L ${b.x},${b.y}`; +} + +function toNodeRect(r: EdgeRect): NodeRect { + return { l: r.l, t: r.t, r: r.l + r.w, b: r.t + r.h, w: r.w, h: r.h, cx: r.l + r.w / 2, cy: r.t + r.h / 2 }; +} + +// ─── Public component ────────────────────────────────────── + +export function AgentPrimitivesDiagram({ label = "Agent parts" }: { label?: string }) { + return ( + <Diagram label={label}> + <AgentPrimitivesBody /> + </Diagram> + ); +} + +// ─── Body ────────────────────────────────────────────────── + +const EMPTY_RECT: EdgeRect = { l: 0, t: 0, w: 0, h: 0 }; +type View = "primitives" | "composed"; + +function AgentPrimitivesBody() { + const ctx = useDiagram(); + const [view, setView] = useState<View>("primitives"); + const composed = view === "composed"; + + const containerRef = useRef<HTMLDivElement>(null); + const wrapperRef = useRef<HTMLDivElement>(null); + const cardRefs = useRef<Partial<Record<PartId, HTMLAnchorElement | null>>>({}); + const labelRef = useRef<SVGTextElement>(null); + const [labelSize, setLabelSize] = useState({ w: 0, h: 0 }); + + const selector = useCallback((c: HTMLDivElement) => { + const cr = c.getBoundingClientRect(); + const box = (el: Element): EdgeRect => { + const b = el.getBoundingClientRect(); + return { l: b.left - cr.left, t: b.top - cr.top, w: b.width, h: b.height }; + }; + const rects: Partial<Record<PartId, EdgeRect>> = {}; + for (const p of PARTS) { + const el = cardRefs.current[p.id]; + if (el) rects[p.id] = box(el); + } + return { rects, wrap: wrapperRef.current ? box(wrapperRef.current) : EMPTY_RECT }; + }, []); + + const { selected } = useMeasure(containerRef, selector, { deps: [composed] }); + const rects = selected?.rects ?? {}; + const wrap = selected?.wrap ?? EMPTY_RECT; + + const bx = wrap.l - BORDER_PAD; + const by = wrap.t - BORDER_PAD; + const bw = wrap.w + BORDER_PAD * 2; + const bh = wrap.h + BORDER_PAD * 2; + + const labelText = composed ? "Composed" : "Primitives"; + useLayoutEffect(() => { + try { + const b = labelRef.current?.getBBox(); + if (b) setLabelSize({ w: b.width, h: b.height }); + } catch { + /* not laid out yet */ + } + }, [labelText, bx, by, bw]); + + const hasRects = Object.keys(rects).length === PARTS.length && wrap.w > 0; + + // Reduced-motion: drop transitions. + const transitionMs = ctx?.reducedMotion ? 0 : 700; + const fadeMs = ctx?.reducedMotion ? 0 : 350; + const fade = { transition: `opacity ${fadeMs}ms ease-out` }; + + // Wires + stubs + their connector dots, derived once. + const pt = ([id, side, frac]: Anchor) => { + const r = rects[id]; + return r ? edgePoint(r, side, frac) : null; + }; + const toBoundary = (p: Point, side: Side): Point => + side === "left" ? { x: bx, y: p.y } + : side === "right" ? { x: bx + bw, y: p.y } + : side === "top" ? { x: p.x, y: by } + : { x: p.x, y: by + bh }; + + const segments: { d: string; dots: Point[]; opacity: number }[] = []; + if (hasRects) { + for (const [from, to] of WIRES) { + const a = pt(from); + const b = pt(to); + if (a && b) segments.push({ d: orthPath(a, b, from[1]), dots: [a, b], opacity: 0.3 }); + } + for (const s of STUBS) { + const a = pt(s); + if (a) segments.push({ d: orthPath(a, toBoundary(a, s[1]), s[1]), dots: [a], opacity: 0.2 }); + } + } + + return ( + <div className="relative w-full mt-8"> + {/* Welded-chrome drop-shadow filter. */} + <svg aria-hidden width="0" height="0" style={{ position: "absolute", width: 0, height: 0 }}> + <defs> + <filter id="welded-shadow" x="-4%" y="-4%" width="108%" height="116%"> + <feDropShadow dx="0" dy="1" stdDeviation="1" floodColor="rgb(0,0,0)" floodOpacity="0.06" /> + </filter> + </defs> + </svg> + + <DiagramStage accent={BRAND}> + {/* View toggle */} + <div className="flex items-center justify-end gap-2 p-3"> + <Tabs<View> + options={[ + { id: "primitives", label: "Primitives" }, + { id: "composed", label: "Composed" }, + ]} + active={view} + onChange={setView} + ariaLabel="Diagram view" + /> + </div> + + {/* Viz body — HTML tiles under the SVG chrome. */} + <div className="flex items-center justify-center px-3 pt-6 pb-12 md:px-10 md:pt-10 md:pb-20"> + <div ref={containerRef} className="relative"> + <svg + className="absolute inset-0 w-full h-full pointer-events-none overflow-visible" + style={{ zIndex: 2 }} + aria-hidden + > + {hasRects && ( + <> + {/* Boundary + its label */} + <rect + x={bx} y={by} width={bw} height={bh} rx={RX} + fill="none" stroke="rgba(0,0,0,0.25)" strokeWidth={1} + strokeDasharray={composed ? "none" : "6 4"} + className="dark:stroke-white/[0.15]" + /> + {labelSize.w > 0 && ( + <rect + x={bx + bw / 2 - labelSize.w / 2 - 8} + y={by - labelSize.h / 2 - 1} + width={labelSize.w + 16} + height={labelSize.h + 2} + className="fill-white dark:fill-neutral-950" + /> + )} + <text + ref={labelRef} + x={bx + bw / 2} y={by} + textAnchor="middle" dominantBaseline="middle" letterSpacing="0.15em" + className="fill-neutral-800 dark:fill-neutral-200 font-mono font-medium text-[10px] uppercase select-none" + > + {labelText} + </text> + + {/* Wires + stubs */} + {segments.map((s, i) => ( + <path + key={`seg-${i}`} + d={s.d} + fill="none" stroke={BRAND} strokeWidth={1.5} + strokeLinecap="round" strokeLinejoin="round" + opacity={composed ? 0 : s.opacity} + style={fade} + /> + ))} + + {/* Welded node cards */} + {PARTS.map((p) => { + const r = rects[p.id]; + return r ? ( + <path + key={p.id} + d={indentedRect(toNodeRect(r), NODE_NOTCHES[p.id] ?? {})} + fill="white" stroke="rgba(0,0,0,0.15)" strokeWidth={1} + filter="url(#welded-shadow)" + className="dark:fill-neutral-900" + /> + ) : null; + })} + + {/* Connector dots */} + {segments.flatMap((s, i) => + s.dots.map((d, j) => ( + <rect + key={`dot-${i}-${j}`} + x={d.x - 2.5} y={d.y - 2.5} width={5} height={5} + fill={BRAND} + opacity={composed ? 0 : 1} + style={fade} + /> + )), + )} + </> + )} + </svg> + + {/* HTML tile layer */} + <div + ref={wrapperRef} + className="relative grid items-center justify-items-center" + style={{ + gridTemplateAreas: `"channels harness tools" "channels runtime tools"`, + gridTemplateColumns: "auto auto auto", + gap: composed ? 0 : 20, + transition: `gap ${transitionMs}ms ease-out`, + zIndex: 3, + }} + > + {PARTS.map((p) => ( + <a + key={p.id} + href={p.href} + ref={(el) => { + cardRefs.current[p.id] = el; + }} + style={{ gridArea: p.id }} + className="flex items-center justify-center w-[112px] h-[40px] sm:w-[148px] sm:h-[44px] no-underline" + > + <span + className={cn( + "relative z-10 text-[11px] sm:text-[13px] font-mono font-medium px-2 py-1 sm:px-3 sm:py-1.5 select-none text-center transition-colors duration-500", + composed + ? "text-neutral-400 dark:text-neutral-600" + : "text-neutral-900 dark:text-neutral-100", + )} + > + {p.label} + </span> + </a> + ))} + </div> + </div> + </div> + </DiagramStage> + </div> + ); +} diff --git a/src/nimbus/components/react/diagram/ActionBar.tsx b/src/nimbus/components/react/diagram/ActionBar.tsx new file mode 100644 index 00000000000..370914a7789 --- /dev/null +++ b/src/nimbus/components/react/diagram/ActionBar.tsx @@ -0,0 +1,14 @@ +"use client"; + +import type { ReactNode } from "react"; +import { cn } from "@/lib/cn"; + +export interface ActionBarProps { + children: ReactNode; + className?: string; +} + +/** Flex row, `gap-2`, vertically centred. Layout container for diagram controls. */ +export function ActionBar({ children, className }: ActionBarProps) { + return <div className={cn("flex items-center gap-2", className)}>{children}</div>; +} diff --git a/src/nimbus/components/react/diagram/ActionButton.tsx b/src/nimbus/components/react/diagram/ActionButton.tsx new file mode 100644 index 00000000000..02dbb90a031 --- /dev/null +++ b/src/nimbus/components/react/diagram/ActionButton.tsx @@ -0,0 +1,60 @@ +"use client"; + +import type { ReactNode } from "react"; +import { cn } from "@/lib/cn"; + +export interface ActionButtonProps { + /** Visible label. Strings or icons. */ + label: ReactNode; + onClick: () => void; + disabled?: boolean; + /** Render in the brand-active state. */ + active?: boolean; + /** Tooltip / a11y title. */ + title?: string; + /** `ghost` (default) for toolbar controls; `primary` is the solid accent CTA. */ + variant?: "ghost" | "primary"; + className?: string; +} + +/** + * Ghost button — neutral border, monospace label, click feedback. + * The workhorse control for diagram toolbars (Play / Pause / Reset / + * mode toggles). The `primary` variant fills with the diagram accent + * for a card's main call-to-action. + */ +export function ActionButton({ + label, + onClick, + disabled, + active, + title, + variant = "ghost", + className, +}: ActionButtonProps) { + return ( + <button + type="button" + onClick={onClick} + disabled={disabled} + title={title} + className={cn( + "px-3 py-1.5 text-[10px] font-mono font-medium uppercase tracking-widest", + "border rounded-sm shadow-xs", + "cursor-pointer select-none active:scale-[0.97]", + "disabled:opacity-40 disabled:cursor-not-allowed", + "transition-[background-color,opacity,transform] duration-200 ease-out", + variant === "primary" + ? "text-white border-transparent bg-[var(--diagram-accent,#1447e6)] hover:opacity-90" + : cn( + "border-neutral-200 dark:border-neutral-800", + "bg-white dark:bg-neutral-900 hover:bg-neutral-50 dark:hover:bg-neutral-800", + active && "text-primary bg-primary/[0.08] dark:bg-primary/[0.12]", + ), + className, + )} + > + {label} + </button> + ); +} diff --git a/src/nimbus/components/react/diagram/CardBadge.tsx b/src/nimbus/components/react/diagram/CardBadge.tsx new file mode 100644 index 00000000000..ed301ee4098 --- /dev/null +++ b/src/nimbus/components/react/diagram/CardBadge.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { cn } from "@/lib/cn"; + +/** Accent color token for diagram cards — themed via `--diagram-accent`. */ +export const DIAGRAM_ACCENT = "var(--diagram-accent, #1447e6)"; + +export interface CardBadgeProps { + label: string; + /** `accent` (default) tints the label; `neutral` keeps the accent in the dot only. */ + tone?: "accent" | "neutral"; + /** Position within the relative parent. Default: top-left corner. */ + className?: string; +} + +/** Floating ID pill that sits on a card's top border. */ +export function CardBadge({ label, tone = "accent", className }: CardBadgeProps) { + return ( + <div + className={cn( + "absolute -top-3 left-3 z-10 flex items-center gap-1.5 border border-neutral-300 dark:border-neutral-700 bg-white dark:bg-neutral-900 rounded-sm shadow-xs px-2 py-0.5", + className, + )} + > + <div className="size-1.5" style={{ backgroundColor: DIAGRAM_ACCENT }} /> + <span + className={cn( + "font-mono text-[10px] font-medium uppercase tracking-widest", + tone === "neutral" && "text-neutral-900 dark:text-neutral-100", + )} + style={tone === "accent" ? { color: DIAGRAM_ACCENT } : undefined} + > + {label} + </span> + </div> + ); +} diff --git a/src/nimbus/components/react/diagram/ChipGroup.tsx b/src/nimbus/components/react/diagram/ChipGroup.tsx new file mode 100644 index 00000000000..7784fbbbbfe --- /dev/null +++ b/src/nimbus/components/react/diagram/ChipGroup.tsx @@ -0,0 +1,69 @@ +"use client"; + +import type { ReactNode } from "react"; +import { cn } from "@/lib/cn"; + +export interface ChipOption<T extends string = string> { + id: T; + label: ReactNode; +} + +export interface ChipGroupProps<T extends string = string> { + options: readonly ChipOption<T>[]; + /** Single id, `null` for none, or `Set<id>` for multi-select. */ + active: T | null | ReadonlySet<T>; + onChange: (id: T) => void; + /** Disable the whole group, or per-id via predicate. */ + disabled?: boolean | ((id: T) => boolean); + className?: string; +} + +/** Segmented control — mutually-exclusive or multi-select chips. */ +export function ChipGroup<T extends string = string>({ + options, + active, + onChange, + disabled, + className, +}: ChipGroupProps<T>) { + const isActive = (id: T): boolean => + active instanceof Set ? active.has(id) : active === id; + + const isDisabled = (id: T): boolean => + typeof disabled === "function" ? disabled(id) : Boolean(disabled); + + return ( + <div + className={cn( + "flex border border-neutral-200 dark:border-neutral-800", + "bg-white dark:bg-neutral-900 rounded-sm shadow-xs overflow-hidden", + className, + )} + > + {options.map((opt) => { + const a = isActive(opt.id); + const d = isDisabled(opt.id); + return ( + <button + key={opt.id} + type="button" + onClick={() => !d && onChange(opt.id)} + disabled={d} + className={cn( + "select-none cursor-pointer", + "border-r last:border-r-0 border-neutral-200 dark:border-neutral-800", + "px-2.5 py-1.5 font-mono text-[10px] font-medium uppercase tracking-wider", + "transition-[color,background-color] duration-150 ease-out", + "disabled:opacity-40 disabled:cursor-not-allowed", + a + ? "text-primary bg-primary/[0.08] dark:bg-primary/[0.12]" + : "text-neutral-500 dark:text-neutral-400 hover:bg-neutral-50 dark:hover:bg-neutral-800/40", + )} + > + {opt.label} + </button> + ); + })} + </div> + ); +} diff --git a/src/nimbus/components/react/diagram/DiagramControls.tsx b/src/nimbus/components/react/diagram/DiagramControls.tsx new file mode 100644 index 00000000000..ee0d88f8c08 --- /dev/null +++ b/src/nimbus/components/react/diagram/DiagramControls.tsx @@ -0,0 +1,64 @@ +"use client"; + +import type { ReactNode } from "react"; +import { useDiagram } from "nimbus-docs/react"; +import { cn } from "@/lib/cn"; +import { ActionBar } from "./ActionBar"; +import { ActionButton } from "./ActionButton"; + +export interface DiagramControlsProps { + /** Left-aligned status text (phase labels, step counters). */ + status?: ReactNode; + statusClassName?: string; + /** Render the Play/Pause button. */ + playPause?: boolean; + /** Render the Reset button. */ + reset?: boolean; + /** Extra controls rendered alongside the defaults (mode toggles, etc.). */ + children?: ReactNode; + className?: string; +} + +/** + * Pre-wired toolbar for the surrounding `<Diagram>`. Reads `playing`, + * `toggle`, `reset` from the framework's `useDiagram` hook; the framework + * renders no UI of its own. + * + * Status text sits left, action buttons right. Extras (tabs, chips, + * bespoke run buttons) pass as children and sit alongside the defaults; + * drop the built-ins via `playPause={false}` / `reset={false}` when a + * card supplies its own. + */ +export function DiagramControls({ + status, + statusClassName, + playPause = true, + reset = true, + children, + className, +}: DiagramControlsProps) { + const ctx = useDiagram(); + if (!ctx) return null; + + return ( + <div className={cn("flex items-center justify-between flex-wrap gap-2 p-3", className)}> + {status != null && ( + <span + className={cn( + "font-mono text-[9px] uppercase tracking-widest text-neutral-500 dark:text-neutral-400 select-none", + statusClassName, + )} + > + {status} + </span> + )} + <ActionBar className="ml-auto"> + {playPause && ( + <ActionButton label={ctx.playing ? "Pause" : "Play"} onClick={ctx.toggle} /> + )} + {reset && <ActionButton label="Reset" onClick={ctx.reset} />} + {children} + </ActionBar> + </div> + ); +} diff --git a/src/nimbus/components/react/diagram/DiagramDebug.tsx b/src/nimbus/components/react/diagram/DiagramDebug.tsx new file mode 100644 index 00000000000..202c1be8495 --- /dev/null +++ b/src/nimbus/components/react/diagram/DiagramDebug.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useDiagram } from "nimbus-docs/react"; + +/** + * Signal inspector. Reads DiagramContext and prints every signal — handy + * for verifying the wrapper's lifecycle from SSR + browser. Mount inside + * a `<Diagram>`. + */ +export function DiagramDebug() { + const ctx = useDiagram(); + if (!ctx) { + return ( + <div className="rounded-md border border-amber-300 bg-amber-50 px-3 py-2 font-mono text-xs text-amber-900 dark:border-amber-700 dark:bg-amber-950 dark:text-amber-200"> + <span className="font-medium">DiagramDebug:</span> no context (not wrapped in + <Diagram>). + </div> + ); + } + const rows: Array<[string, string]> = [ + ["id", ctx.id], + ["phase", ctx.phase], + ["playing", String(ctx.playing)], + ["visible", String(ctx.visible)], + ["tabVisible", String(ctx.tabVisible)], + ["reducedMotion", String(ctx.reducedMotion)], + ["depth", ctx.depth], + ["theme", ctx.theme.id], + ]; + return ( + <div className="rounded-md border border-neutral-300 bg-neutral-50 px-3 py-2 font-mono text-xs text-neutral-700 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300"> + <div className="mb-1 font-medium text-neutral-900 dark:text-neutral-100"> + DiagramContext + </div> + <ul className="grid grid-cols-[max-content_1fr] gap-x-3 gap-y-0.5"> + {rows.map(([k, v]) => ( + <li key={k} className="contents"> + <span className="text-neutral-500 dark:text-neutral-500">{k}</span> + <span + className={ + v === "true" || v === "playing" + ? "text-emerald-600 dark:text-emerald-400" + : v === "false" || v === "paused" || v === "idle" + ? "text-neutral-400 dark:text-neutral-500" + : "" + } + > + {v} + </span> + </li> + ))} + </ul> + </div> + ); +} diff --git a/src/nimbus/components/react/diagram/DiagramDefs.tsx b/src/nimbus/components/react/diagram/DiagramDefs.tsx new file mode 100644 index 00000000000..181dbab270c --- /dev/null +++ b/src/nimbus/components/react/diagram/DiagramDefs.tsx @@ -0,0 +1,59 @@ +/** + * Shared SVG defs for diagram cards — one drop-shadow filter + one arrow + * marker, referenced by id from card SVG layers: + * + * filter="url(#diagram-shadow)" subtle card/pill lift + * markerEnd="url(#diagram-arrow)" filled glyph; refX=0 + userSpaceOnUse + * means the glyph extends ~6px forward from the path end — pair with + * routeEdge's arrowOffset (6) so the tip lands flush on the node edge. + * + * Rendered inside each card so cards stay self-contained: an element + * referencing a missing filter id does not render at all (Firefox enforces + * this per spec), so relying on a page-level <defs> block makes a card + * silently disappear when copied to a page without one. ids are idempotent — + * multiple instances on one page resolve to the first definition in document + * order, and all instances are identical. + */ +export function DiagramDefs() { + return ( + <svg + aria-hidden="true" + focusable="false" + width="0" + height="0" + style={{ + position: "absolute", + width: 0, + height: 0, + overflow: "hidden", + pointerEvents: "none", + }} + > + <defs> + <filter id="diagram-shadow" x="-20%" y="-20%" width="140%" height="140%"> + <feDropShadow + dx="0" + dy="1" + stdDeviation="1" + floodColor="rgb(0,0,0)" + floodOpacity="0.06" + /> + </filter> + <marker + id="diagram-arrow" + markerWidth="8" + markerHeight="8" + refX="0" + refY="4" + orient="auto" + markerUnits="userSpaceOnUse" + > + <path + d="M 0,1.5 Q 0,0 1.5,0 Q 3.5,1 5.8,3.2 Q 6.5,4 5.8,4.8 Q 3.5,7 1.5,8 Q 0,8 0,6.5 Z" + fill="currentColor" + /> + </marker> + </defs> + </svg> + ); +} diff --git a/src/nimbus/components/react/diagram/DiagramPauseAll.tsx b/src/nimbus/components/react/diagram/DiagramPauseAll.tsx new file mode 100644 index 00000000000..28fc9d95347 --- /dev/null +++ b/src/nimbus/components/react/diagram/DiagramPauseAll.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { useSyncExternalStore } from "react"; +import { diagramRegistry } from "nimbus-docs/react"; +import { cn } from "@/lib/cn"; + +/** Ghost-button styling — matches `ActionButton`. */ +const PAUSE_ALL_BTN = + "px-3 py-1.5 text-[10px] font-mono font-medium uppercase tracking-widest " + + "border border-neutral-200 dark:border-neutral-800 rounded-sm shadow-xs " + + "bg-white dark:bg-neutral-900 hover:bg-neutral-50 dark:hover:bg-neutral-800 " + + "cursor-pointer select-none active:scale-[0.97] " + + "disabled:opacity-40 disabled:cursor-not-allowed " + + "transition-[background-color,transform] duration-200 ease-out"; + +export interface DiagramPauseAllProps { + className?: string; + label?: string; +} + +/** + * Page-level pause-all control. Subscribes to the module-scoped + * `diagramRegistry` (shared across Astro islands — React Context can't + * bridge them). + * + * Hidden via visibility rather than returning null when count===0: + * Astro 6 + React 19 dev SSR throws "Invalid hook call" when a component + * with scheduled effects returns null, and a stable DOM also avoids layout + * shift when the button appears. + */ +export function DiagramPauseAll({ className, label = "Pause all" }: DiagramPauseAllProps) { + const count = useSyncExternalStore( + diagramRegistry.subscribe, + diagramRegistry.count, + () => 0, + ); + + return ( + <button + type="button" + onClick={() => diagramRegistry.toggleAll()} + data-nb-diagram-pause-all + className={cn(PAUSE_ALL_BTN, className)} + style={count === 0 ? { visibility: "hidden" } : undefined} + aria-hidden={count === 0 ? true : undefined} + > + {label} ({count}) + </button> + ); +} diff --git a/src/nimbus/components/react/diagram/DiagramStage.tsx b/src/nimbus/components/react/diagram/DiagramStage.tsx new file mode 100644 index 00000000000..3ab4919f6f7 --- /dev/null +++ b/src/nimbus/components/react/diagram/DiagramStage.tsx @@ -0,0 +1,56 @@ +"use client"; + +import type { CSSProperties, ReactNode } from "react"; +import { cn } from "@/lib/cn"; + +export interface DiagramStageProps { + children: ReactNode; + /** Override the accent token for everything inside (e.g. a brand color). */ + accent?: string; + className?: string; +} + +/** + * Bordered, dotted-grid canvas for diagram cards — same recipe as the + * flue diagrams. Also registers the shared `diagram-enter` / + * `diagram-progress` keyframes so cards inside need no embedded CSS. + */ +export function DiagramStage({ children, accent, className }: DiagramStageProps) { + return ( + <div + className={cn( + "diagram-stage relative w-full border border-border rounded-lg overflow-hidden", + className, + )} + style={accent ? ({ "--diagram-accent": accent } as CSSProperties) : undefined} + > + <style>{` + @keyframes diagram-enter { + from { opacity: 0; transform: translateX(-10px); } + to { opacity: 1; transform: translateX(0); } + } + @keyframes diagram-progress { + from { width: 0%; } + to { width: 100%; } + } + .diagram-stage { + background-color: var(--nb-background); + background-image: radial-gradient( + circle at 1px 1px, + color-mix(in oklch, var(--nb-border-strong) 65%, transparent) 1px, + transparent 0 + ); + background-size: 14px 14px; + } + [data-mode="dark"] .diagram-stage { + background-image: radial-gradient( + circle at 1px 1px, + color-mix(in oklch, var(--nb-border) 55%, transparent) 1px, + transparent 0 + ); + } + `}</style> + {children} + </div> + ); +} diff --git a/src/nimbus/components/react/diagram/Tabs.tsx b/src/nimbus/components/react/diagram/Tabs.tsx new file mode 100644 index 00000000000..31e30cbacb7 --- /dev/null +++ b/src/nimbus/components/react/diagram/Tabs.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { useCallback, useRef, type ReactNode } from "react"; +import { useTabIndicator } from "nimbus-docs/react"; +import { cn } from "@/lib/cn"; +import type { ChipOption } from "./ChipGroup"; + +export interface TabsProps<T extends string = string> { + options: readonly ChipOption<T>[]; + /** Selected id; `null` for "no tab active". */ + active: T | null; + onChange: (id: T) => void; + disabled?: boolean | ((id: T) => boolean); + ariaLabel?: string; + className?: string; +} + +/** + * Single-select tabs with a sliding floating-pill indicator. Recessed + * track + elevated active pill; mono/uppercase labels. + * + * Pill measurement comes from the framework's `useTabIndicator` hook — + * this component owns the rendering only. + */ +export function Tabs<T extends string = string>({ + options, + active, + onChange, + disabled, + ariaLabel, + className, +}: TabsProps<T>) { + const isDisabled = (id: T): boolean => + typeof disabled === "function" ? disabled(id) : Boolean(disabled); + + const containerRef = useRef<HTMLDivElement>(null); + const tabRefs = useRef<Record<string, HTMLButtonElement | null>>({}); + + const getTab = useCallback( + (id: string) => tabRefs.current[id] ?? null, + [], + ); + + const { style } = useTabIndicator(containerRef, getTab, active); + + return ( + <div + ref={containerRef} + role="tablist" + aria-label={ariaLabel} + className={cn( + "relative inline-flex items-stretch shrink min-w-0 px-0.5 h-8 rounded-lg", + "bg-neutral-100/70 dark:bg-neutral-900/40", + "ring-1 ring-black/[0.06] dark:ring-white/[0.08]", + className, + )} + > + {options.map((opt) => { + const a = active === opt.id; + const d = isDisabled(opt.id); + return ( + <button + key={opt.id} + ref={(el) => { + const key = opt.id as string; + tabRefs.current[key] = el; + return () => { delete tabRefs.current[key]; }; + }} + type="button" + role="tab" + aria-selected={a} + tabIndex={a ? 0 : -1} + onClick={() => !d && onChange(opt.id)} + disabled={d} + className={cn( + "relative z-10 my-0.5 px-2.5", + "flex items-center cursor-pointer select-none whitespace-nowrap", + "rounded-sm font-mono text-[10px] font-medium uppercase tracking-wider", + "bg-transparent transition-colors duration-150", + "focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-neutral-400 dark:focus-visible:ring-neutral-500", + "disabled:opacity-40 disabled:cursor-not-allowed", + a + ? "text-neutral-900 dark:text-neutral-100" + : "text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200", + )} + > + {opt.label} + </button> + ); + })} + {style && ( + <div + role="presentation" + aria-hidden + className={cn( + "absolute z-0 pointer-events-none", + "rounded-sm bg-white dark:bg-neutral-800", + "shadow-sm ring-1 ring-black/[0.06] dark:ring-white/[0.08]", + "transition-all duration-200 ease-out", + )} + style={{ + top: style.top, + left: 0, + width: style.width, + height: style.height, + transform: `translateX(${style.left}px)`, + }} + /> + )} + </div> + ); +} diff --git a/src/nimbus/components/react/diagram/index.ts b/src/nimbus/components/react/diagram/index.ts new file mode 100644 index 00000000000..42f3a9db755 --- /dev/null +++ b/src/nimbus/components/react/diagram/index.ts @@ -0,0 +1,37 @@ +/** + * Diagram UI — user-owned visual components for the headless + * `nimbus-docs/react` primitives. Cards compose framework hooks for + * behaviour with components from this dir for the visuals. Restyle freely. + */ + +export { CardBadge, DIAGRAM_ACCENT } from "./CardBadge"; +export type { CardBadgeProps } from "./CardBadge"; + +export { ActionBar } from "./ActionBar"; +export type { ActionBarProps } from "./ActionBar"; + +export { ActionButton } from "./ActionButton"; +export type { ActionButtonProps } from "./ActionButton"; + +export { ChipGroup } from "./ChipGroup"; +export type { ChipGroupProps, ChipOption } from "./ChipGroup"; + +export { Tabs } from "./Tabs"; +export type { TabsProps } from "./Tabs"; + +export { DiagramControls } from "./DiagramControls"; +export type { DiagramControlsProps } from "./DiagramControls"; + +export { DiagramStage } from "./DiagramStage"; +export type { DiagramStageProps } from "./DiagramStage"; + +export { DiagramDebug } from "./DiagramDebug"; + +export { DiagramPauseAll } from "./DiagramPauseAll"; +export type { DiagramPauseAllProps } from "./DiagramPauseAll"; + +export { DiagramDefs } from "./DiagramDefs"; + +// scene.tsx ships in the separate `diagram-scene` registry slug — do not +// re-export it here, or installing `diagram` alone breaks the barrel. +// Import from "@/components/react/diagram/scene" directly. diff --git a/src/nimbus/components/react/diagram/scene.tsx b/src/nimbus/components/react/diagram/scene.tsx new file mode 100644 index 00000000000..5ff34d5d6e7 --- /dev/null +++ b/src/nimbus/components/react/diagram/scene.tsx @@ -0,0 +1,244 @@ +"use client"; + +import { useRef, type ReactNode } from "react"; +import { + Diagram, + useDiagram, + usePhase, + useMeasure, + useRefSetters, + resolveEdges, + type UsePhaseStep, + type EdgeSpec, + type EdgeRect, + type ResolvedEdge, +} from "nimbus-docs/react"; +import { cn } from "@/lib/cn"; +import { DiagramDefs } from "./DiagramDefs"; + +/** + * Scene — the "data + CSS layout + labels" diagram card. Supply phase + * steps, an active-id lookup table, edge specs, and a layout function that + * places labelled nodes with plain CSS; measurement, edge routing, the SVG + * layer, and active-state styling are handled here. + * + * Built for pill-and-arrow cards. Bespoke cards should compose `<Diagram>` + * + hooks directly, or use the `rects`/`edges`/`active`/`ctx` escape hatch + * the layout receives. + * + * `<Scene>` composes inside an existing `<Diagram>`; `createScene` wraps + * it into a standalone card component. + */ + +const PILL_RX = 6; + +export interface SceneActive<N extends string, E extends string> { + node?: N; + edge?: E; +} + +export interface SceneNodeOptions { + /** `pill` (default) participates in active state; `chip` is a quiet satellite label. */ + variant?: "pill" | "chip"; + /** Render dimmed until active. */ + ghost?: boolean; + /** Replace the default SVG rect for this node (bespoke outlines). */ + shape?: (rect: EdgeRect, active: boolean) => ReactNode; + className?: string; +} + +export interface SceneLayoutApi<N extends string, E extends string> { + /** Place a measured, active-aware node label. */ + node: (id: N, label: ReactNode, opts?: SceneNodeOptions) => ReactNode; + /** Escape hatch: measured rects, routed edges, current active ids. */ + rects: Partial<Record<N, EdgeRect>>; + edges: ResolvedEdge<E>[]; + active: { node: N | null; edge: E | null }; + ctx: ReturnType<typeof useDiagram>; +} + +export interface SceneProps<N extends string, E extends string> { + /** Phase steps, passed straight to `usePhase`. */ + steps: UsePhaseStep[]; + /** Step id → which node/edge lights up while that step holds. */ + active?: Record<string, SceneActive<N, E>>; + /** Declarative edges, resolved against measured node rects. */ + edges?: readonly EdgeSpec<N, E>[]; + layout: (api: SceneLayoutApi<N, E>) => ReactNode; + className?: string; +} + +function measure(el: HTMLElement, container: HTMLElement): EdgeRect { + const c = container.getBoundingClientRect(); + const b = el.getBoundingClientRect(); + return { l: b.left - c.left, t: b.top - c.top, w: b.width, h: b.height }; +} + +export function Scene<N extends string, E extends string>({ + steps, + active, + edges: edgeSpecs = [], + layout, + className, +}: SceneProps<N, E>) { + const ctx = useDiagram(); + const containerRef = useRef<HTMLDivElement>(null); + const nodeRefs = useRef<Partial<Record<N, HTMLDivElement | null>>>({}); + const setNode = useRefSetters(nodeRefs); + + const { selected } = useMeasure( + containerRef, + (el: HTMLDivElement) => { + const rects: Partial<Record<N, EdgeRect>> = {}; + for (const [id, node] of Object.entries(nodeRefs.current) as [ + N, + HTMLDivElement | null, + ][]) { + if (node) rects[id] = measure(node, el); + } + return { rects, edges: resolveEdges(edgeSpecs, rects) }; + }, + // edgeSpecs feeds the selector via closure — re-resolve when the spec + // table itself changes (mode switches that swap edges without resizing). + { deps: [edgeSpecs] }, + ); + + const phase = usePhase<Record<string, never>, SceneActive<N, E>>({ + steps: steps.map((s) => ({ ...s, data: active?.[s.id] })), + loop: true, + }); + const activeNode = phase.data?.node ?? null; + const activeEdge = phase.data?.edge ?? null; + + const rects = selected?.rects ?? {}; + const edges = selected?.edges ?? []; + + const nodeOpts = new Map<N, SceneNodeOptions>(); + const node = (id: N, label: ReactNode, opts?: SceneNodeOptions) => { + if (opts) nodeOpts.set(id, opts); + const chip = opts?.variant === "chip"; + return ( + <div + key={id} + ref={setNode(id)} + className={cn( + "relative z-10 font-mono font-medium uppercase select-none text-center", + chip + ? "text-[10px] tracking-wider px-2 py-1 text-neutral-700 dark:text-neutral-300" + : cn( + "text-xs sm:text-sm px-2.5 py-1.5 sm:px-4 sm:py-2 transition-colors duration-400", + activeNode === id + ? "text-neutral-900 dark:text-neutral-100" + : opts?.ghost + ? "text-neutral-400 dark:text-neutral-600" + : "text-neutral-500 dark:text-neutral-500", + ), + opts?.className, + )} + > + {label} + </div> + ); + }; + + const content = layout({ + node, + rects, + edges, + active: { node: activeNode, edge: activeEdge }, + ctx, + }); + + return ( + <div ref={containerRef} className={cn("relative w-full p-3 md:p-10", className)}> + <DiagramDefs /> + <svg + className="absolute inset-0 w-full h-full pointer-events-none overflow-visible" + aria-hidden="true" + > + {(Object.entries(rects) as [N, EdgeRect][]).map(([id, rect]) => { + const opts = nodeOpts.get(id); + const isActive = activeNode === id; + if (opts?.shape) return <g key={`node-${id}`}>{opts.shape(rect, isActive)}</g>; + if (opts?.variant === "chip") { + return ( + <rect + key={`node-${id}`} + x={rect.l} + y={rect.t} + width={rect.w} + height={rect.h} + rx={PILL_RX} + fill="white" + stroke="rgba(0,0,0,0.1)" + strokeWidth={0.75} + filter="url(#diagram-shadow)" + className="dark:fill-neutral-900" + /> + ); + } + const isGhost = opts?.ghost ?? false; + return ( + <rect + key={`node-${id}`} + x={rect.l} + y={rect.t} + width={rect.w} + height={rect.h} + rx={PILL_RX} + fill="white" + stroke={isActive ? "rgba(0,0,0,0.35)" : "rgba(0,0,0,0.1)"} + strokeWidth={isActive ? 1 : 0.75} + filter="url(#diagram-shadow)" + opacity={isGhost && !isActive ? 0.4 : 1} + className="dark:fill-neutral-900" + style={{ transition: "stroke 400ms ease-out, stroke-width 400ms ease-out" }} + /> + ); + })} + {edges.map((e) => { + const isActive = e.id === activeEdge; + return ( + <path + key={`edge-${e.id}`} + d={e.d} + fill="none" + stroke="currentColor" + strokeWidth={1.5} + strokeLinecap="round" + strokeLinejoin="round" + markerEnd="url(#diagram-arrow)" + className={ + isActive + ? "text-neutral-900 dark:text-neutral-100" + : "text-neutral-300 dark:text-neutral-700" + } + opacity={e.ghost ? (isActive ? 0.7 : 0.3) : 1} + style={{ transition: "color 400ms ease-out, opacity 400ms ease-out" }} + /> + ); + })} + </svg> + {content} + </div> + ); +} + +export interface CreateSceneConfig<N extends string, E extends string> + extends SceneProps<N, E> { + label: string; +} + +/** Wrap a `<Scene>` into a standalone card with its own `<Diagram>`. */ +export function createScene<N extends string, E extends string>({ + label: defaultLabel, + ...sceneProps +}: CreateSceneConfig<N, E>) { + return function SceneCard({ label = defaultLabel }: { label?: string }) { + return ( + <Diagram label={label}> + <Scene {...sceneProps} /> + </Diagram> + ); + }; +} diff --git a/src/nimbus/components/react/diagram/welding.ts b/src/nimbus/components/react/diagram/welding.ts new file mode 100644 index 00000000000..1bb4ef0b16e --- /dev/null +++ b/src/nimbus/components/react/diagram/welding.ts @@ -0,0 +1,177 @@ +/** + * welding.ts — SVG path + measurement helpers for "welded" node cards. + * + * Cards are HTML for content + SVG for chrome. The SVG path tracks the + * HTML box and draws a rounded border with small notches where connectors + * dock or where internal dividers are. + * + * `true` → single notch at center (frac 0.5) + * `[0.33, 0.66]` → two notches at 33% and 66% along the edge + * + * const path = indentedRect(rect, { top: true, left: [0.3, 0.7] }); + * <path d={path} fill="white" stroke="..." strokeWidth={1} /> + * + * `measureRect` and `measureDividerFracs` mirror the per-card pattern + * shared by every flue visualisation: read DOM boxes, hand them to + * `indentedRect`. Live here so each viz can stay focused on its own logic. + */ + +// ── Geometry tokens ───────────────────────────────────────────────────── + +/** Card corner radius (px). Matches `rounded-sm` (~6px). */ +export const RX = 6; +/** Half-width of the notch indentation. */ +export const NOTCH_W = 4; +/** Depth of the notch dip into the card. */ +export const NOTCH_D = 2; + +// ── Types ─────────────────────────────────────────────────────────────── + +export interface NodeRect { + cx: number; + cy: number; + l: number; + r: number; + t: number; + b: number; + w: number; + h: number; +} + +/** Either a boolean (single center notch) or an array of fractional positions. */ +export type NotchSide = boolean | number[]; + +export interface NotchConfig { + top?: NotchSide; + bottom?: NotchSide; + left?: NotchSide; + right?: NotchSide; +} + +// ── Path builder ──────────────────────────────────────────────────────── + +/** + * Resolve a NotchSide to a sorted array of fractional positions (0–1), + * filtered to positions whose notch geometry (`f * edge ± NOTCH_W`) clears + * both corners by `RX`. Notches that would overlap a corner radius are + * silently dropped — the alternative is a malformed path, which is a + * silent visual bug far harder to track down than a missing notch. + * + * `edge` is the length of the side the notch sits on. For top/bottom that's + * the rect width; for left/right it's the height. + */ +function resolveSide(side: NotchSide | undefined, edge: number): number[] { + if (!side) return []; + const raw = side === true ? [0.5] : [...side].sort((a, b) => a - b); + if (edge <= 0) return []; + // A notch spans [f*edge - NOTCH_W, f*edge + NOTCH_W]. To keep clear of a + // corner radius we need that span to sit entirely inside [RX, edge - RX]. + const min = (RX + NOTCH_W) / edge; + const max = 1 - min; + if (min >= max) return []; + return raw.filter((f) => f >= min && f <= max); +} + +/** + * Build a rounded-rect SVG path with optional notches on each edge. + * Each notch is `NOTCH_D` deep and `NOTCH_W * 2` wide. Position is fractional + * along the edge — 0 = top/left, 1 = bottom/right. + */ +export function indentedRect(rect: NodeRect, notches: NotchConfig): string { + const { l, t, r: right, b, w, h } = rect; + const rx = Math.min(RX, w / 2, h / 2); + const nw = NOTCH_W; + const nd = NOTCH_D; + + const top = resolveSide(notches.top, w); + const bottom = resolveSide(notches.bottom, w); + const leftSide = resolveSide(notches.left, h); + const rightSide = resolveSide(notches.right, h); + + const parts: string[] = []; + + // Start at top-left corner (after the radius) + parts.push(`M ${l + rx},${t}`); + + // Top edge — left to right (fracs sorted ascending) + for (const frac of top) { + const nx = l + w * frac; + parts.push(`L ${nx - nw},${t}`); + parts.push(`C ${nx - nw * 0.5},${t} ${nx - nw * 0.4},${t + nd} ${nx},${t + nd}`); + parts.push(`C ${nx + nw * 0.4},${t + nd} ${nx + nw * 0.5},${t} ${nx + nw},${t}`); + } + parts.push(`L ${right - rx},${t}`); + parts.push(`Q ${right},${t} ${right},${t + rx}`); + + // Right edge — top to bottom (fracs sorted ascending) + for (const frac of rightSide) { + const ny = t + h * frac; + parts.push(`L ${right},${ny - nw}`); + parts.push(`C ${right},${ny - nw * 0.5} ${right - nd},${ny - nw * 0.4} ${right - nd},${ny}`); + parts.push(`C ${right - nd},${ny + nw * 0.4} ${right},${ny + nw * 0.5} ${right},${ny + nw}`); + } + parts.push(`L ${right},${b - rx}`); + parts.push(`Q ${right},${b} ${right - rx},${b}`); + + // Bottom edge — right to left (reverse sort) + for (const frac of [...bottom].reverse()) { + const nx = l + w * frac; + parts.push(`L ${nx + nw},${b}`); + parts.push(`C ${nx + nw * 0.5},${b} ${nx + nw * 0.4},${b - nd} ${nx},${b - nd}`); + parts.push(`C ${nx - nw * 0.4},${b - nd} ${nx - nw * 0.5},${b} ${nx - nw},${b}`); + } + parts.push(`L ${l + rx},${b}`); + parts.push(`Q ${l},${b} ${l},${b - rx}`); + + // Left edge — bottom to top (reverse sort) + for (const frac of [...leftSide].reverse()) { + const ny = t + h * frac; + parts.push(`L ${l},${ny + nw}`); + parts.push(`C ${l},${ny + nw * 0.5} ${l + nd},${ny + nw * 0.4} ${l + nd},${ny}`); + parts.push(`C ${l + nd},${ny - nw * 0.4} ${l},${ny - nw * 0.5} ${l},${ny - nw}`); + } + parts.push(`L ${l},${t + rx}`); + parts.push(`Q ${l},${t} ${l + rx},${t}`); + parts.push("Z"); + + return parts.join(" "); +} + +// ── Measurement helpers ───────────────────────────────────────────────── + +/** Measure an element relative to a container, as a NodeRect. */ +export function measureRect(el: HTMLElement, container: HTMLElement): NodeRect { + const c = container.getBoundingClientRect(); + const b = el.getBoundingClientRect(); + return { + cx: b.left - c.left + b.width / 2, + cy: b.top - c.top + b.height / 2, + l: b.left - c.left, + r: b.right - c.left, + t: b.top - c.top, + b: b.bottom - c.top, + w: b.width, + h: b.height, + }; +} + +/** + * Y-fractions (0–1) of `[data-notch]` markers that are direct children of + * the card. Used to align left/right notches with internal divider lines — + * skips dividers that belong to nested cards. + */ +export function measureDividerFracs(card: HTMLElement): number[] { + const cb = card.getBoundingClientRect(); + if (cb.height === 0) return []; + return Array.from(card.querySelectorAll<HTMLElement>("[data-notch]")) + .filter((d) => d.parentElement === card) + .map((d) => { + const dr = d.getBoundingClientRect(); + return (dr.top + dr.height / 2 - cb.top) / cb.height; + }); +} + +/** `[fracs] → fracs` if non-empty, else `true` (single center notch). */ +export function fracsOrCenter(fracs: number[] | undefined): NotchSide { + return fracs && fracs.length > 0 ? fracs : true; +} diff --git a/src/nimbus/components/realtimekit/RTKCodeSnippet/RTKCodeSnippet.astro b/src/nimbus/components/realtimekit/RTKCodeSnippet/RTKCodeSnippet.astro new file mode 100644 index 00000000000..6c690be18a5 --- /dev/null +++ b/src/nimbus/components/realtimekit/RTKCodeSnippet/RTKCodeSnippet.astro @@ -0,0 +1,16 @@ +--- +import { z } from "astro/zod"; + +const props = z.object({ + id: z.union([z.string(), z.array(z.string())]), +}); + +const { id } = props.parse(Astro.props); +const ids = Array.isArray(id) ? id : [id]; +--- + +<div class="rtk-code-snippet hidden" data-rtk-ids={ids.join(",")}> + <slot /> +</div> + +<script src="./RTKCodeSnippet.ts"></script> diff --git a/src/nimbus/components/realtimekit/RTKCodeSnippet/RTKCodeSnippet.ts b/src/nimbus/components/realtimekit/RTKCodeSnippet/RTKCodeSnippet.ts new file mode 100644 index 00000000000..c4ad8ce232e --- /dev/null +++ b/src/nimbus/components/realtimekit/RTKCodeSnippet/RTKCodeSnippet.ts @@ -0,0 +1,45 @@ +const STORAGE_KEY = "realtimekit-sdk-selector"; +const DEFAULT_PLATFORM = "web"; +const DEFAULT_FRAMEWORK_ID = "react"; + +function getActiveId(): string { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw) { + const parsed = JSON.parse(raw); + const platform = parsed.platform === "mobile" ? "mobile" : "web"; + const frameworkId = + parsed.frameworkId || + (platform === "web" ? DEFAULT_FRAMEWORK_ID : "android"); + return `${platform}-${frameworkId}`; + } + } catch { + // Ignore localStorage / JSON errors. + } + return `${DEFAULT_PLATFORM}-${DEFAULT_FRAMEWORK_ID}`; +} + +function updateSnippets(activeId: string) { + document.querySelectorAll<HTMLElement>(".rtk-code-snippet").forEach((el) => { + const ids = (el.dataset.rtkIds || "").split(","); + el.classList.toggle("hidden", !ids.includes(activeId)); + }); +} + +// Reveal the correct snippets on initial load. +updateSnippets(getActiveId()); + +// Re-run after view-transition navigations (if enabled). +document.addEventListener("astro:page-load", () => + updateSnippets(getActiveId()), +); + +// React to framework changes broadcast by RTKSDKSelector. +window.addEventListener("realtimekit-sdk-selector-change", (( + event: CustomEvent, +) => { + const { platform, frameworkId } = event.detail || {}; + if (platform && frameworkId) { + updateSnippets(`${platform}-${frameworkId}`); + } +}) as EventListener); diff --git a/src/nimbus/components/realtimekit/RTKPill/RTKPill.astro b/src/nimbus/components/realtimekit/RTKPill/RTKPill.astro new file mode 100644 index 00000000000..92fc66319d4 --- /dev/null +++ b/src/nimbus/components/realtimekit/RTKPill/RTKPill.astro @@ -0,0 +1,7 @@ +--- +import Pill from "./RTKPill"; +--- + +<Pill client:load> + <slot /> +</Pill> diff --git a/src/nimbus/components/realtimekit/RTKPill/RTKPill.tsx b/src/nimbus/components/realtimekit/RTKPill/RTKPill.tsx new file mode 100644 index 00000000000..ea2c051f1b1 --- /dev/null +++ b/src/nimbus/components/realtimekit/RTKPill/RTKPill.tsx @@ -0,0 +1,25 @@ +import type { ReactNode } from "react"; + +type RTKPillProps = { + children: ReactNode; +}; + +function RTKPill({ children }: RTKPillProps) { + return ( + <span + style={{ + backgroundColor: "#f9d59aff", + borderRadius: "9999px", + padding: "0.1rem 0.5rem", + fontSize: "12px", + margin: "0 4px", + fontWeight: 500, + display: "inline-block", + }} + > + {children} + </span> + ); +} + +export default RTKPill; diff --git a/src/nimbus/components/realtimekit/RTKSDKSelector/RTKSDKSelector.astro b/src/nimbus/components/realtimekit/RTKSDKSelector/RTKSDKSelector.astro new file mode 100644 index 00000000000..84861bdd8ba --- /dev/null +++ b/src/nimbus/components/realtimekit/RTKSDKSelector/RTKSDKSelector.astro @@ -0,0 +1,27 @@ +--- +import { z } from "astro/zod"; +import SDKSelector from "./RTKSDKSelector"; + +const platform = z.enum([ + "web", + "mobile", + "web-react", + "web-web-components", + "web-angular", + "mobile-android", + "mobile-ios", + "mobile-flutter", + "mobile-react-native", +]); + +const props = z.object({ + disabledPlatforms: z + .union([platform, z.array(platform)]) + .optional() + .transform((value) => (typeof value === "string" ? [value] : value)), +}); + +const { disabledPlatforms } = props.parse(Astro.props); +--- + +<SDKSelector client:load disabledPlatforms={disabledPlatforms} /> diff --git a/src/nimbus/components/realtimekit/RTKSDKSelector/RTKSDKSelector.tsx b/src/nimbus/components/realtimekit/RTKSDKSelector/RTKSDKSelector.tsx new file mode 100644 index 00000000000..ec3fd89382d --- /dev/null +++ b/src/nimbus/components/realtimekit/RTKSDKSelector/RTKSDKSelector.tsx @@ -0,0 +1,202 @@ +import { useEffect, useMemo } from "react"; +import { + useFramework, + type Platform, + webFrameworks, + mobileFrameworks, +} from "../hooks/useFramework"; + +interface SDKSelectorProps { + disabledPlatforms?: Platform[]; +} + +export default function SDKSelector({ disabledPlatforms }: SDKSelectorProps) { + const { platform, framework, setSelection } = useFramework(); + + const platforms: { + label: string; + id: Platform; + }[] = [ + { + label: "Web", + id: "web", + }, + { + label: "Mobile", + id: "mobile", + }, + { + label: "React", + id: "web-react", + }, + { + label: "Web Components", + id: "web-web-components", + }, + { + label: "Angular", + id: "web-angular", + }, + { + label: "Android", + id: "mobile-android", + }, + { + label: "iOS", + id: "mobile-ios", + }, + { + label: "Flutter", + id: "mobile-flutter", + }, + { + label: "React Native", + id: "mobile-react-native", + }, + ]; + + const mainPlatforms = platforms.filter( + (p) => p.id === "web" || p.id === "mobile", + ); + + const frameworkToPlatform: Record<string, Platform> = { + react: "web-react", + "web-components": "web-web-components", + angular: "web-angular", + android: "mobile-android", + ios: "mobile-ios", + flutter: "mobile-flutter", + "react-native": "mobile-react-native", + }; + + const frameworks = useMemo( + () => (platform === "web" ? webFrameworks : mobileFrameworks), + [platform], + ); + + const isPlatformDisabled = (p: Platform) => + Boolean(disabledPlatforms?.includes(p)); + + const isFrameworkDisabled = (fw: { id: string; label: string }) => { + const subPlatform = frameworkToPlatform[fw.id]; + return subPlatform ? isPlatformDisabled(subPlatform) : false; + }; + + // Auto-select the first enabled framework when the current one is disabled + useEffect(() => { + if (!framework) return; + if (!isFrameworkDisabled(framework)) return; + + const availableFrameworks = + platform === "web" ? webFrameworks : mobileFrameworks; + const firstEnabled = availableFrameworks.find( + (fw) => !isFrameworkDisabled(fw), + ); + if (firstEnabled) { + setSelection(platform, firstEnabled); + } + }, [platform, framework, disabledPlatforms]); + + const isWebPlatformDisabled = () => { + if (isPlatformDisabled("web")) return true; + + const allWebDisabled = webFrameworks.every((fw) => isFrameworkDisabled(fw)); + return allWebDisabled; + }; + + const isMobilePlatformDisabled = () => { + if (isPlatformDisabled("mobile")) return true; + + const allMobileDisabled = mobileFrameworks.every((fw) => + isFrameworkDisabled(fw), + ); + return allMobileDisabled; + }; + + const activePlatformDisabled = + platform === "web" ? isWebPlatformDisabled() : isMobilePlatformDisabled(); + + const disabledPlatformsString = disabledPlatforms + ?.map((p) => platforms.find((platform) => platform.id === p)?.label || p) + .join(", "); + + return ( + <> + {disabledPlatforms && ( + <div className="flex flex-row gap-1 rounded-md bg-blue-100 p-2 text-blue-900 dark:bg-neutral-800 dark:text-neutral-300"> + This page is not available for the <b>{disabledPlatformsString}</b> + platform. + </div> + )} + <div className="my-5 flex flex-col gap-0 rounded-md bg-blue-100 p-2 dark:bg-neutral-800"> + <div className="flex w-full flex-row items-start justify-start gap-2"> + {mainPlatforms.map((p) => { + const disabled = + p.id === "web" + ? isWebPlatformDisabled() + : p.id === "mobile" + ? isMobilePlatformDisabled() + : isPlatformDisabled(p.id); + + return ( + <button + key={p.id} + type="button" + disabled={disabled} + className={`m-0 ${p.id === platform ? "rounded-t-md bg-neutral-50 text-blue-500 dark:bg-neutral-700" : "bg-blue-100 text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300"} ${disabled ? "cursor-not-allowed opacity-50" : ""} px-2 py-1 font-medium`} + onClick={() => { + if (disabled) return; + const nextPlatform = p.id; + const candidates = + nextPlatform === "web" ? webFrameworks : mobileFrameworks; + const nextFramework = + candidates.find((fw) => { + const sub = frameworkToPlatform[fw.id]; + return sub ? !disabledPlatforms?.includes(sub) : true; + }) ?? candidates[0]; + + setSelection(nextPlatform, nextFramework); + }} + > + {p.label} + </button> + ); + })} + </div> + {activePlatformDisabled && ( + <div className="m-0 w-full rounded-r-md rounded-b-md bg-neutral-50 p-4 text-sm text-gray-700 dark:bg-neutral-700 dark:text-gray-300"> + This page is not available for the {platform} platform. + </div> + )} + {!activePlatformDisabled && frameworks.length < 1 && ( + <div className="m-0 w-full rounded-r-md rounded-b-md bg-neutral-50 p-4 text-sm text-gray-500 italic dark:bg-neutral-700 dark:text-gray-400"> + No frameworks available. + </div> + )} + {!activePlatformDisabled && ( + <div className="m-0 flex w-full flex-row items-center gap-2 rounded-r-md rounded-b-md bg-neutral-50 p-2 text-gray-500 dark:bg-neutral-700 dark:text-gray-400"> + {frameworks.map((fw) => { + const disabled = isFrameworkDisabled(fw); + const handleClick = () => { + if (disabled) return; + setSelection(platform, fw); + }; + + return ( + <button + key={fw.id} + type="button" + disabled={disabled} + className={`m-0 flex ${framework?.id === fw.id ? "text-blue-500 italic" : ""} ${disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer"} text-md items-center rounded-md bg-neutral-50 px-3 py-1 font-medium dark:bg-neutral-700`} + onClick={handleClick} + > + {fw.label} + </button> + ); + })} + </div> + )} + </div> + </> + ); +} diff --git a/src/nimbus/components/realtimekit/RTKUIComponent/RTKUIComponent.astro b/src/nimbus/components/realtimekit/RTKUIComponent/RTKUIComponent.astro new file mode 100644 index 00000000000..8d402868c42 --- /dev/null +++ b/src/nimbus/components/realtimekit/RTKUIComponent/RTKUIComponent.astro @@ -0,0 +1,24 @@ +--- +interface Props { + id: string; + name: string; + imagePath: string; + componentName: string; +} + +const { name, imagePath, componentName, id } = Astro.props; +import UIComponent from "./RTKUIComponent.tsx"; + +// Convert ~ alias to proper import path for Astro +const resolvedImagePath = imagePath.startsWith("~/") + ? imagePath.replace("~/", "/src/") + : imagePath; +--- + +<UIComponent + client:load + id={id} + name={name} + imagePath={resolvedImagePath} + componentName={componentName} +/> diff --git a/src/nimbus/components/realtimekit/RTKUIComponent/RTKUIComponent.tsx b/src/nimbus/components/realtimekit/RTKUIComponent/RTKUIComponent.tsx new file mode 100644 index 00000000000..3dd77b0c012 --- /dev/null +++ b/src/nimbus/components/realtimekit/RTKUIComponent/RTKUIComponent.tsx @@ -0,0 +1,95 @@ +interface Props { + id: string; + name: string; + imagePath: string; + componentName: string; +} + +import { useMemo, useState } from "react"; +import { useFramework } from "../hooks/useFramework"; + +const kebabToPascalCase = (str: string): string => { + return str + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(""); +}; + +const RTKUIComponent = ({ id, imagePath, name, componentName }: Props) => { + const [isExpanded, setIsExpanded] = useState(false); + const { platform, framework } = useFramework(); + + const component = useMemo(() => { + if (platform !== "web") return componentName; + if (framework.id === "react") return kebabToPascalCase(componentName); + return componentName; + }, [platform, framework, componentName]); + + const toggleImageSize = () => { + setIsExpanded(!isExpanded); + }; + + return ( + <div className="mt-2 flex flex-col items-center gap-3" id={id}> + {!isExpanded && ( + <div className="relative"> + <img + src={imagePath} + alt={name} + style={{ border: "solid 1px #ccc", width: "200px" }} + className={`w-full rounded-md transition-all duration-300 ease-in-out`} + /> + <button + onClick={toggleImageSize} + style={{ border: "solid 4px #fff" }} + className={`absolute bottom-0 left-0 flex h-8 w-8 cursor-pointer items-center justify-center rounded-md p-1 text-black`} + > + ⛶ + </button> + </div> + )} + + {isExpanded && ( + <div + className="flex flex-col items-center gap-4 rounded-md p-4" + style={{ + width: "100%", + background: "white", + border: "solid 1px #ccc", + }} + > + <div className="relative"> + <img + src={imagePath} + alt={name} + style={{ + border: "solid 1px #ccc", + width: "100%", + height: "500px", + }} + className={`w-full rounded-md transition-all duration-300 ease-in-out`} + /> + <button + onClick={toggleImageSize} + style={{ border: "solid 4px #fff" }} + className={`absolute bottom-0 left-0 flex h-8 w-8 cursor-pointer items-center justify-center rounded-md p-1 text-black`} + > + − + </button> + </div> + <code className="w-fit rounded-sm bg-gray-100 p-1 dark:bg-neutral-700"> + {component} + </code> + </div> + )} + + {!isExpanded && ( + <code className="w-fit rounded-sm bg-gray-100 p-1 dark:bg-neutral-700"> + {component} + </code> + )} + </div> + ); +}; + +export default RTKUIComponent; diff --git a/src/nimbus/components/realtimekit/RTKUIComponentGrid/RTKUIComponentGrid.astro b/src/nimbus/components/realtimekit/RTKUIComponentGrid/RTKUIComponentGrid.astro new file mode 100644 index 00000000000..c13135dbaa1 --- /dev/null +++ b/src/nimbus/components/realtimekit/RTKUIComponentGrid/RTKUIComponentGrid.astro @@ -0,0 +1,5 @@ +--- +import UIComponentGrid from "./RTKUIComponentGrid.tsx"; +--- + +<UIComponentGrid client:load /> diff --git a/src/nimbus/components/realtimekit/RTKUIComponentGrid/RTKUIComponentGrid.tsx b/src/nimbus/components/realtimekit/RTKUIComponentGrid/RTKUIComponentGrid.tsx new file mode 100644 index 00000000000..7b7aa39d592 --- /dev/null +++ b/src/nimbus/components/realtimekit/RTKUIComponentGrid/RTKUIComponentGrid.tsx @@ -0,0 +1,502 @@ +import { useState, useMemo } from "react"; +import RTKUIComponent from "../RTKUIComponent/RTKUIComponent"; + +const componentGalleryImageModules = import.meta.glob( + "../../../assets/images/realtime/realtimekit/web/components-gallery/*.svg", + { eager: true }, +); + +const componentGalleryImageSrcByFileName = Object.fromEntries( + Object.entries(componentGalleryImageModules).map(([path, mod]) => { + const fileName = path.split("/").pop() as string; + const defaultExport = (mod as any).default; + const src = defaultExport?.src ?? defaultExport; + return [fileName, src]; + }), +) as Record<string, string>; + +const imageSrc = (fileName: string) => + componentGalleryImageSrcByFileName[fileName]; + +const RTKUIComponentGrid = () => { + const [searchTerm, setSearchTerm] = useState(""); + const basicComponents = [ + { + id: "rtk-avatar", + name: "Avatar", + imagePath: imageSrc("rtk-avatar.svg"), + componentName: "rtk-avatar", + tags: ["participant", "tile", "grid"], + }, + { + id: "rtk-audio-visualizer", + name: "Audio Visualizer", + imagePath: imageSrc("rtk-audio-visualizer.svg"), + componentName: "rtk-audio-visualizer", + tags: ["participant", "audio", "visualizer", "grid"], + }, + { + id: "rtk-button", + name: "Button", + imagePath: imageSrc("rtk-button.svg"), + componentName: "rtk-button", + tags: ["button", "controlbar", "controlbar-button"], + }, + { + id: "rtk-clock", + name: "Clock", + imagePath: imageSrc("rtk-clock.svg"), + componentName: "rtk-clock", + tags: ["clock", "header", "sidebar"], + }, + { + id: "rtk-header", + name: "Header", + imagePath: imageSrc("rtk-header.svg"), + componentName: "rtk-header", + tags: ["header", "sidebar"], + }, + { + id: "rtk-logo", + name: "Logo", + imagePath: imageSrc("rtk-logo.svg"), + componentName: "rtk-logo", + tags: ["logo", "header", "sidebar"], + }, + { + id: "rtk-meeting-title", + name: "Meeting Title", + imagePath: imageSrc("rtk-meeting-title.svg"), + componentName: "rtk-meeting-title", + tags: ["meeting-title", "header", "sidebar"], + }, + { + id: "rtk-recording-indicator", + name: "Recording Indicator", + imagePath: imageSrc("rtk-recording-indicator.svg"), + componentName: "rtk-recording-indicator", + tags: ["recording", "indicator", "header", "sidebar", "controlbar"], + }, + { + id: "rtk-spinner", + name: "Spinner", + imagePath: imageSrc("rtk-spinner.svg"), + componentName: "rtk-spinner", + tags: ["spinner", "controlbar", "controlbar-button"], + }, + { + id: "rtk-switch", + name: "Switch", + imagePath: imageSrc("rtk-switch.svg"), + componentName: "rtk-switch", + tags: ["switch", "controlbar", "button"], + }, + { + id: "rtk-tooltip", + name: "Tooltip", + imagePath: imageSrc("rtk-tooltip.svg"), + componentName: "rtk-tooltip", + tags: ["tooltip", "controlbar", "button"], + }, + ]; + const uiComponents = [ + { + id: "rtk-controlbar", + name: "Control Bar", + imagePath: imageSrc("rtk-controlbar.svg"), + componentName: "rtk-controlbar", + tags: ["controlbar", "button"], + }, + { + id: "rtk-controlbar-button", + name: "Control Bar Button", + imagePath: imageSrc("rtk-controlbar-button.svg"), + componentName: "rtk-controlbar-button", + tags: ["controlbar", "button"], + }, + { + id: "rtk-dialog", + name: "Dialog", + imagePath: imageSrc("rtk-dialog.svg"), + componentName: "rtk-dialog", + tags: ["dialog", "modal", "popup"], + }, + { + id: "rtk-emoji-picker", + name: "Emoji Picker", + imagePath: imageSrc("rtk-emoji-picker.svg"), + componentName: "rtk-emoji-picker", + tags: ["emoji-picker", "sidebar", "chat", "message"], + }, + { + id: "rtk-grid-pagination", + name: "Grid Pagination", + imagePath: imageSrc("rtk-grid-pagination.svg"), + componentName: "rtk-grid-pagination", + tags: ["pagination", "grid", "participant", "tile", "header"], + }, + { + id: "rtk-menu", + name: "Menu", + imagePath: imageSrc("rtk-menu.svg"), + componentName: "rtk-menu", + tags: ["menu", "sidebar", "controlbar", "button"], + }, + { + id: "rtk-name-tag", + name: "Name Tag", + imagePath: imageSrc("rtk-name-tag.svg"), + componentName: "rtk-name-tag", + tags: ["name-tag", "participant", "tile", "grid"], + }, + { + id: "rtk-notification", + name: "Notification", + imagePath: imageSrc("rtk-notification.svg"), + componentName: "rtk-notification", + tags: ["notification", "sidebar", "popup", "chat"], + }, + { + id: "rtk-participant-count", + name: "Participant Count", + imagePath: imageSrc("rtk-participant-count.svg"), + componentName: "rtk-participant-count", + tags: ["participant-count", "header", "sidebar"], + }, + { + id: "rtk-participant-tile", + name: "Participant Tile", + imagePath: imageSrc("rtk-participant-tile.svg"), + componentName: "rtk-participant-tile", + tags: ["participant-tile", "participant", "tile", "grid"], + }, + { + id: "rtk-plugin-main", + name: "Plugin Main View", + imagePath: imageSrc("rtk-plugin-main.svg"), + componentName: "rtk-plugin-main", + tags: ["plugin-main", "plugin", "sidebar", "controlbar", "button"], + }, + ]; + const compositeComponents = [ + { + id: "rtk-chat", + name: "Chat", + imagePath: imageSrc("rtk-chat.svg"), + componentName: "rtk-chat", + tags: ["chat", "message", "sidebar"], + }, + { + id: "rtk-grid", + name: "Grid", + imagePath: imageSrc("rtk-grid.svg"), + componentName: "rtk-grid", + tags: ["grid", "participant", "tile", "layout"], + }, + { + id: "rtk-image-viewer", + name: "Image Viewer", + imagePath: imageSrc("rtk-image-viewer.svg"), + componentName: "rtk-image-viewer", + tags: ["image-viewer", "media", "chat", "sidebar"], + }, + { + id: "rtk-leave-meeting", + name: "Leave Meeting", + imagePath: imageSrc("rtk-leave-meeting.svg"), + componentName: "rtk-leave-meeting", + tags: ["leave", "dialog", "modal", "controlbar", "button", "end"], + }, + { + id: "rtk-mixed-grid", + name: "Mixed Grid", + imagePath: imageSrc("rtk-mixed-grid.svg"), + componentName: "rtk-mixed-grid", + tags: ["mixed", "grid", "participant", "tile", "layout"], + }, + { + id: "rtk-participants", + name: "Participants", + imagePath: imageSrc("rtk-participants.svg"), + componentName: "rtk-participants", + tags: ["participants", "sidebar", "list", "participant", "tile"], + }, + { + id: "rtk-participants-audio", + name: "Participants Audio", + imagePath: imageSrc("rtk-participants-audio.svg"), + componentName: "rtk-participants-audio", + tags: ["participants-audio", "audio", "sidebar", "participant", "list"], + }, + { + id: "rtk-plugins", + name: "Plugins", + imagePath: imageSrc("rtk-plugins.svg"), + componentName: "rtk-plugins", + tags: ["plugins", "sidebar", "list", "plugin"], + }, + { + id: "rtk-polls", + name: "Polls", + imagePath: imageSrc("rtk-polls.svg"), + componentName: "rtk-polls", + tags: ["polls", "sidebar", "voting", "interactive"], + }, + { + id: "rtk-screenshare-view", + name: "Screenshare View", + imagePath: imageSrc("rtk-screenshare-view.svg"), + componentName: "rtk-screenshare-view", + tags: ["screenshare-view", "screenshare", "media", "grid"], + }, + { + id: "rtk-settings", + name: "Settings", + imagePath: imageSrc("rtk-settings.svg"), + componentName: "rtk-settings", + tags: [ + "settings", + "sidebar", + "configuration", + "preferences", + "dialog", + "modal", + ], + }, + { + id: "rtk-settings-audio", + name: "Settings Audio", + imagePath: imageSrc("rtk-settings-audio.svg"), + componentName: "rtk-settings-audio", + tags: [ + "settings-audio", + "audio", + "settings", + "sidebar", + "configuration", + "dialog", + "modal", + ], + }, + { + id: "rtk-settings-video", + name: "Settings Video", + imagePath: imageSrc("rtk-settings-video.svg"), + componentName: "rtk-settings-video", + tags: [ + "settings-video", + "video", + "settings", + "sidebar", + "configuration", + "dialog", + "modal", + ], + }, + { + id: "rtk-sidebar", + name: "Sidebar", + imagePath: imageSrc("rtk-sidebar.svg"), + componentName: "rtk-sidebar", + tags: ["sidebar", "layout", "navigation", "panel"], + }, + { + id: "rtk-simple-grid", + name: "Simple Grid", + imagePath: imageSrc("rtk-simple-grid.svg"), + componentName: "rtk-simple-grid", + tags: ["simple", "grid", "participant", "tile", "layout", "basic"], + }, + { + id: "rtk-spotlight-grid", + name: "Spotlight Grid", + imagePath: imageSrc("rtk-spotlight-grid.svg"), + componentName: "rtk-spotlight-grid", + tags: ["spotlight", "grid", "participant", "tile", "layout", "pinned"], + }, + ]; + const screenComponents = [ + { + id: "rtk-ended-screen", + name: "Ended Screen", + imagePath: imageSrc("rtk-ended-screen.svg"), + componentName: "rtk-ended-screen", + tags: ["ended", "screen", "meeting", "end", "leave"], + }, + { + id: "rtk-idle-screen", + name: "Idle Screen", + imagePath: imageSrc("rtk-idle-screen.svg"), + componentName: "rtk-idle-screen", + tags: ["idle", "screen", "waiting", "lobby", "standby"], + }, + { + id: "rtk-meeting", + name: "Meeting Screen", + imagePath: imageSrc("rtk-meeting.svg"), + componentName: "rtk-meeting", + tags: ["meeting", "screen", "main", "active"], + }, + { + id: "rtk-setup-screen", + name: "Setup Screen", + imagePath: imageSrc("rtk-setup-screen.svg"), + componentName: "rtk-setup-screen", + tags: ["setup", "screen", "configuration", "preview"], + }, + ]; + + // Filter function to search through components + const filterComponents = (components: typeof basicComponents) => { + if (!searchTerm.trim()) return components; + + const lowercaseSearch = searchTerm.toLowerCase(); + return components.filter((component) => { + // Search in name + if (component.name.toLowerCase().includes(lowercaseSearch)) return true; + // Search in component name + if (component.componentName.toLowerCase().includes(lowercaseSearch)) + return true; + // Search in tags + if ( + component.tags.some((tag) => + tag.toLowerCase().includes(lowercaseSearch), + ) + ) + return true; + return false; + }); + }; + + // Filtered component arrays + const filteredBasicComponents = useMemo( + () => filterComponents(basicComponents), + [searchTerm], + ); + const filteredUiComponents = useMemo( + () => filterComponents(uiComponents), + [searchTerm], + ); + const filteredCompositeComponents = useMemo( + () => filterComponents(compositeComponents), + [searchTerm], + ); + const filteredScreenComponents = useMemo( + () => filterComponents(screenComponents), + [searchTerm], + ); + + return ( + <div> + <h2 className="mb-2 text-2xl font-bold">Component Gallery</h2> + <p className="mb-4"> + Search through the comoponent gallery for the component you need. + </p> + <input + className="mb-2 w-full rounded-md border bg-neutral-50 p-1 px-2 dark:border-neutral-600 dark:bg-neutral-800" + placeholder="Search for 'Chat'" + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} + /> + + {/* Show no results message if search term exists but no components found */} + {searchTerm.trim() && + filteredBasicComponents.length === 0 && + filteredUiComponents.length === 0 && + filteredCompositeComponents.length === 0 && + filteredScreenComponents.length === 0 && ( + <div className="py-8 text-center"> + <p className="text-gray-500"> + No components found for "{searchTerm}" + </p> + <p className="mt-2 text-sm text-gray-400"> + Try searching for terms like "grid", "chat", "button", or + "settings" + </p> + </div> + )} + + {/* Basic Components */} + {filteredBasicComponents.length > 0 && ( + <> + <h2 className="mb-2 text-2xl font-bold">Basic Components</h2> + <p className="mb-4">Small, reusable building blocks for your UI.</p> + <div className="flex flex-wrap items-start gap-4"> + {filteredBasicComponents.map((component) => ( + <RTKUIComponent + key={component.id} + id={component.id} + name={component.name} + imagePath={component.imagePath} + componentName={component.componentName} + /> + ))} + </div> + </> + )} + + {/* UI Components */} + {filteredUiComponents.length > 0 && ( + <> + <h2 className="mb-2 text-2xl font-bold">UI Components</h2> + <p className="mb-4">Interactive controls and interface elements.</p> + <div className="flex flex-wrap items-start gap-4"> + {filteredUiComponents.map((component) => ( + <RTKUIComponent + key={component.id} + id={component.id} + name={component.name} + imagePath={component.imagePath} + componentName={component.componentName} + /> + ))} + </div> + </> + )} + + {/* Composite Components */} + {filteredCompositeComponents.length > 0 && ( + <> + <h2 className="mb-2 text-2xl font-bold">Composite Components</h2> + <p className="mb-4"> + Complete, feature-rich components combining multiple elements. + </p> + <div className="flex flex-wrap items-start gap-4"> + {filteredCompositeComponents.map((component) => ( + <RTKUIComponent + key={component.id} + id={component.id} + name={component.name} + imagePath={component.imagePath} + componentName={component.componentName} + /> + ))} + </div> + </> + )} + + {/* Screen Components */} + {filteredScreenComponents.length > 0 && ( + <> + <h2 className="mb-2 text-2xl font-bold">Screen Components</h2> + <p className="mb-4"> + Full-screen views for different meeting states. + </p> + <div className="flex flex-wrap items-start gap-4"> + {filteredScreenComponents.map((component) => ( + <RTKUIComponent + key={component.id} + id={component.id} + name={component.name} + imagePath={component.imagePath} + componentName={component.componentName} + /> + ))} + </div> + </> + )} + </div> + ); +}; + +export default RTKUIComponentGrid; diff --git a/src/nimbus/components/realtimekit/RTKicons/RTKIcon.astro b/src/nimbus/components/realtimekit/RTKicons/RTKIcon.astro new file mode 100644 index 00000000000..0e6a432dc86 --- /dev/null +++ b/src/nimbus/components/realtimekit/RTKicons/RTKIcon.astro @@ -0,0 +1,13 @@ +--- +import { z } from "astro/zod"; +import Icon from "./RTKIcon"; + +const props = z.object({ + name: z.string(), + className: z.string().optional(), +}); + +const { name, className } = props.parse(Astro.props); +--- + +<Icon client:load name={name} className={className} /> diff --git a/src/nimbus/components/realtimekit/RTKicons/RTKIcon.tsx b/src/nimbus/components/realtimekit/RTKicons/RTKIcon.tsx new file mode 100644 index 00000000000..41ab111fb14 --- /dev/null +++ b/src/nimbus/components/realtimekit/RTKicons/RTKIcon.tsx @@ -0,0 +1,44 @@ +import type { SVGProps } from "react"; +import icons from "./RTKicons.json"; + +const ICONS = icons as Record< + string, + { + viewBox: string; + paths: string[]; + } +>; + +export type RTKIconName = keyof typeof ICONS; + +interface RTKIconProps extends Omit<SVGProps<SVGSVGElement>, "children"> { + name: RTKIconName; + onClick?: () => void; + className?: string; +} + +function RTKIcon({ name, className, onClick, ...rest }: RTKIconProps) { + const icon = ICONS[name]; + + if (!icon) { + return null; + } + + return ( + <div onClick={onClick}> + <svg + aria-hidden="true" + focusable="false" + viewBox={icon.viewBox} + className={className} + {...rest} + > + {icon.paths.map((d, index) => ( + <path key={index} d={d} /> + ))} + </svg> + </div> + ); +} + +export default RTKIcon; diff --git a/src/nimbus/components/realtimekit/RTKicons/RTKicons.json b/src/nimbus/components/realtimekit/RTKicons/RTKicons.json new file mode 100644 index 00000000000..2598a493336 --- /dev/null +++ b/src/nimbus/components/realtimekit/RTKicons/RTKicons.json @@ -0,0 +1,8 @@ +{ + "copy": { + "viewBox": "0 0 24 24", + "paths": [ + "M5.503 4.627 5.5 6.75v10.504a3.25 3.25 0 0 0 3.25 3.25h8.616a2.251 2.251 0 0 1-2.122 1.5H8.75A4.75 4.75 0 0 1 4 17.254V6.75c0-.98.627-1.815 1.503-2.123ZM17.75 2A2.25 2.25 0 0 1 20 4.25v13a2.25 2.25 0 0 1-2.25 2.25h-9a2.25 2.25 0 0 1-2.25-2.25v-13A2.25 2.25 0 0 1 8.75 2h9Zm0 1.5h-9a.75.75 0 0 0-.75.75v13c0 .414.336.75.75.75h9a.75.75 0 0 0 .75-.75v-13a.75.75 0 0 0-.75-.75Z" + ] + } +} diff --git a/src/nimbus/components/realtimekit/hooks/useFramework.ts b/src/nimbus/components/realtimekit/hooks/useFramework.ts new file mode 100644 index 00000000000..9468ba70189 --- /dev/null +++ b/src/nimbus/components/realtimekit/hooks/useFramework.ts @@ -0,0 +1,181 @@ +import { useEffect, useState } from "react"; +const STORAGE_KEY = "realtimekit-sdk-selector"; + +export type Platform = + | "web" + | "mobile" + | "web-react" + | "web-web-components" + | "web-angular" + | "mobile-android" + | "mobile-ios" + | "mobile-flutter" + | "mobile-react-native"; +export type Framework = { + id: string; + label: string; +}; + +export interface SelectedFramework { + platform: Platform; + framework: Framework; +} + +export const webFrameworks: Framework[] = [ + { + id: "react", + label: "React", + }, + { + id: "web-components", + label: "Web Components", + }, + { + id: "angular", + label: "Angular", + }, +]; +export const mobileFrameworks: Framework[] = [ + { + id: "android", + label: "Android", + }, + { + id: "ios", + label: "iOS", + }, + { + id: "flutter", + label: "Flutter", + }, + { + id: "react-native", + label: "React Native", + }, +]; + +/** + * Shared hook to read and update the currently selected platform/framework. + * + * - Persists selection in localStorage under STORAGE_KEY. + * - Broadcasts changes via the `realtimekit-sdk-selector-change` custom event. + * - Listens to the same event to stay in sync across multiple components. + */ +export function useFramework() { + const [platform, setPlatform] = useState<Platform>("web"); + const [framework, setFramework] = useState<Framework>(webFrameworks[0]); + + // Helper: broadcast selection changes so other listeners can sync. + function notifySelectionChange( + nextPlatform: Platform, + nextFrameworkId: string, + ) { + if (typeof window === "undefined") return; + try { + window.dispatchEvent( + new CustomEvent("realtimekit-sdk-selector-change", { + detail: { platform: nextPlatform, frameworkId: nextFrameworkId }, + }), + ); + } catch { + // Ignore event dispatch errors. + } + } + + // Initialise selection from localStorage (if available) on first render. + useEffect(() => { + if (typeof window === "undefined") return; + + try { + const raw = window.localStorage.getItem(STORAGE_KEY); + if (raw) { + const parsed = JSON.parse(raw) as { + platform?: Platform; + frameworkId?: string; + }; + + const storedPlatform: Platform = + parsed.platform === "mobile" ? "mobile" : "web"; + const availableFrameworks = + storedPlatform === "web" ? webFrameworks : mobileFrameworks; + const selectedFromStore = availableFrameworks.find( + (fw) => fw.id === parsed.frameworkId, + ); + + const nextFramework = selectedFromStore ?? availableFrameworks[0]; + setPlatform(storedPlatform); + setFramework(nextFramework); + notifySelectionChange(storedPlatform, nextFramework.id); + return; + } + } catch { + // Ignore JSON or storage errors and fall back to defaults. + } + + // No stored selection: default to web and its first framework. + setPlatform("web"); + setFramework(webFrameworks[0]); + notifySelectionChange("web", webFrameworks[0].id); + }, []); + + // Keep local state in sync with external changes. + useEffect(() => { + if (typeof window === "undefined") return; + + function handleChange( + event: Event & { + detail?: { platform?: Platform; frameworkId?: Framework["id"] }; + }, + ) { + if (!event.detail?.platform || !event.detail.frameworkId) return; + const nextPlatform = event.detail.platform; + const availableFrameworks = + nextPlatform === "web" ? webFrameworks : mobileFrameworks; + const nextFramework = + availableFrameworks.find((fw) => fw.id === event.detail?.frameworkId) ?? + availableFrameworks[0]; + + setPlatform(nextPlatform); + setFramework(nextFramework); + } + + window.addEventListener( + "realtimekit-sdk-selector-change", + handleChange as EventListener, + ); + + return () => { + window.removeEventListener( + "realtimekit-sdk-selector-change", + handleChange as EventListener, + ); + }; + }, []); + + function updateSelection(nextPlatform: Platform, nextFramework: Framework) { + setPlatform(nextPlatform); + setFramework(nextFramework); + + if (typeof window !== "undefined") { + try { + window.localStorage.setItem( + STORAGE_KEY, + JSON.stringify({ + platform: nextPlatform, + frameworkId: nextFramework.id, + }), + ); + } catch { + // Ignore storage errors. + } + } + + notifySelectionChange(nextPlatform, nextFramework.id); + } + + return { + platform, + framework, + setSelection: updateSelection, + }; +} diff --git a/src/nimbus/components/ui/aside/Aside.astro b/src/nimbus/components/ui/aside/Aside.astro new file mode 100644 index 00000000000..f7ed56c5b1f --- /dev/null +++ b/src/nimbus/components/ui/aside/Aside.astro @@ -0,0 +1,75 @@ +--- +import { Icon } from "astro-icon/components"; +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"aside"> { + type?: "note" | "tip" | "caution" | "danger"; + title?: string; +} + +const { type = "note", title, class: className, ...attrs } = Astro.props; + +const config: Record<string, { label: string; color: string; tint: string }> = { + note: { label: "Note", color: "var(--nb-info)", tint: "var(--nb-info-muted)" }, + tip: { label: "Tip", color: "var(--nb-success)", tint: "var(--nb-success-muted)" }, + caution: { label: "Caution", color: "var(--nb-warning)", tint: "var(--nb-warning-muted)" }, + danger: { label: "Danger", color: "var(--nb-danger)", tint: "var(--nb-danger-muted)" }, +}; + +const c = config[type] ?? config.note; +const displayTitle = title ?? c.label; +--- + +<aside + role="note" + aria-label={displayTitle} + class={cn("aside-card flex items-start gap-3 rounded-lg px-4 py-3 my-4", className)} + style={`--_c: ${c.color}; --_t: ${c.tint};`} + {...attrs} +> + <span class="shrink-0 flex items-center h-[1.375em]" aria-hidden="true"> + {type === "note" && <Icon name="ph:info" class="w-[1em] h-[1em]" />} + {type === "tip" && <Icon name="ph:lightbulb" class="w-[1em] h-[1em]" />} + {type === "caution" && <Icon name="ph:warning" class="w-[1em] h-[1em]" />} + {type === "danger" && <Icon name="ph:warning-circle" class="w-[1em] h-[1em]" />} + </span> + <div class="flex min-w-0 flex-1 flex-col gap-0.5"> + <p class="m-0 text-base font-semibold leading-snug">{displayTitle}</p> + <div class="aside-card-body text-sm leading-normal"> + <slot /> + </div> + </div> +</aside> + +<style> + .aside-card { + border: 1px solid color-mix(in oklch, var(--_c) 25%, transparent); + background: var(--_t); + } + + /* Icon + title stay in semantic color; body text stays readable */ + .aside-card > :global(:first-child) { color: var(--_c); } + .aside-card > :global(:last-child) > :global(:first-child) { color: var(--_c); } + .aside-card-body { color: var(--nb-foreground); } + + .aside-card-body :global(p) { margin: 0; } + .aside-card-body :global(p + p) { margin-top: 0.375rem; } + .aside-card-body :global(a) { text-decoration: underline; text-underline-offset: 2px; } + + /* Inline code only — exclude code inside Expressive Code pre blocks */ + .aside-card-body :global(code:not(:where(pre *))) { + font-family: var(--nb-font-mono); + font-size: 0.8125em; + background: color-mix(in oklch, var(--_c) 10%, transparent); + border: 1px solid color-mix(in oklch, var(--_c) 15%, transparent); + border-radius: 0.25rem; + padding: 0.0625rem 0.3125rem; + } + .aside-card-body :global(strong) { font-weight: 600; color: inherit; } + + /* Shiki code blocks inside Asides */ + .aside-card-body :global(.astro-code) { + margin: 0.75rem 0; + } +</style> diff --git a/src/nimbus/components/ui/aside/index.ts b/src/nimbus/components/ui/aside/index.ts new file mode 100644 index 00000000000..816ef78d041 --- /dev/null +++ b/src/nimbus/components/ui/aside/index.ts @@ -0,0 +1 @@ +export { default as Aside } from "./Aside.astro"; diff --git a/src/nimbus/components/ui/badge/Badge.astro b/src/nimbus/components/ui/badge/Badge.astro new file mode 100644 index 00000000000..e2fdd513eb0 --- /dev/null +++ b/src/nimbus/components/ui/badge/Badge.astro @@ -0,0 +1,42 @@ +--- +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"span"> { + text: string; + variant?: "default" | "info" | "note" | "success" | "tip" | "warning" | "caution" | "danger"; + size?: "small" | "medium" | "large"; +} + +const { text, variant = "default", size = "small", class: className, ...attrs } = Astro.props; + +// note→info, tip→success, caution→warning: Starlight-compatible aliases +const variantClass: Record<string, string> = { + default: "bg-accent text-muted-foreground", + info: "bg-info-muted text-info", + note: "bg-info-muted text-info", + success: "bg-success-muted text-success", + tip: "bg-success-muted text-success", + warning: "bg-warning-muted text-warning", + caution: "bg-warning-muted text-warning", + danger: "bg-danger-muted text-danger", +}; + +const sizeClass: Record<string, string> = { + small: "px-2 py-0.5 text-xs", + medium: "px-2.5 py-0.5 text-[0.8125rem]", + large: "px-3 py-1 text-sm", +}; +--- + +<span + class={cn( + "inline-flex items-center rounded-full font-medium whitespace-nowrap leading-none", + variantClass[variant] ?? variantClass.default, + sizeClass[size] ?? sizeClass.small, + className, + )} + {...attrs} +> + {text} +</span> diff --git a/src/nimbus/components/ui/badge/index.ts b/src/nimbus/components/ui/badge/index.ts new file mode 100644 index 00000000000..2a0ea5df766 --- /dev/null +++ b/src/nimbus/components/ui/badge/index.ts @@ -0,0 +1 @@ +export { default as Badge } from "./Badge.astro"; diff --git a/src/nimbus/components/ui/banner/Banner.astro b/src/nimbus/components/ui/banner/Banner.astro new file mode 100644 index 00000000000..09a5efa068f --- /dev/null +++ b/src/nimbus/components/ui/banner/Banner.astro @@ -0,0 +1,82 @@ +--- +/** + * Banner — announcement strip. Pass `dismissible: { id, days? }` to + * show a close button that persists to localStorage. + */ +import { Icon } from "astro-icon/components"; +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"div"> { + content: string; + type?: "note" | "tip" | "caution" | "danger"; + dismissible?: { id: string; days?: number }; +} + +const { content, type = "note", dismissible, class: className, ...attrs } = Astro.props; + +function sanitizeBannerHtml(value: string): string { + return value + .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "") + .replace(/\s+on[a-z]+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, "") + .replace(/\s+(href|src)\s*=\s*(["'])\s*javascript:[\s\S]*?\2/gi, ""); +} + +const config: Record<string, { color: string; tint: string }> = { + note: { color: "var(--nb-info)", tint: "var(--nb-info-muted)" }, + tip: { color: "var(--nb-success)", tint: "var(--nb-success-muted)" }, + caution: { color: "var(--nb-warning)", tint: "var(--nb-warning-muted)" }, + danger: { color: "var(--nb-danger)", tint: "var(--nb-danger-muted)" }, +}; + +const c = config[type] ?? config.note; +const safeContent = sanitizeBannerHtml(content); +--- + +<div + role={type === "danger" || type === "caution" ? "alert" : "status"} + class={cn("banner-card my-4 flex w-full items-start gap-3 rounded-lg px-4 py-3 text-sm leading-normal text-foreground", className)} + style={`--_c: ${c.color}; --_t: ${c.tint};`} + {...(dismissible + ? { "data-nb-banner-dismiss": dismissible.id, "data-nb-banner-days": dismissible.days } + : {})} + {...attrs} +> + <div class="banner-card-body min-w-0 flex-1"> + <Fragment set:html={safeContent} /> + </div> + {dismissible && ( + <button + type="button" + data-nb-banner-close + class="-my-1 -mr-2 flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-current opacity-60 transition-[background-color,opacity] hover:bg-card/60 hover:opacity-100" + aria-label="Dismiss banner" + > + <Icon name="ph:x" class="w-3.5 h-3.5" /> + </button> + )} +</div> + +<script> + import "./banner.client"; +</script> + +<style> + .banner-card { + border: 1px solid color-mix(in oklch, var(--_c) 25%, transparent); + background: var(--_t); + } + + .banner-card-body :global(p) { margin: 0; } + .banner-card-body :global(p + p) { margin-top: 0.375rem; } + .banner-card-body :global(a) { text-decoration: underline; text-underline-offset: 2px; } + + .banner-card-body :global(code:not(:where(pre *))) { + font-family: var(--nb-font-mono); + font-size: 0.8125em; + background: color-mix(in oklch, var(--_c) 10%, transparent); + border: 1px solid color-mix(in oklch, var(--_c) 15%, transparent); + border-radius: 0.25rem; + padding: 0.0625rem 0.3125rem; + } +</style> diff --git a/src/nimbus/components/ui/banner/banner.client.ts b/src/nimbus/components/ui/banner/banner.client.ts new file mode 100644 index 00000000000..4ee2fda1a55 --- /dev/null +++ b/src/nimbus/components/ui/banner/banner.client.ts @@ -0,0 +1,49 @@ +/** + * Storage key: `nb-banner-dismissed-{id}`. Value is "0" for permanent, + * or a future timestamp (ms) for time-limited dismissal. + */ + +import { mount } from "nimbus-docs/client"; + +const KEY_PREFIX = "nb-banner-dismissed-"; + +function initBanner(banner: HTMLElement): () => void { + const id = banner.dataset.nbBannerDismiss; + if (!id) return () => {}; + + const key = `${KEY_PREFIX}${id}`; + + try { + const stored = localStorage.getItem(key); + if (stored) { + const expiry = Number(stored); + if (expiry === 0 || expiry > Date.now()) { + banner.remove(); + return () => {}; + } + localStorage.removeItem(key); + } + } catch { + // localStorage unavailable; show without persistence. + } + + const btn = banner.querySelector<HTMLButtonElement>("[data-nb-banner-close]"); + if (!btn) return () => {}; + + function handleClick() { + const days = Number(banner.dataset.nbBannerDays) || 0; + const value = days > 0 ? String(Date.now() + days * 86400000) : "0"; + try { + localStorage.setItem(key, value); + } catch { + // localStorage unavailable; dismissal is session-only. + } + banner.remove(); + } + + btn.addEventListener("click", handleClick); + + return () => btn.removeEventListener("click", handleClick); +} + +mount("[data-nb-banner-dismiss]", initBanner); diff --git a/src/nimbus/components/ui/banner/index.ts b/src/nimbus/components/ui/banner/index.ts new file mode 100644 index 00000000000..9b90dcf959b --- /dev/null +++ b/src/nimbus/components/ui/banner/index.ts @@ -0,0 +1 @@ +export { default as Banner } from "./Banner.astro"; diff --git a/src/nimbus/components/ui/breadcrumbs/Breadcrumbs.astro b/src/nimbus/components/ui/breadcrumbs/Breadcrumbs.astro new file mode 100644 index 00000000000..53da5f32afe --- /dev/null +++ b/src/nimbus/components/ui/breadcrumbs/Breadcrumbs.astro @@ -0,0 +1,84 @@ +--- +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; +import type { Breadcrumb } from "nimbus-docs/types"; + +interface Props extends HTMLAttributes<"nav"> { + items: Breadcrumb[]; + /** Max visible crumbs before collapsing middle items (default: 4) */ + maxVisible?: number; +} + +const { items, maxVisible = 4, class: className, ...attrs } = Astro.props; + +// Determine if we need to collapse the middle +const shouldCollapse = items.length > maxVisible; +// When collapsed: Home + first segment + ... + last 2 (parent + current) +const headCount = 2; // Home + first segment +const tailCount = 2; // parent + current page +const headItems = shouldCollapse ? items.slice(0, headCount) : items; +const collapsedItems = shouldCollapse ? items.slice(headCount, items.length - tailCount) : []; +const tailItems = shouldCollapse ? items.slice(items.length - tailCount) : []; +--- + +{items.length > 1 && ( + <nav aria-label="Breadcrumb" class={cn("text-xs font-medium", className)} {...attrs}> + <ol class="flex flex-wrap items-center gap-y-0.5 text-muted-foreground"> + {headItems.map((crumb, i) => ( + <li class="flex items-center"> + {i > 0 && <span class="mx-1.5 text-muted-foreground/50">/</span>} + {(!shouldCollapse && i === items.length - 1) || !crumb.href ? ( + <span class="text-foreground truncate max-w-[12rem]">{crumb.label}</span> + ) : ( + <a href={crumb.href} class="hover:text-foreground transition-colors truncate max-w-[12rem]"> + {crumb.label} + </a> + )} + </li> + ))} + + {shouldCollapse && collapsedItems.length > 0 && ( + <li class="flex items-center"> + <span class="mx-1.5 text-muted-foreground/50">/</span> + <details class="relative group"> + <summary + class="list-none cursor-pointer select-none rounded px-1 py-0.5 transition-colors hover:bg-accent hover:text-foreground [&::-webkit-details-marker]:hidden group-open:before:fixed group-open:before:inset-0 group-open:before:z-[39] group-open:before:cursor-default group-open:before:content-['']" + aria-label={`Show ${collapsedItems.length} more path segments`} + > + <span class="tracking-widest">…</span> + </summary> + <div class="absolute left-0 top-full z-40 mt-1 min-w-[10rem] max-w-[16rem] rounded-lg border border-border bg-card p-1 shadow-lg"> + {collapsedItems.map((crumb) => ( + crumb.href ? ( + <a + href={crumb.href} + class="block truncate rounded-md px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground transition-colors" + > + {crumb.label} + </a> + ) : ( + <span class="block truncate rounded-md px-2.5 py-1.5 text-xs text-muted-foreground"> + {crumb.label} + </span> + ) + ))} + </div> + </details> + </li> + )} + + {shouldCollapse && tailItems.map((crumb, i) => ( + <li class="flex items-center"> + <span class="mx-1.5 text-muted-foreground/50">/</span> + {i === tailItems.length - 1 || !crumb.href ? ( + <span class="text-foreground truncate max-w-[12rem]">{crumb.label}</span> + ) : ( + <a href={crumb.href} class="hover:text-foreground transition-colors truncate max-w-[12rem]"> + {crumb.label} + </a> + )} + </li> + ))} + </ol> + </nav> +)} diff --git a/src/nimbus/components/ui/breadcrumbs/index.ts b/src/nimbus/components/ui/breadcrumbs/index.ts new file mode 100644 index 00000000000..734e2089641 --- /dev/null +++ b/src/nimbus/components/ui/breadcrumbs/index.ts @@ -0,0 +1 @@ +export { default as Breadcrumbs } from "./Breadcrumbs.astro"; diff --git a/src/nimbus/components/ui/button/Button.astro b/src/nimbus/components/ui/button/Button.astro new file mode 100644 index 00000000000..256bedee146 --- /dev/null +++ b/src/nimbus/components/ui/button/Button.astro @@ -0,0 +1,71 @@ +--- +/** + * Button — primary action trigger. Astro port of the kumo React Button + * (@cloudflare/kumo), mapped to Nimbus tokens. + * + * <Button variant="primary">Save</Button> + * <Button variant="secondary" icon="ph:plus">Create</Button> + * <Button variant="ghost" loading>Saving…</Button> + * <Button variant="outline" shape="square" icon="ph:gear" aria-label="Settings" /> + * + * Variants: primary · secondary (default) · ghost · destructive · + * secondary-destructive · outline. + * Sizes: xs · sm · base (default) · lg. + * Shapes: base (default) · square · circle (icon-only — pass `aria-label`). + * + * `icon` takes an iconify name (astro-icon) rendered before the label; + * `loading` swaps it for a spinner and disables the button. + * + * Styling lives in `./variants` (shared with LinkButton). For an anchor + * styled as a button, use `~/components/ui/link-button`. + */ +import { cn } from "@/lib/cn"; +import { Icon } from "astro-icon/components"; +import type { HTMLAttributes } from "astro/types"; +import { + buttonVariants, + buttonIconSize, + type ButtonVariant, + type ButtonSize, + type ButtonShape, +} from "./variants"; + +interface Props extends Omit<HTMLAttributes<"button">, "size"> { + variant?: ButtonVariant; + size?: ButtonSize; + shape?: ButtonShape; + /** Iconify name rendered before the label, e.g. "ph:plus". */ + icon?: string; + /** Show a spinner and disable interaction. */ + loading?: boolean; +} + +const { + variant = "secondary", + size = "base", + shape = "base", + icon, + loading = false, + type = "button", + disabled, + class: className, + ...attrs +} = Astro.props; +--- + +<button + type={type} + disabled={disabled || loading} + data-nb-button + class={cn(buttonVariants({ variant, size, shape }), className)} + {...attrs} +> + { + loading ? ( + <Icon name="ph:circle-notch" class={cn("animate-spin", buttonIconSize[size])} /> + ) : ( + icon && <Icon name={icon} class={buttonIconSize[size]} /> + ) + } + <slot /> +</button> diff --git a/src/nimbus/components/ui/button/index.ts b/src/nimbus/components/ui/button/index.ts new file mode 100644 index 00000000000..32440d6b622 --- /dev/null +++ b/src/nimbus/components/ui/button/index.ts @@ -0,0 +1,13 @@ +export { default as Button } from "./Button.astro"; +export { + buttonVariants, + buttonBase, + buttonVariantClasses, + buttonSizeText, + buttonSizeCompact, + buttonIconSize, + type ButtonVariant, + type ButtonSize, + type ButtonShape, + type ButtonVariantsOptions, +} from "./variants"; diff --git a/src/nimbus/components/ui/button/variants.ts b/src/nimbus/components/ui/button/variants.ts new file mode 100644 index 00000000000..bafa43a0285 --- /dev/null +++ b/src/nimbus/components/ui/button/variants.ts @@ -0,0 +1,81 @@ +/** + * Shared button styling — the single source of truth for both <Button> + * (a real button) and <LinkButton> (an anchor styled as a button), so the + * two stay visually identical. Inspired by kumo's `buttonVariants`. + * + * Token-mapped to Nimbus. Import `buttonVariants()` to compose the trigger + * classes for a button-shaped element; `buttonIconSize` sizes a leading/ + * trailing icon for a given size. + */ +import { cn } from "@/lib/cn"; + +export type ButtonVariant = + | "primary" + | "secondary" + | "ghost" + | "destructive" + | "secondary-destructive" + | "outline"; +export type ButtonSize = "xs" | "sm" | "base" | "lg"; +export type ButtonShape = "base" | "square" | "circle"; + +// `rounded-full` is the default radius for every button; `square` overrides +// it (see `buttonVariants`), `circle` keeps it. +export const buttonBase = + "group inline-flex w-max shrink-0 items-center justify-center rounded-full font-medium whitespace-nowrap no-underline shadow-xs transition-colors cursor-pointer select-none focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-50"; + +export const buttonVariantClasses: Record<ButtonVariant, string> = { + primary: "bg-primary text-primary-foreground hover:bg-primary-hover", + secondary: + "bg-card text-foreground ring ring-border hover:bg-accent hover:ring-border-strong", + ghost: "bg-transparent text-foreground shadow-none hover:bg-accent", + destructive: "bg-danger text-white hover:bg-danger/90", + "secondary-destructive": + "bg-card text-danger ring ring-border hover:bg-accent hover:ring-danger/40", + outline: + "bg-transparent text-foreground ring ring-border hover:ring-border-strong", +}; + +// Rectangular sizing (shape="base"). Radius comes from `buttonBase`. +export const buttonSizeText: Record<ButtonSize, string> = { + xs: "h-5 gap-1 px-1.5 text-xs", + sm: "h-7 gap-1 px-2 text-xs", + base: "h-9 gap-1.5 px-3 text-sm", + lg: "h-10 gap-2 px-4 text-sm", +}; + +// Square/circle sizing (icon-only): equal dimensions, no horizontal padding. +export const buttonSizeCompact: Record<ButtonSize, string> = { + xs: "size-5", + sm: "size-7", + base: "size-9", + lg: "size-10", +}; + +export const buttonIconSize: Record<ButtonSize, string> = { + xs: "h-3.5 w-3.5", + sm: "h-3.5 w-3.5", + base: "h-4 w-4", + lg: "h-[1.125rem] w-[1.125rem]", +}; + +export interface ButtonVariantsOptions { + variant?: ButtonVariant; + size?: ButtonSize; + shape?: ButtonShape; +} + +/** Compose the base + variant + size/shape classes for a button-shaped element. */ +export function buttonVariants({ + variant = "secondary", + size = "base", + shape = "base", +}: ButtonVariantsOptions = {}): string { + // base + circle inherit `rounded-full` from buttonBase; square overrides it + // to a rounded square. + const dims = + shape === "base" + ? buttonSizeText[size] + : cn(buttonSizeCompact[size], "p-0", shape === "square" && "rounded-lg"); + return cn(buttonBase, buttonVariantClasses[variant], dims); +} diff --git a/src/nimbus/components/ui/card-grid/CardGrid.astro b/src/nimbus/components/ui/card-grid/CardGrid.astro new file mode 100644 index 00000000000..a7c50fb7326 --- /dev/null +++ b/src/nimbus/components/ui/card-grid/CardGrid.astro @@ -0,0 +1,15 @@ +--- +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +type Props = HTMLAttributes<"div">; + +const { class: className, ...attrs } = Astro.props; +--- + +<div + class={cn("grid grid-cols-1 gap-4 my-4 sm:grid-cols-2 [&>*]:my-0", className)} + {...attrs} +> + <slot /> +</div> diff --git a/src/nimbus/components/ui/card-grid/index.ts b/src/nimbus/components/ui/card-grid/index.ts new file mode 100644 index 00000000000..417cc1fd3a7 --- /dev/null +++ b/src/nimbus/components/ui/card-grid/index.ts @@ -0,0 +1 @@ +export { default as CardGrid } from "./CardGrid.astro"; diff --git a/src/nimbus/components/ui/card/Card.astro b/src/nimbus/components/ui/card/Card.astro new file mode 100644 index 00000000000..f4f268a64a6 --- /dev/null +++ b/src/nimbus/components/ui/card/Card.astro @@ -0,0 +1,21 @@ +--- +import { Icon } from "astro-icon/components"; +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"article"> { + title: string; + /** Iconify icon name, e.g. `ph:lightning`. */ + icon?: string; +} + +const { title, icon, class: className, ...attrs } = Astro.props; +--- + +<article class={cn("my-4 rounded-lg bg-card p-5 shadow-sm ring ring-border", className)} {...attrs}> + {icon && <Icon name={icon} class="mb-2.5 block w-6 h-6 text-foreground" />} + <h3 class="m-0 text-base font-semibold leading-snug text-foreground">{title}</h3> + <div class="mt-1.5 text-sm leading-normal text-muted-foreground"> + <slot /> + </div> +</article> diff --git a/src/nimbus/components/ui/card/index.ts b/src/nimbus/components/ui/card/index.ts new file mode 100644 index 00000000000..b491741f307 --- /dev/null +++ b/src/nimbus/components/ui/card/index.ts @@ -0,0 +1 @@ +export { default as Card } from "./Card.astro"; diff --git a/src/nimbus/components/ui/checkbox/Checkbox.astro b/src/nimbus/components/ui/checkbox/Checkbox.astro new file mode 100644 index 00000000000..5d2f46b2187 --- /dev/null +++ b/src/nimbus/components/ui/checkbox/Checkbox.astro @@ -0,0 +1,92 @@ +--- +/** + * Checkbox — accessible checkbox control. Astro port of the kumo React + * Checkbox (@cloudflare/kumo, built on Base UI), mapped to Nimbus tokens. + * + * Built on a real <input type="checkbox"> for free keyboard + screen-reader + * support. The visible box and check/minus glyphs are siblings of the input + * and are driven entirely by CSS `peer-*` state — no JS, except a one-line + * inline script to seed `indeterminate`, which the HTML spec only exposes as + * a JS property. + * + * <Checkbox label="Email notifications" name="prefs" value="email" /> + * <Checkbox label="Enable" controlFirst={false} /> // label before box + * <Checkbox label="Select all" indeterminate /> + * <Checkbox aria-label="Select row" /> // no visible label + * + * Group related checkboxes under a shared legend with <CheckboxGroup>. + */ +import { cn } from "@/lib/cn"; +import { Icon } from "astro-icon/components"; +import type { HTMLAttributes } from "astro/types"; +import { checkboxVariants, type CheckboxVariant } from "./variants"; + +interface Props extends Omit<HTMLAttributes<"input">, "size"> { + /** Visible label. Omit only when passing `aria-label` / `aria-labelledby`. */ + label?: string; + /** Visual variant. @default "default" */ + variant?: CheckboxVariant; + /** Box before label (default) or after. @default true */ + controlFirst?: boolean; + /** Seed the indeterminate ("mixed") state. */ + indeterminate?: boolean; +} + +const { + label, + variant = "default", + controlFirst = true, + indeterminate = false, + checked, + disabled, + class: className, + id, + ...attrs +} = Astro.props; + +// Stable id so the inline indeterminate-seeding script can target this input. +const inputId = id ?? `cb-${Math.random().toString(36).slice(2, 10)}`; +--- + +<label + class={cn( + "group/checkbox m-0 inline-flex items-start gap-2", + !controlFirst && "flex-row-reverse justify-end", + disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer", + className, + )} +> + <span class={cn("relative inline-flex size-4 shrink-0", label && "mt-0.5")}> + <input + type="checkbox" + id={inputId} + checked={checked} + disabled={disabled} + data-indeterminate={indeterminate ? "true" : undefined} + class="peer absolute inset-0 z-20 m-0 size-4 cursor-[inherit] appearance-none opacity-0" + {...attrs} + /> + <span aria-hidden="true" class={checkboxVariants({ variant })}></span> + {/* Glyphs are siblings of the input (not children of the box) so the + `peer-*` combinator can reach them. */} + <Icon + name="ph:check-bold" + class="pointer-events-none absolute inset-0 z-10 m-auto size-3 text-background opacity-0 transition-opacity peer-checked:opacity-100 peer-indeterminate:opacity-0" + /> + <Icon + name="ph:minus-bold" + class="pointer-events-none absolute inset-0 z-10 m-auto size-3 text-background opacity-0 transition-opacity peer-indeterminate:opacity-100" + /> + </span> + {label && <span class="text-sm text-foreground select-none">{label}</span>} +</label> + +{/* `indeterminate` is a JS-only property — seed it once the element exists. */} +{ + indeterminate && ( + <script is:inline define:vars={{ inputId }}> + var el = document.getElementById(inputId); + if (el) el.indeterminate = true; + </script> + ) +} diff --git a/src/nimbus/components/ui/checkbox/CheckboxGroup.astro b/src/nimbus/components/ui/checkbox/CheckboxGroup.astro new file mode 100644 index 00000000000..e2bc34d97ad --- /dev/null +++ b/src/nimbus/components/ui/checkbox/CheckboxGroup.astro @@ -0,0 +1,59 @@ +--- +/** + * CheckboxGroup — a labelled <fieldset> wrapping related <Checkbox>es so the + * set has one screen-reader-announced name. Astro port of kumo's + * Checkbox.Group (@cloudflare/kumo). The controlled value / allValues bookkeeping is + * the consumer's job here — Astro only renders the static, grouped markup. + * + * <CheckboxGroup legend="Filter by category"> + * <Checkbox label="Storage" name="group" value="storage" /> + * <Checkbox label="AI" name="group" value="ai" /> + * </CheckboxGroup> + * + * Pass `legendClass` to restyle the legend (e.g. an uppercase rail label), or + * `srOnlyLegend` to keep it for assistive tech only. + */ +import { cn } from "@/lib/cn"; + +interface Props { + /** Visible group label. */ + legend?: string; + /** Override the legend's classes (otherwise a medium foreground label). */ + legendClass?: string; + /** Visually hide the legend (still announced to assistive tech). */ + srOnlyLegend?: boolean; + /** Helper text shown beneath the group. */ + description?: string; + /** Error message shown beneath the group. */ + error?: string; + class?: string; +} + +const { + legend, + legendClass, + srOnlyLegend = false, + description, + error, + class: className, +} = Astro.props; +--- + +<fieldset class={cn("m-0 flex min-w-0 flex-col gap-3 border-0 p-0", className)}> + { + legend && ( + <legend + class={cn( + "float-none mb-1 p-0 text-sm font-medium text-foreground", + srOnlyLegend && "sr-only", + legendClass, + )} + > + {legend} + </legend> + ) + } + <div class="flex flex-col gap-2"><slot /></div> + {error && <p class="m-0 text-sm text-danger">{error}</p>} + {description && <p class="m-0 text-sm text-muted-foreground">{description}</p>} +</fieldset> diff --git a/src/nimbus/components/ui/checkbox/index.ts b/src/nimbus/components/ui/checkbox/index.ts new file mode 100644 index 00000000000..141d0c4c4e1 --- /dev/null +++ b/src/nimbus/components/ui/checkbox/index.ts @@ -0,0 +1,8 @@ +export { default as Checkbox } from "./Checkbox.astro"; +export { default as CheckboxGroup } from "./CheckboxGroup.astro"; +export { + checkboxVariants, + checkboxBox, + checkboxVariantRing, + type CheckboxVariant, +} from "./variants"; diff --git a/src/nimbus/components/ui/checkbox/variants.ts b/src/nimbus/components/ui/checkbox/variants.ts new file mode 100644 index 00000000000..c91d94ba46b --- /dev/null +++ b/src/nimbus/components/ui/checkbox/variants.ts @@ -0,0 +1,35 @@ +/** + * Shared checkbox styling — the visual "box" that sits behind the check / + * minus glyph. Inspired by kumo's `checkboxVariants` (@cloudflare/kumo), token-mapped + * to Nimbus. + * + * The box is a sibling of a visually-hidden native <input>, so every stateful + * style is expressed through Tailwind's `peer-*` variants (checked, hover, + * focus-visible, indeterminate, disabled). Kumo token map: + * bg-kumo-base → bg-background ring-kumo-hairline → ring-border + * bg-kumo-contrast → bg-foreground ring-kumo-brand → ring-ring + * text-kumo-inverse → text-background (the glyph, set on the icon) + * ring-kumo-danger → ring-danger + */ +import { cn } from "@/lib/cn"; + +export type CheckboxVariant = "default" | "error"; + +// Geometry + state styles common to both variants. `ring` is a 1px hairline; +// focus-visible bumps it to 2px in the focus colour. +export const checkboxBox = + "pointer-events-none absolute inset-0 z-0 rounded-sm bg-background ring transition-[box-shadow,background-color,opacity] peer-disabled:opacity-50 peer-focus-visible:ring-2 peer-focus-visible:ring-ring"; + +// Per-variant ring + filled (checked / indeterminate) colours. +export const checkboxVariantRing: Record<CheckboxVariant, string> = { + default: + "ring-border peer-hover:ring-border-strong peer-checked:bg-foreground peer-checked:ring-foreground peer-indeterminate:bg-foreground peer-indeterminate:ring-foreground", + error: + "ring-danger peer-checked:bg-danger peer-checked:ring-danger peer-indeterminate:bg-danger peer-indeterminate:ring-danger", +}; + +export function checkboxVariants({ + variant = "default", +}: { variant?: CheckboxVariant } = {}) { + return cn(checkboxBox, checkboxVariantRing[variant]); +} diff --git a/src/nimbus/components/ui/code/Code.astro b/src/nimbus/components/ui/code/Code.astro new file mode 100644 index 00000000000..d4fae4aa138 --- /dev/null +++ b/src/nimbus/components/ui/code/Code.astro @@ -0,0 +1,31 @@ +--- +/** + * Code — syntax-highlighted code block from a string prop. + * + * <Code code={generated} lang="ts" /> + * <Code code={...} lang="ts" meta='title="src/foo.ts" {1,3-5}' /> + */ +import { Code as AstroCode } from "astro:components"; +import { defaultCodeTransformers } from "nimbus-docs"; + +type Props = Parameters<typeof AstroCode>[0]; +const rawProps = Astro.props as Props; +const usesNimbusDefaultThemes = !("theme" in rawProps) && !("themes" in rawProps); +const themed = usesNimbusDefaultThemes + ? { + ...rawProps, + themes: { light: "github-light", dark: "github-dark" }, + defaultColor: false, + } + : rawProps; +const userTransformers = themed.transformers ?? []; +const props = { + ...themed, + transformers: defaultCodeTransformers({ + classTokens: usesNimbusDefaultThemes, + beforeTitleTransformers: userTransformers, + }), +}; +--- + +<AstroCode {...props} /> diff --git a/src/nimbus/components/ui/code/index.ts b/src/nimbus/components/ui/code/index.ts new file mode 100644 index 00000000000..152859d832f --- /dev/null +++ b/src/nimbus/components/ui/code/index.ts @@ -0,0 +1 @@ +export { default as Code } from "./Code.astro"; diff --git a/src/nimbus/components/ui/collapsible/Collapsible.astro b/src/nimbus/components/ui/collapsible/Collapsible.astro new file mode 100644 index 00000000000..24b51c2dcfd --- /dev/null +++ b/src/nimbus/components/ui/collapsible/Collapsible.astro @@ -0,0 +1,25 @@ +--- +/** Collapsible — disclosure. Compose with CollapsibleTrigger + CollapsibleContent. */ +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"div"> { + /** Start open. Default false. */ + open?: boolean; +} + +const { open = false, class: className, ...attrs } = Astro.props; +--- + +<div + data-nb-collapsible + data-nb-default-open={open ? "true" : undefined} + class={cn(className)} + {...attrs} +> + <slot /> +</div> + +<script> + import "./collapsible.client"; +</script> diff --git a/src/nimbus/components/ui/collapsible/CollapsibleContent.astro b/src/nimbus/components/ui/collapsible/CollapsibleContent.astro new file mode 100644 index 00000000000..e8b76860d09 --- /dev/null +++ b/src/nimbus/components/ui/collapsible/CollapsibleContent.astro @@ -0,0 +1,27 @@ +--- +/** + * CollapsibleContent — the panel that animates open/closed. + * Uses `grid-template-rows: 0fr → 1fr` for smooth height transition. + */ +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"div"> {} + +const { class: className, ...attrs } = Astro.props; +--- + +<div + data-nb-collapsible-content + class={cn( + "grid grid-rows-[0fr] transition-[grid-template-rows] duration-250 ease-[cubic-bezier(0.87,0,0.13,1)]", + "data-[nb-state=open]:grid-rows-[1fr]", + "motion-reduce:transition-none", + className, + )} + {...attrs} +> + <div class="overflow-hidden min-h-0"> + <slot /> + </div> +</div> diff --git a/src/nimbus/components/ui/collapsible/CollapsibleTrigger.astro b/src/nimbus/components/ui/collapsible/CollapsibleTrigger.astro new file mode 100644 index 00000000000..d5f8cea9db3 --- /dev/null +++ b/src/nimbus/components/ui/collapsible/CollapsibleTrigger.astro @@ -0,0 +1,21 @@ +--- +/** + * CollapsibleTrigger — the button that toggles the Collapsible. + * Slot accepts arbitrary content; component author provides full visuals. + */ +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"button"> {} + +const { class: className, type, ...attrs } = Astro.props; +--- + +<button + type={type ?? "button"} + data-nb-collapsible-trigger + class={cn("w-full text-left cursor-pointer select-none", className)} + {...attrs} +> + <slot /> +</button> diff --git a/src/nimbus/components/ui/collapsible/collapsible.client.ts b/src/nimbus/components/ui/collapsible/collapsible.client.ts new file mode 100644 index 00000000000..c62d87ce0c4 --- /dev/null +++ b/src/nimbus/components/ui/collapsible/collapsible.client.ts @@ -0,0 +1,32 @@ +/** Wires Collapsible via the disclosure module. */ + +import { mount, makeDisclosure } from "nimbus-docs/client"; + +declare global { + interface HTMLElement { + __nbDisclosure?: ReturnType<typeof makeDisclosure>; + } +} + +function initCollapsible(root: HTMLElement): () => void { + const trigger = root.querySelector<HTMLElement>("[data-nb-collapsible-trigger]"); + const content = root.querySelector<HTMLElement>("[data-nb-collapsible-content]"); + + if (!trigger || !content) return () => {}; + + const defaultOpen = root.dataset.nbDefaultOpen === "true"; + + const disclosure = makeDisclosure({ + trigger, + content, + defaultOpen, + }); + root.__nbDisclosure = disclosure; + + return () => { + delete root.__nbDisclosure; + disclosure.destroy(); + }; +} + +mount("[data-nb-collapsible]", initCollapsible); diff --git a/src/nimbus/components/ui/collapsible/index.ts b/src/nimbus/components/ui/collapsible/index.ts new file mode 100644 index 00000000000..a4c422168f2 --- /dev/null +++ b/src/nimbus/components/ui/collapsible/index.ts @@ -0,0 +1,3 @@ +export { default as Collapsible } from "./Collapsible.astro"; +export { default as CollapsibleTrigger } from "./CollapsibleTrigger.astro"; +export { default as CollapsibleContent } from "./CollapsibleContent.astro"; diff --git a/src/nimbus/components/ui/combobox/Combobox.astro b/src/nimbus/components/ui/combobox/Combobox.astro new file mode 100644 index 00000000000..e2fcb7258ec --- /dev/null +++ b/src/nimbus/components/ui/combobox/Combobox.astro @@ -0,0 +1,227 @@ +--- +/** + * Combobox — a typeahead/autocomplete: an editable input that filters a + * dropdown list as you type, with a clear button, an empty state, and a + * check on the selected row. + * + * Astro port of the kumo React Combobox (@cloudflare/kumo, built on Base UI). Same + * composition shape, driven by a vanilla controller (combobox.client.ts) + * instead of React, and styled with Nimbus tokens. Single-select. + * + * import { Combobox, ComboboxOption, ComboboxGroup, ComboboxGroupLabel } + * from "@/components/ui/combobox"; + * + * <Combobox label="Fruit" name="fruit" placeholder="Search…" + * value="apple" displayValue="Apple"> + * <ComboboxOption value="apple">Apple</ComboboxOption> + * <ComboboxGroup> + * <ComboboxGroupLabel>Citrus</ComboboxGroupLabel> + * <ComboboxOption value="orange">Orange</ComboboxOption> + * <ComboboxOption value="lemon" disabled>Lemon</ComboboxOption> + * </ComboboxGroup> + * </Combobox> + * + * Behaviour: type to filter (case-insensitive substring on the option label), + * ↑/↓/Home/End/Enter/Esc, clear button, click-outside close (reverts the + * input to the selected label), above/below flip, optional hidden + * `<input name>` for form posts, and a bubbling `combobox:change` CustomEvent + * (`detail: { value, label }`). + * + * Accessible name: pass `label` (visible) or `aria-label` (hidden). + */ +import { cn } from "@/lib/cn"; +import { Icon } from "astro-icon/components"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends Omit<HTMLAttributes<"div">, "size"> { + /** Form field name — when set, a hidden input mirrors the selected value. */ + name?: string; + /** Initially selected value (matches an option's `value`). */ + value?: string; + /** Initial text shown in the input (the selected option's label). Avoids + * the placeholder flash before the controller resolves the label. */ + displayValue?: string; + /** Placeholder shown when the input is empty. */ + placeholder?: string; + /** Visible label rendered above the input. */ + label?: string; + /** Text shown when no option matches the filter. */ + emptyText?: string; + /** Trigger size. Mirrors kumo's input sizes. @default "base" */ + size?: "sm" | "base" | "lg"; + disabled?: boolean; + /** When `false`, appends an "(optional)" hint to the label. */ + required?: boolean; + /** Helper text below the input. */ + description?: string; + /** Error message below the input (also flags the input). */ + error?: string; +} + +const { + name, + value, + displayValue, + placeholder = "Search…", + label, + emptyText = "No results found.", + size = "base", + disabled = false, + required, + description, + error, + class: className, + id: idProp, + "aria-label": ariaLabel, + ...attrs +} = Astro.props; + +const id = idProp ?? `nb-combobox-${crypto.randomUUID().slice(0, 8)}`; +const inputId = `${id}-input`; +const labelId = `${id}-label`; +const listId = `${id}-list`; +const descId = description ? `${id}-desc` : undefined; +const errId = error ? `${id}-err` : undefined; + +const sizeInput = { + sm: "h-8 pl-2.5 pr-10 text-[0.8125rem]", + base: "h-9 pl-3 pr-12 text-sm", + lg: "h-10 pl-3.5 pr-14 text-base", +}[size]; + +const icon = { + sm: { size: "h-3.5 w-3.5", clear: "right-7", caret: "right-1.5" }, + base: { size: "h-4 w-4", clear: "right-8", caret: "right-2" }, + lg: { size: "h-[1.125rem] w-[1.125rem]", clear: "right-9", caret: "right-3" }, +}[size]; +--- + +<div + data-nb-combobox + data-value={value ?? ""} + class={cn("grid gap-1.5", className)} + {...attrs} +> + { + label && ( + <label + id={labelId} + for={inputId} + class="text-sm font-medium text-foreground select-none" + > + {label} + {required === false && ( + <span class="ml-1 font-normal text-muted-foreground">(optional)</span> + )} + </label> + ) + } + + <div class="relative"> + <input + id={inputId} + type="text" + role="combobox" + autocomplete="off" + aria-autocomplete="list" + aria-expanded="false" + aria-controls={listId} + aria-labelledby={label ? labelId : undefined} + aria-label={!label ? ariaLabel : undefined} + aria-describedby={[descId, errId].filter(Boolean).join(" ") || undefined} + data-nb-combobox-input + placeholder={placeholder} + value={displayValue} + disabled={disabled} + class={cn( + "w-full rounded-lg border border-border bg-background font-normal text-foreground", + "transition-colors hover:border-border-strong placeholder:text-muted-foreground", + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset", + "disabled:cursor-not-allowed disabled:opacity-50", + error && "border-danger focus-visible:ring-danger", + sizeInput, + )} + /> + + <button + type="button" + data-nb-combobox-clear + aria-label="Clear" + hidden + class={cn( + "absolute top-1/2 flex -translate-y-1/2 cursor-pointer items-center justify-center rounded-sm bg-transparent p-0 text-muted-foreground hover:text-foreground", + icon.clear, + )} + > + <Icon name="ph:x" class={icon.size} /> + </button> + + <button + type="button" + data-nb-combobox-trigger + aria-label="Show options" + tabindex="-1" + class={cn( + "absolute top-1/2 flex -translate-y-1/2 cursor-pointer items-center justify-center bg-transparent p-0 text-muted-foreground", + icon.caret, + )} + > + <Icon name="ph:caret-down" class={icon.size} /> + </button> + + <div + data-nb-combobox-popup + hidden + class={cn( + "absolute right-0 left-0 z-50 mt-1 flex max-h-72 flex-col overflow-hidden rounded-lg border border-border bg-card py-1.5 shadow-lg", + "data-[placement=top]:top-auto data-[placement=top]:bottom-full data-[placement=top]:mt-0 data-[placement=top]:mb-1", + )} + > + <div + id={listId} + role="listbox" + data-nb-combobox-list + aria-labelledby={label ? labelId : undefined} + class="min-h-0 flex-1 overflow-y-auto overscroll-contain" + > + <slot /> + </div> + <div + data-nb-combobox-empty + hidden + class="mx-1.5 px-4 py-2 text-sm leading-tight text-muted-foreground" + > + {emptyText} + </div> + </div> + + { + name && ( + <input + type="hidden" + data-nb-combobox-value-input + name={name} + value={value ?? ""} + /> + ) + } + </div> + + { + error ? ( + <span id={errId} class="text-sm text-danger"> + {error} + </span> + ) : ( + description && ( + <span id={descId} class="text-sm leading-snug text-muted-foreground"> + {description} + </span> + ) + ) + } +</div> + +<script> + import "./combobox.client"; +</script> diff --git a/src/nimbus/components/ui/combobox/ComboboxGroup.astro b/src/nimbus/components/ui/combobox/ComboboxGroup.astro new file mode 100644 index 00000000000..dfe696c59d1 --- /dev/null +++ b/src/nimbus/components/ui/combobox/ComboboxGroup.astro @@ -0,0 +1,25 @@ +--- +/** + * ComboboxGroup — groups related options with role="group" and a top divider + * between groups. Pair with <ComboboxGroupLabel>. Port of kumo's + * Combobox.Group. + */ +import { cn } from "@/lib/cn"; + +interface Props { + class?: string; +} + +const { class: className } = Astro.props; +--- + +<div + role="group" + data-nb-combobox-group + class={cn( + "mt-2 border-t border-border pt-2 first:mt-0 first:border-t-0 first:pt-0", + className, + )} +> + <slot /> +</div> diff --git a/src/nimbus/components/ui/combobox/ComboboxGroupLabel.astro b/src/nimbus/components/ui/combobox/ComboboxGroupLabel.astro new file mode 100644 index 00000000000..cc0d496565d --- /dev/null +++ b/src/nimbus/components/ui/combobox/ComboboxGroupLabel.astro @@ -0,0 +1,23 @@ +--- +/** + * ComboboxGroupLabel — heading for a <ComboboxGroup>. Port of kumo's + * Combobox.GroupLabel. + */ +import { cn } from "@/lib/cn"; + +interface Props { + class?: string; +} + +const { class: className } = Astro.props; +--- + +<div + data-nb-combobox-group-label + class={cn( + "mx-1.5 px-2 py-1.5 text-xs font-semibold text-muted-foreground select-none", + className, + )} +> + <slot /> +</div> diff --git a/src/nimbus/components/ui/combobox/ComboboxOption.astro b/src/nimbus/components/ui/combobox/ComboboxOption.astro new file mode 100644 index 00000000000..71976ef4a07 --- /dev/null +++ b/src/nimbus/components/ui/combobox/ComboboxOption.astro @@ -0,0 +1,40 @@ +--- +/** + * ComboboxOption — one filterable, selectable row. Port of kumo's + * Combobox.Item. The label text is both the display value and the string the + * typeahead filters on (the check indicator is excluded). + */ +import { cn } from "@/lib/cn"; +import { Icon } from "astro-icon/components"; + +interface Props { + /** Value reported when this option is chosen. */ + value: string; + disabled?: boolean; + class?: string; +} + +const { value, disabled = false, class: className } = Astro.props; +--- + +<div + role="option" + data-nb-combobox-option + data-value={value} + aria-selected="false" + data-disabled={disabled ? "" : undefined} + class={cn( + "group mx-1.5 grid grid-cols-[1fr_1rem] items-center gap-2 rounded px-2 py-1.5 text-sm text-foreground outline-none select-none", + "cursor-pointer data-[highlighted]:bg-accent aria-selected:font-medium", + "data-[disabled]:pointer-events-none data-[disabled]:opacity-50", + className, + )} +> + <span data-nb-combobox-option-label class="col-start-1 min-w-0 truncate"> + <slot /> + </span> + <Icon + name="ph:check" + class="col-start-2 h-4 w-4 shrink-0 opacity-0 group-aria-selected:opacity-100" + /> +</div> diff --git a/src/nimbus/components/ui/combobox/combobox.client.ts b/src/nimbus/components/ui/combobox/combobox.client.ts new file mode 100644 index 00000000000..9d1ca91f818 --- /dev/null +++ b/src/nimbus/components/ui/combobox/combobox.client.ts @@ -0,0 +1,305 @@ +/** + * Combobox controller — progressive enhancement for the <Combobox> typeahead. + * Vanilla DOM (no framework); re-inits on Astro view transitions via `mount`. + * + * Owns: type-to-filter (case-insensitive substring on the option label), + * open/close, keyboard nav (↑/↓/Home/End/Enter/Esc), clear button, empty + * state, group hide-when-empty, above/below flip, value sync (input text + + * aria-selected + hidden input + `data-value`), and a bubbling + * `combobox:change` CustomEvent (`detail: { value, label }`). + */ +import { mount } from "nimbus-docs/client"; + +let uid = 0; + +function initCombobox(root: HTMLElement): () => void { + const input = root.querySelector<HTMLInputElement>("[data-nb-combobox-input]"); + const popup = root.querySelector<HTMLElement>("[data-nb-combobox-popup]"); + const list = root.querySelector<HTMLElement>("[data-nb-combobox-list]"); + const emptyEl = root.querySelector<HTMLElement>("[data-nb-combobox-empty]"); + const clearBtn = root.querySelector<HTMLButtonElement>( + "[data-nb-combobox-clear]", + ); + const triggerBtn = root.querySelector<HTMLButtonElement>( + "[data-nb-combobox-trigger]", + ); + const valueInput = root.querySelector<HTMLInputElement>( + "[data-nb-combobox-value-input]", + ); + if (!input || !popup || !list || !emptyEl) return () => {}; + + const scope = `nb-combobox-${uid++}`; + const allOptions = () => + Array.from(list.querySelectorAll<HTMLElement>("[data-nb-combobox-option]")); + const groups = () => + Array.from(list.querySelectorAll<HTMLElement>("[data-nb-combobox-group]")); + const visibleEnabled = () => + allOptions().filter((o) => !o.hidden && !o.hasAttribute("data-disabled")); + + allOptions().forEach((o, i) => { + if (!o.id) o.id = `${scope}-opt-${i}`; + }); + + let open = false; + let activeEl: HTMLElement | null = null; + + const labelOf = (o: HTMLElement): string => + ( + o.querySelector("[data-nb-combobox-option-label]")?.textContent ?? + o.textContent ?? + "" + ).trim(); + + const selectedOption = (): HTMLElement | null => { + const v = root.dataset.value || ""; + if (!v) return null; + return allOptions().find((o) => o.dataset.value === v) ?? null; + }; + + function syncClearVisibility(): void { + if (clearBtn) clearBtn.hidden = !(input!.value.length > 0); + } + + function setActive(o: HTMLElement | null): void { + if (activeEl) activeEl.removeAttribute("data-highlighted"); + activeEl = o; + if (o) { + o.setAttribute("data-highlighted", ""); + input!.setAttribute("aria-activedescendant", o.id); + o.scrollIntoView({ block: "nearest" }); + } else { + input!.removeAttribute("aria-activedescendant"); + } + } + + /** Show/hide options by substring match; hide empty groups; toggle empty. */ + function applyFilter(query: string): void { + const q = query.trim().toLowerCase(); + let anyVisible = false; + for (const o of allOptions()) { + const match = q === "" || labelOf(o).toLowerCase().includes(q); + o.hidden = !match; + if (match) anyVisible = true; + } + for (const g of groups()) { + const hasVisible = g.querySelector( + '[data-nb-combobox-option]:not([hidden])', + ); + g.hidden = !hasVisible; + } + emptyEl!.hidden = anyVisible; + // Keep the active row valid. + if (!activeEl || activeEl.hidden) { + const selected = selectedOption(); + setActive( + selected && !selected.hidden ? selected : (visibleEnabled()[0] ?? null), + ); + } + } + + function placePopup(): void { + popup!.removeAttribute("data-placement"); + const rect = input!.getBoundingClientRect(); + const spaceBelow = window.innerHeight - rect.bottom; + const needed = Math.min(popup!.scrollHeight, 288) + 8; + if (spaceBelow < needed && rect.top > spaceBelow) { + popup!.setAttribute("data-placement", "top"); + } + } + + function openPopup(showAll = true): void { + if (open || input!.disabled) return; + open = true; + popup!.hidden = false; + input!.setAttribute("aria-expanded", "true"); + if (showAll) applyFilter(""); + else applyFilter(input!.value); + placePopup(); + const selected = selectedOption(); + setActive( + selected && !selected.hidden ? selected : (visibleEnabled()[0] ?? null), + ); + document.addEventListener("pointerdown", onDocPointer, true); + } + + function closePopup(): void { + if (!open) return; + open = false; + popup!.hidden = true; + input!.setAttribute("aria-expanded", "false"); + setActive(null); + document.removeEventListener("pointerdown", onDocPointer, true); + // Reconcile the input text with the committed selection (the typeahead is + // select-from-list, not free text). + const selected = selectedOption(); + input!.value = selected ? labelOf(selected) : ""; + syncClearVisibility(); + } + + function commit(value: string | null, emit = true): void { + let selected: HTMLElement | null = null; + for (const o of allOptions()) { + const isSel = value != null && o.dataset.value === value; + o.setAttribute("aria-selected", isSel ? "true" : "false"); + if (isSel) selected = o; + } + root.dataset.value = selected ? (value ?? "") : ""; + if (valueInput) valueInput.value = selected ? (value ?? "") : ""; + if (selected) input!.value = labelOf(selected); + syncClearVisibility(); + if (emit) { + root.dispatchEvent( + new CustomEvent("combobox:change", { + bubbles: true, + detail: { + value: selected ? value : null, + label: selected ? labelOf(selected) : null, + }, + }), + ); + } + } + + function choose(o: HTMLElement): void { + if (o.hasAttribute("data-disabled")) return; + commit(o.dataset.value ?? null); + closePopup(); + input!.focus(); + } + + function clear(): void { + commit(null); + input!.value = ""; + syncClearVisibility(); + applyFilter(""); + input!.focus(); + openPopup(); + } + + function move(dir: 1 | -1): void { + const list = visibleEnabled(); + if (list.length === 0) return; + const idx = activeEl ? list.indexOf(activeEl) : -1; + let next = idx + dir; + if (next < 0) next = list.length - 1; + if (next >= list.length) next = 0; + setActive(list[next]); + } + + function onDocPointer(e: PointerEvent): void { + if (!root.contains(e.target as Node)) closePopup(); + } + + function onInput(): void { + if (!open) openPopup(false); + applyFilter(input!.value); + setActive(visibleEnabled()[0] ?? null); + syncClearVisibility(); + } + + function onInputFocus(): void { + if (!open) { + openPopup(true); + input!.select(); + } + } + + function onInputKeydown(e: KeyboardEvent): void { + switch (e.key) { + case "ArrowDown": + e.preventDefault(); + open ? move(1) : openPopup(true); + break; + case "ArrowUp": + e.preventDefault(); + open ? move(-1) : openPopup(true); + break; + case "Enter": + if (open && activeEl) { + e.preventDefault(); + choose(activeEl); + } + break; + case "Escape": + if (open) { + e.preventDefault(); + closePopup(); + } else if (input!.value) { + e.preventDefault(); + clear(); + } + break; + case "Home": + if (open) { + e.preventDefault(); + setActive(visibleEnabled()[0] ?? null); + } + break; + case "End": + if (open) { + e.preventDefault(); + const l = visibleEnabled(); + setActive(l[l.length - 1] ?? null); + } + break; + case "Tab": + if (open) closePopup(); + break; + } + } + + function onTriggerClick(): void { + if (open) { + closePopup(); + } else { + input!.focus(); + openPopup(true); + input!.select(); + } + } + + function onClearClick(): void { + clear(); + } + + function onListClick(e: MouseEvent): void { + const o = (e.target as HTMLElement).closest<HTMLElement>( + "[data-nb-combobox-option]", + ); + if (o && list!.contains(o)) choose(o); + } + + function onListMousemove(e: MouseEvent): void { + const o = (e.target as HTMLElement).closest<HTMLElement>( + "[data-nb-combobox-option]", + ); + if (o && !o.hidden && !o.hasAttribute("data-disabled")) setActive(o); + } + + input.addEventListener("input", onInput); + input.addEventListener("focus", onInputFocus); + input.addEventListener("keydown", onInputKeydown); + triggerBtn?.addEventListener("click", onTriggerClick); + clearBtn?.addEventListener("click", onClearClick); + list.addEventListener("click", onListClick); + list.addEventListener("mousemove", onListMousemove); + + // Reflect the initial selection (set by the Astro component) without + // emitting a change. Seeds aria-selected, the hidden input, and the + // displayed label. + commit(root.dataset.value || null, false); + syncClearVisibility(); + + return () => { + input.removeEventListener("input", onInput); + input.removeEventListener("focus", onInputFocus); + input.removeEventListener("keydown", onInputKeydown); + triggerBtn?.removeEventListener("click", onTriggerClick); + clearBtn?.removeEventListener("click", onClearClick); + list.removeEventListener("click", onListClick); + list.removeEventListener("mousemove", onListMousemove); + document.removeEventListener("pointerdown", onDocPointer, true); + }; +} + +mount("[data-nb-combobox]", initCombobox); diff --git a/src/nimbus/components/ui/combobox/index.ts b/src/nimbus/components/ui/combobox/index.ts new file mode 100644 index 00000000000..55c27ef0591 --- /dev/null +++ b/src/nimbus/components/ui/combobox/index.ts @@ -0,0 +1,4 @@ +export { default as Combobox } from "./Combobox.astro"; +export { default as ComboboxOption } from "./ComboboxOption.astro"; +export { default as ComboboxGroup } from "./ComboboxGroup.astro"; +export { default as ComboboxGroupLabel } from "./ComboboxGroupLabel.astro"; diff --git a/src/nimbus/components/ui/dialog/Dialog.astro b/src/nimbus/components/ui/dialog/Dialog.astro new file mode 100644 index 00000000000..09b67e420f4 --- /dev/null +++ b/src/nimbus/components/ui/dialog/Dialog.astro @@ -0,0 +1,54 @@ +--- +/** + * Dialog — modal overlay built on the native <dialog>. + * + * Open with `el.showModal()`, close with `el.close()` or Escape. + * Scroll-lock and backdrop-click-to-close are handled automatically. + * + * <Dialog id="confirm"> + * <DialogContent> + * <div>Are you sure?</div> + * <DialogClose>Cancel</DialogClose> + * </DialogContent> + * </Dialog> + */ +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"dialog"> {} + +const { class: className, ...attrs } = Astro.props; +--- + +<dialog + data-dialog + class={cn("fixed inset-0 z-50 m-0 h-full w-full max-h-full max-w-full bg-transparent p-0 backdrop:bg-black/40", className)} + {...attrs} +> + <slot /> +</dialog> + +<script> + import { lockScroll, unlockScroll } from "nimbus-docs/client"; + + function initDialogs() { + document.querySelectorAll<HTMLDialogElement>("[data-dialog]").forEach((dialog) => { + if (dialog.hasAttribute("data-initialized")) return; + dialog.setAttribute("data-initialized", "true"); + + const observer = new MutationObserver(() => { + if (dialog.open) lockScroll(); + else unlockScroll(); + }); + observer.observe(dialog, { attributes: true, attributeFilter: ["open"] }); + + dialog.addEventListener("close", () => unlockScroll()); + + dialog.addEventListener("click", (e) => { + if (e.target === dialog) dialog.close(); + }); + }); + } + initDialogs(); + document.addEventListener("astro:page-load", initDialogs); +</script> diff --git a/src/nimbus/components/ui/dialog/DialogClose.astro b/src/nimbus/components/ui/dialog/DialogClose.astro new file mode 100644 index 00000000000..12af0f8f996 --- /dev/null +++ b/src/nimbus/components/ui/dialog/DialogClose.astro @@ -0,0 +1,36 @@ +--- +/** + * DialogClose — button that closes the nearest ancestor <dialog>. + * Consumer provides the visual (icon, text, kbd hint) via default slot. + */ +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"button"> {} + +const { class: className, ...attrs } = Astro.props; +--- + +<button + data-dialog-close + type="button" + class={cn(className)} + {...attrs} +> + <slot /> +</button> + +<script> + function initDialogClose() { + document.querySelectorAll<HTMLButtonElement>("[data-dialog-close]").forEach((btn) => { + if (btn.hasAttribute("data-initialized")) return; + btn.setAttribute("data-initialized", "true"); + + btn.addEventListener("click", () => { + btn.closest("dialog")?.close(); + }); + }); + } + initDialogClose(); + document.addEventListener("astro:page-load", initDialogClose); +</script> diff --git a/src/nimbus/components/ui/dialog/DialogContent.astro b/src/nimbus/components/ui/dialog/DialogContent.astro new file mode 100644 index 00000000000..462448a5880 --- /dev/null +++ b/src/nimbus/components/ui/dialog/DialogContent.astro @@ -0,0 +1,21 @@ +--- +/** + * DialogContent — inner frame of a Dialog. Centered, constrained, styled. + * Consumer controls max-width/height via class override. + */ +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"div"> {} + +const { class: className, ...attrs } = Astro.props; +--- + +<div class="flex items-start justify-center pt-[10vh] px-4 pointer-events-none"> + <div + class={cn("pointer-events-auto flex max-h-[70vh] w-full flex-col overflow-hidden rounded-lg bg-muted shadow-lg ring-1 ring-border", className)} + {...attrs} + > + <slot /> + </div> +</div> diff --git a/src/nimbus/components/ui/dialog/index.ts b/src/nimbus/components/ui/dialog/index.ts new file mode 100644 index 00000000000..fe7fb64139f --- /dev/null +++ b/src/nimbus/components/ui/dialog/index.ts @@ -0,0 +1,3 @@ +export { default as Dialog } from "./Dialog.astro"; +export { default as DialogContent } from "./DialogContent.astro"; +export { default as DialogClose } from "./DialogClose.astro"; diff --git a/src/nimbus/components/ui/file-tree/FileTree.astro b/src/nimbus/components/ui/file-tree/FileTree.astro new file mode 100644 index 00000000000..a2cb2ede2aa --- /dev/null +++ b/src/nimbus/components/ui/file-tree/FileTree.astro @@ -0,0 +1,263 @@ +--- +/** + * FileTree — collapsible file/directory tree from a nested <ul>. + * + * Usage: + * <FileTree> + * - src/ + * - components/ + * - **Header.astro** + * - Footer.astro + * - package.json + * - ... + * </FileTree> + * + * Conventions: + * - Trailing `/` → directory (collapsible, folder icon) + * - `**name**` → highlighted entry + * - `...` → placeholder ellipsis + */ +import { Icon } from "astro-icon/components"; +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"div"> {} + +const { class: className, ...attrs } = Astro.props; +--- + +<div class={cn("file-tree", className)} data-file-tree {...attrs}> + <template data-ft-icon-chev><Icon name="ph:caret-right" class="ft-chev" is:inline /></template> + <template data-ft-icon-folder><Icon name="ph:folder" class="ft-icon" is:inline /></template> + <template data-ft-icon-file><Icon name="ph:file" class="ft-icon" is:inline /></template> + <slot /> +</div> + +<style> + .file-tree { + margin: 1rem 0; + border-radius: 0.5rem; + padding: 0.75rem 1rem; + font-family: var(--nb-font-mono); + font-size: 0.875rem; + line-height: 1; + background: var(--nb-card); + box-shadow: 0 0 0 1px var(--nb-border); + } + + /* Inline any <p> wrappers from markdown */ + .file-tree :global(p) { display: inline; margin: 0; } + + .file-tree :global(ul) { + list-style: none; + margin: 0; + padding: 0 0 0 1.25rem; + } + + .file-tree > :global(ul) { padding-left: 0; } + + /* Guide lines on nested lists */ + .file-tree :global(ul ul) { position: relative; } + .file-tree :global(ul ul)::before { + content: ""; + position: absolute; + left: 0.4375rem; + top: 0; + bottom: 0.25rem; + width: 1px; + background: var(--nb-border); + pointer-events: none; + } + + .file-tree :global(li) { + position: relative; + margin: 0; + padding-top: 0.0625rem; + padding-bottom: 0.0625rem; + color: var(--nb-foreground); + } + + /* Directory rows — details/summary */ + .file-tree :global(details) { margin: 0; } + + .file-tree :global(summary) { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.3125rem 0.375rem; + margin: 0 -0.375rem; + cursor: pointer; + list-style: none; + user-select: none; + border-radius: 0.375rem; + transition: background 0.12s ease; + } + + .file-tree :global(summary::-webkit-details-marker) { display: none; } + .file-tree :global(summary::marker) { display: none; content: ""; } + + .file-tree :global(summary:hover) { + background: var(--nb-accent); + } + + /* File rows */ + .file-tree :global([data-file]) { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.3125rem 0.375rem; + margin: 0 -0.375rem; + border-radius: 0.375rem; + cursor: default; + } + + /* Chevron — rotates when open */ + .file-tree :global(.ft-chev) { + flex-shrink: 0; + width: 0.875rem; + height: 0.875rem; + color: var(--nb-muted-foreground); + transition: transform 0.15s ease; + } + + .file-tree :global(details[open] > summary > .ft-chev) { + transform: rotate(90deg); + } + + /* File/folder icons */ + .file-tree :global(.ft-icon) { + flex-shrink: 0; + width: 0.875rem; + height: 0.875rem; + color: var(--nb-muted-foreground); + } + + /* Invisible spacer — keeps files aligned with directory names */ + .file-tree :global(.ft-sp) { + flex-shrink: 0; + width: 0.875rem; + } + + /* Clamp label to one line */ + .file-tree :global(.ft-label) { + min-width: 0; + white-space: nowrap; + } + + /* Highlighted entries */ + .file-tree :global(strong) { + font-weight: 600; + color: var(--nb-foreground); + } + + /* Placeholder (...) */ + .file-tree :global([data-placeholder]) { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.3125rem 0.375rem; + margin: 0 -0.375rem; + color: var(--nb-muted-foreground); + letter-spacing: 0.1em; + } +</style> + +<script> + function cloneIcon(tpl: HTMLTemplateElement | null): Node { + return tpl ? tpl.content.cloneNode(true) : document.createTextNode(""); + } + + function createSpacer(): HTMLElement { + const span = document.createElement("span"); + span.className = "ft-sp"; + span.setAttribute("aria-hidden", "true"); + return span; + } + + function createEmptyIcon(): HTMLElement { + const span = document.createElement("span"); + span.className = "ft-icon"; + span.setAttribute("aria-hidden", "true"); + return span; + } + + function escapeHtml(value: string): string { + return value + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + function getSafeLabel(nodes: ChildNode[], stripTrailingSlash = false): string { + return nodes + .map((node) => { + let text = node.textContent ?? ""; + if (stripTrailingSlash) { + text = text.replace(/\/\s*$/, ""); + } + text = text.trim(); + if (!text) return ""; + + if (node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName === "STRONG") { + return `<strong>${escapeHtml(text)}</strong>`; + } + + return escapeHtml(text); + }) + .join(""); + } + + function createLabel(label: string): HTMLSpanElement { + const span = document.createElement("span"); + span.className = "ft-label"; + span.innerHTML = label; + return span; + } + + function initFileTree() { + document.querySelectorAll<HTMLElement>("[data-file-tree]").forEach((tree) => { + if (tree.hasAttribute("data-initialized")) return; + tree.setAttribute("data-initialized", "true"); + + const chevTpl = tree.querySelector<HTMLTemplateElement>("[data-ft-icon-chev]"); + const folderTpl = tree.querySelector<HTMLTemplateElement>("[data-ft-icon-folder]"); + const fileTpl = tree.querySelector<HTMLTemplateElement>("[data-ft-icon-file]"); + + for (const li of tree.querySelectorAll("li")) { + const childUl = li.querySelector<HTMLUListElement>(":scope > ul"); + const text = li.textContent?.trim() ?? ""; + + if (text === "..." || text === "\u2026") { + li.setAttribute("data-placeholder", ""); + li.replaceChildren(createSpacer(), createEmptyIcon(), createLabel("\u22EF")); + continue; + } + + if (childUl) { + const label = getSafeLabel( + [...li.childNodes].filter((node) => node !== childUl), + true, + ); + + const details = document.createElement("details"); + details.open = true; + const summary = document.createElement("summary"); + summary.appendChild(cloneIcon(chevTpl)); + summary.appendChild(cloneIcon(folderTpl)); + summary.appendChild(createLabel(label)); + details.appendChild(summary); + details.appendChild(childUl); + li.replaceChildren(details); + } else { + li.setAttribute("data-file", ""); + const label = getSafeLabel([...li.childNodes]); + li.replaceChildren(createSpacer(), cloneIcon(fileTpl), createLabel(label)); + } + } + }); + } + initFileTree(); + document.addEventListener("astro:page-load", initFileTree); +</script> diff --git a/src/nimbus/components/ui/file-tree/index.ts b/src/nimbus/components/ui/file-tree/index.ts new file mode 100644 index 00000000000..35b84e480d8 --- /dev/null +++ b/src/nimbus/components/ui/file-tree/index.ts @@ -0,0 +1 @@ +export { default as FileTree } from "./FileTree.astro"; diff --git a/src/nimbus/components/ui/layer-card/LayerCard.astro b/src/nimbus/components/ui/layer-card/LayerCard.astro new file mode 100644 index 00000000000..8473c7bcd84 --- /dev/null +++ b/src/nimbus/components/ui/layer-card/LayerCard.astro @@ -0,0 +1,23 @@ +--- +/** + * LayerCard — two-layer card (recessed header + raised content). + * + * <LayerCard> + * <LayerCardHeader>Title or tabs</LayerCardHeader> + * <LayerCardContent>Main content</LayerCardContent> + * </LayerCard> + */ +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"div"> {} + +const { class: className, ...attrs } = Astro.props; +--- + +<div + class={cn("flex w-full flex-col overflow-hidden rounded-lg bg-muted text-sm ring ring-border", className)} + {...attrs} +> + <slot /> +</div> diff --git a/src/nimbus/components/ui/layer-card/LayerCardContent.astro b/src/nimbus/components/ui/layer-card/LayerCardContent.astro new file mode 100644 index 00000000000..12bdaeac48a --- /dev/null +++ b/src/nimbus/components/ui/layer-card/LayerCardContent.astro @@ -0,0 +1,16 @@ +--- +/** LayerCardContent — raised content layer of a LayerCard. */ +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"div"> {} + +const { class: className, ...attrs } = Astro.props; +--- + +<div + class={cn("relative flex flex-col gap-2 overflow-hidden rounded-lg bg-card p-4 pr-3 text-sm leading-6 text-foreground ring ring-border", className)} + {...attrs} +> + <slot /> +</div> diff --git a/src/nimbus/components/ui/layer-card/LayerCardHeader.astro b/src/nimbus/components/ui/layer-card/LayerCardHeader.astro new file mode 100644 index 00000000000..b9be4045888 --- /dev/null +++ b/src/nimbus/components/ui/layer-card/LayerCardHeader.astro @@ -0,0 +1,16 @@ +--- +/** LayerCardHeader — recessed header layer of a LayerCard. */ +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"div"> {} + +const { class: className, ...attrs } = Astro.props; +--- + +<div + class={cn("flex items-center gap-2 bg-muted px-3 py-2 text-[0.8125rem] font-medium leading-5 text-muted-foreground", className)} + {...attrs} +> + <slot /> +</div> diff --git a/src/nimbus/components/ui/layer-card/index.ts b/src/nimbus/components/ui/layer-card/index.ts new file mode 100644 index 00000000000..dd347fe4e05 --- /dev/null +++ b/src/nimbus/components/ui/layer-card/index.ts @@ -0,0 +1,3 @@ +export { default as LayerCard } from "./LayerCard.astro"; +export { default as LayerCardHeader } from "./LayerCardHeader.astro"; +export { default as LayerCardContent } from "./LayerCardContent.astro"; diff --git a/src/nimbus/components/ui/link-button/LinkButton.astro b/src/nimbus/components/ui/link-button/LinkButton.astro new file mode 100644 index 00000000000..153252f6636 --- /dev/null +++ b/src/nimbus/components/ui/link-button/LinkButton.astro @@ -0,0 +1,76 @@ +--- +/** + * LinkButton — an anchor styled as a button. + * + * <LinkButton href="/start" variant="primary">Get started</LinkButton> + * <LinkButton href="/docs" variant="secondary">Read the docs</LinkButton> + * <LinkButton href="#" variant="minimal" icon>Learn more</LinkButton> + * <LinkButton href="/x" shape="square" aria-label="Open"> … </LinkButton> + * + * The `icon` prop appends a right-caret that nudges on hover. + * + * API note: the original props are unchanged (used by many MDX files) — this + * only *adds* `shape` and widens `variant`/`size` to also accept Button's + * values. Styling is delegated to the shared `ui/button/variants`, and the + * LinkButton-only aliases map onto it: `minimal → ghost`, `md → base`. + */ +import { cn } from "@/lib/cn"; +import { Icon } from "astro-icon/components"; +import type { HTMLAttributes } from "astro/types"; +import { + buttonVariants, + buttonIconSize, + type ButtonVariant, + type ButtonSize, + type ButtonShape, +} from "../button/variants"; + +interface Props extends HTMLAttributes<"a"> { + href: string; + /** Original `primary | secondary | minimal`, plus Button's variants. */ + variant?: ButtonVariant | "minimal"; + /** Original `sm | md | lg`, plus Button's `xs | base`. */ + size?: ButtonSize | "md"; + /** Added: `base` (default) · `square` · `circle` (icon-only — pass `aria-label`). */ + shape?: ButtonShape; + /** Append a caret-right that nudges on hover. */ + icon?: boolean; +} + +const { + href, + variant = "primary", + size = "md", + shape = "base", + icon = false, + class: className, + ...attrs +} = Astro.props; + +// Map the LinkButton-only aliases onto the shared Button vocabulary. +const resolvedVariant: ButtonVariant = variant === "minimal" ? "ghost" : variant; +const resolvedSize: ButtonSize = size === "md" ? "base" : size; +--- + +<a + href={href} + data-nb-button + class={cn( + buttonVariants({ variant: resolvedVariant, size: resolvedSize, shape }), + className, + )} + {...attrs} +> + <slot /> + { + icon && ( + <Icon + name="ph:caret-right" + class={cn( + "transition-transform group-hover:translate-x-0.5", + buttonIconSize[resolvedSize], + )} + /> + ) + } +</a> diff --git a/src/nimbus/components/ui/link-button/index.ts b/src/nimbus/components/ui/link-button/index.ts new file mode 100644 index 00000000000..71411b5eb64 --- /dev/null +++ b/src/nimbus/components/ui/link-button/index.ts @@ -0,0 +1 @@ +export { default as LinkButton } from "./LinkButton.astro"; diff --git a/src/nimbus/components/ui/package-managers/PackageManagers.astro b/src/nimbus/components/ui/package-managers/PackageManagers.astro new file mode 100644 index 00000000000..6f25c5c1bf7 --- /dev/null +++ b/src/nimbus/components/ui/package-managers/PackageManagers.astro @@ -0,0 +1,112 @@ +--- +/** + * PackageManagers — code block with a tab per package manager (npm, + * pnpm, yarn, bun). Selection syncs across instances via sessionStorage; + * an inline custom element restores the saved tab before paint. + */ +import { Icon } from "astro-icon/components"; +import { getTabs } from "nimbus-docs/lib/pkgm"; +import type { CommandType, CommandOptions } from "nimbus-docs/lib/pkgm"; +import { LayerCard, LayerCardHeader } from "@/components/ui/layer-card"; + +interface Props extends CommandOptions { + pkg?: string; + type?: CommandType; +} + +const { pkg, type = "add", args, dev, comment, managers } = Astro.props; +const tabs = getTabs(type, pkg, { args, dev, comment, managers }); +const uid = `pm-${crypto.randomUUID()}`; +--- + +<script is:inline> + if (!customElements.get("nb-pm-restore")) { + customElements.define( + "nb-pm-restore", + class extends HTMLElement { + connectedCallback() { + const card = this.closest("[data-nb-pm]"); + if (!card) return; + let saved; + try { + saved = sessionStorage.getItem("ui-pm-tab"); + } catch { + return; + } + if (!saved) return; + const tabs = card.querySelectorAll("[data-nb-pm-tab]"); + let idx = -1; + tabs.forEach(function (t, i) { + if (t.textContent.trim() === saved) idx = i; + }); + if (idx < 1) return; + tabs.forEach(function (t, i) { + t.setAttribute("aria-selected", String(i === idx)); + }); + card.querySelectorAll("[data-nb-pm-panel]").forEach(function (p, i) { + p.hidden = i !== idx; + }); + } + }, + ); + } +</script> + +<div data-nb-pm class="w-full"> + <LayerCard> + <LayerCardHeader role="tablist" aria-label="Package manager"> + {tabs.map((tab, i) => ( + <button + role="tab" + type="button" + aria-selected={i === 0 ? "true" : "false"} + aria-controls={`pm-panel-${uid}-${tab.mgr}`} + id={`pm-tab-${uid}-${tab.mgr}`} + data-nb-pm-tab + class="m-0 cursor-pointer rounded-md border-0 bg-transparent px-2 py-0.5 text-xs leading-5 font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground aria-selected:bg-selected aria-selected:text-foreground" + > + {tab.mgr} + </button> + ))} + </LayerCardHeader> + + {tabs.map((tab, i) => { + const cmdLines = tab.cmd.split("\n"); + const commentPrefix = cmdLines.length > 1 ? cmdLines.slice(0, -1).join("\n") + "\n" : ""; + const codeLine = cmdLines[cmdLines.length - 1]; + const spaceIdx = codeLine.indexOf(" "); + const codeFirst = spaceIdx === -1 ? codeLine : codeLine.slice(0, spaceIdx); + const codeRest = spaceIdx === -1 ? "" : codeLine.slice(spaceIdx); + return ( + <div + role="tabpanel" + id={`pm-panel-${uid}-${tab.mgr}`} + aria-labelledby={`pm-tab-${uid}-${tab.mgr}`} + hidden={i !== 0} + data-nb-pm-panel + class="relative overflow-hidden rounded-lg bg-card text-inherit ring ring-border" + > + <div class="flex items-stretch"> + <pre class="my-0 min-w-0 grow overflow-x-auto border-0 bg-transparent px-4 py-3 text-sm leading-relaxed whitespace-pre font-mono text-foreground"><code data-nb-pm-code>{commentPrefix && <span class="text-muted-foreground">{commentPrefix}</span>}<span class="text-success">{codeFirst}</span><span class="text-warning">{codeRest}</span></code></pre> + <button + type="button" + data-nb-pm-copy + data-nb-command={tab.cmd} + aria-label="Copy to clipboard" + class="m-0 flex shrink-0 cursor-pointer items-center justify-center border-0 border-l border-solid border-border bg-transparent px-3 text-muted-foreground transition-colors hover:text-foreground" + > + <Icon name="ph:copy" class="w-[18px] h-[18px]" /> + </button> + </div> + </div> + ); + })} + <nb-pm-restore style="display:contents"></nb-pm-restore> + </LayerCard> + <template data-nb-pm-icon-copy><Icon name="ph:copy" class="w-[18px] h-[18px]" is:inline /></template> + <template data-nb-pm-icon-check><Icon name="ph:check" class="w-[18px] h-[18px]" is:inline /></template> +</div> + +<script> + import "./package-managers.client"; +</script> diff --git a/src/nimbus/components/ui/package-managers/index.ts b/src/nimbus/components/ui/package-managers/index.ts new file mode 100644 index 00000000000..248021d7310 --- /dev/null +++ b/src/nimbus/components/ui/package-managers/index.ts @@ -0,0 +1 @@ +export { default as PackageManagers } from "./PackageManagers.astro"; diff --git a/src/nimbus/components/ui/package-managers/package-managers.client.ts b/src/nimbus/components/ui/package-managers/package-managers.client.ts new file mode 100644 index 00000000000..a7c115201a8 --- /dev/null +++ b/src/nimbus/components/ui/package-managers/package-managers.client.ts @@ -0,0 +1,55 @@ +/** + * Sync key `ui-pm-tab` (sessionStorage) is shared with the + * `<nb-pm-restore>` early-paint element to avoid flash across navigations. + */ + +import { mount, initTabs } from "nimbus-docs/client"; + +function cloneIcon(tpl: HTMLTemplateElement | null): Node { + return tpl ? tpl.content.cloneNode(true) : document.createTextNode(""); +} + +function initPackageManager(container: HTMLElement): () => void { + const copyTpl = container.querySelector<HTMLTemplateElement>("[data-nb-pm-icon-copy]"); + const checkTpl = container.querySelector<HTMLTemplateElement>("[data-nb-pm-icon-check]"); + + const tabs = initTabs({ + container, + tabSelector: "[data-nb-pm-tab]", + panelSelector: "[data-nb-pm-panel]", + rovingTabindex: true, + sync: { key: "ui-pm-tab", storage: "session" }, + }); + + const copyHandlers: Array<{ btn: HTMLButtonElement; handler: () => void; timer?: number }> = []; + + container.querySelectorAll<HTMLButtonElement>("[data-nb-pm-copy]").forEach((btn) => { + const handlerInfo: { btn: HTMLButtonElement; handler: () => void; timer?: number } = { + btn, + handler: async () => { + try { + await navigator.clipboard.writeText(btn.dataset.nbCommand ?? ""); + } catch { + return; + } + btn.replaceChildren(cloneIcon(checkTpl)); + if (handlerInfo.timer) window.clearTimeout(handlerInfo.timer); + handlerInfo.timer = window.setTimeout(() => { + btn.replaceChildren(cloneIcon(copyTpl)); + }, 1500); + }, + }; + btn.addEventListener("click", handlerInfo.handler); + copyHandlers.push(handlerInfo); + }); + + return () => { + tabs.destroy(); + copyHandlers.forEach(({ btn, handler, timer }) => { + btn.removeEventListener("click", handler); + if (timer) window.clearTimeout(timer); + }); + }; +} + +mount("[data-nb-pm]", initPackageManager); diff --git a/src/nimbus/components/ui/page-actions/PageActions.astro b/src/nimbus/components/ui/page-actions/PageActions.astro new file mode 100644 index 00000000000..5c9a2bf1f7d --- /dev/null +++ b/src/nimbus/components/ui/page-actions/PageActions.astro @@ -0,0 +1,65 @@ +--- +import { Icon } from "astro-icon/components"; +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"div"> { + markdownUrl?: string; + lastUpdated?: Date; +} + +const { markdownUrl, lastUpdated, class: className, ...attrs } = Astro.props; + +const baseBtn = + "inline-flex cursor-pointer items-center gap-1.5 rounded-md bg-transparent px-2 py-1 text-muted-foreground no-underline transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2"; + +const formattedDate = lastUpdated + ? new Intl.DateTimeFormat(undefined, { year: "numeric", month: "short", day: "numeric" }).format(lastUpdated) + : null; +--- + +{(markdownUrl || lastUpdated) && ( + <div + data-nb-page-actions + data-md-url={markdownUrl} + class={cn("not-prose -ml-2 flex flex-wrap items-center gap-y-1 text-[0.8125rem]", className)} + {...attrs} + > + {lastUpdated && ( + <span class="inline-flex items-center gap-1.5 px-2 py-1 text-muted-foreground"> + <Icon name="ph:clock" class="w-3.5 h-3.5" /> + Updated <time datetime={lastUpdated.toISOString()}>{formattedDate}</time> + </span> + )} + + {markdownUrl && lastUpdated && ( + <span aria-hidden="true" class="select-none text-muted-foreground/40">|</span> + )} + + {markdownUrl && ( + <> + <button type="button" data-nb-page-actions-copy class={baseBtn}> + <Icon name="ph:copy" class="w-3.5 h-3.5" data-nb-page-actions-copy-icon /> + <Icon name="ph:check" class="hidden w-3.5 h-3.5 text-success" data-nb-page-actions-check-icon /> + <span data-nb-page-actions-label aria-live="polite">Copy page</span> + </button> + + <span aria-hidden="true" class="select-none text-muted-foreground/40">|</span> + + <a + href={markdownUrl} + target="_blank" + rel="noopener noreferrer" + class={baseBtn} + > + <Icon name="ph:markdown-logo" class="w-3.5 h-3.5" /> + View as Markdown + </a> + </> + )} + </div> +)} + +<script> + import "./page-actions.client"; +</script> diff --git a/src/nimbus/components/ui/page-actions/index.ts b/src/nimbus/components/ui/page-actions/index.ts new file mode 100644 index 00000000000..3d7bcd1aeec --- /dev/null +++ b/src/nimbus/components/ui/page-actions/index.ts @@ -0,0 +1 @@ +export { default as PageActions } from "./PageActions.astro"; diff --git a/src/nimbus/components/ui/page-actions/page-actions.client.ts b/src/nimbus/components/ui/page-actions/page-actions.client.ts new file mode 100644 index 00000000000..6cae8bc6d31 --- /dev/null +++ b/src/nimbus/components/ui/page-actions/page-actions.client.ts @@ -0,0 +1,54 @@ +import { mount } from "nimbus-docs/client"; + +function initPageActions(root: HTMLElement): () => void { + const copyBtn = root.querySelector<HTMLButtonElement>("[data-nb-page-actions-copy]"); + const copyIcon = root.querySelector<SVGElement>("[data-nb-page-actions-copy-icon]"); + const checkIcon = root.querySelector<SVGElement>("[data-nb-page-actions-check-icon]"); + const label = root.querySelector<HTMLSpanElement>("[data-nb-page-actions-label]"); + const mdUrl = root.dataset.mdUrl; + + if (!copyBtn || !mdUrl) return () => {}; + + let resetTimer: number | undefined; + + function showState(state: "copied" | "error") { + if (!copyIcon || !checkIcon || !label) return; + if (state === "copied") { + copyIcon.classList.add("hidden"); + checkIcon.classList.remove("hidden"); + label.textContent = "Copied"; + } else { + label.textContent = "Couldn't copy"; + } + if (resetTimer) window.clearTimeout(resetTimer); + resetTimer = window.setTimeout(() => { + copyIcon.classList.remove("hidden"); + checkIcon.classList.add("hidden"); + label.textContent = "Copy page"; + }, 1500); + } + + async function handleCopyPage() { + try { + const res = await fetch(mdUrl!); + if (!res.ok) { + showState("error"); + return; + } + const text = await res.text(); + await navigator.clipboard.writeText(text); + showState("copied"); + } catch { + showState("error"); + } + } + + copyBtn.addEventListener("click", handleCopyPage); + + return () => { + if (resetTimer) window.clearTimeout(resetTimer); + copyBtn.removeEventListener("click", handleCopyPage); + }; +} + +mount("[data-nb-page-actions]", initPageActions); diff --git a/src/nimbus/components/ui/pagination/Pagination.astro b/src/nimbus/components/ui/pagination/Pagination.astro new file mode 100644 index 00000000000..6768078bb42 --- /dev/null +++ b/src/nimbus/components/ui/pagination/Pagination.astro @@ -0,0 +1,36 @@ +--- +import { Icon } from "astro-icon/components"; +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; +import type { PrevNext } from "nimbus-docs/types"; + +interface Props extends HTMLAttributes<"nav"> { + prevNext: PrevNext; +} + +const { prevNext, class: className, ...attrs } = Astro.props; +const { prev, next } = prevNext; +--- + +{(prev || next) && ( + <nav aria-label="Pagination" class={cn("flex items-center justify-between mt-12 pt-6 border-t border-border", className)} {...attrs}> + {prev ? ( + <a href={prev.href} class="group flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"> + <Icon name="ph:caret-left" class="w-4 h-4 transition-transform group-hover:-translate-x-0.5" /> + <span> + <span class="block text-xs text-muted-foreground">Previous</span> + <span class="font-medium text-foreground">{prev.label}</span> + </span> + </a> + ) : <span />} + {next ? ( + <a href={next.href} class="group flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors text-right"> + <span> + <span class="block text-xs text-muted-foreground">Next</span> + <span class="font-medium text-foreground">{next.label}</span> + </span> + <Icon name="ph:caret-right" class="w-4 h-4 transition-transform group-hover:translate-x-0.5" /> + </a> + ) : <span />} + </nav> +)} diff --git a/src/nimbus/components/ui/pagination/index.ts b/src/nimbus/components/ui/pagination/index.ts new file mode 100644 index 00000000000..8972ed7fb79 --- /dev/null +++ b/src/nimbus/components/ui/pagination/index.ts @@ -0,0 +1 @@ +export { default as Pagination } from "./Pagination.astro"; diff --git a/src/nimbus/components/ui/search/SearchDialog.astro b/src/nimbus/components/ui/search/SearchDialog.astro new file mode 100644 index 00000000000..e26fb2a0501 --- /dev/null +++ b/src/nimbus/components/ui/search/SearchDialog.astro @@ -0,0 +1,113 @@ +--- +import { Icon } from "astro-icon/components"; +import { Dialog, DialogClose, DialogContent } from "@/components/ui/dialog"; +--- + +<Dialog aria-label="Search documentation" data-search-dialog> + <DialogContent class="max-w-xl bg-muted shadow-xl ring-border"> + <div class="overflow-hidden rounded-t-lg bg-card border-b border-border"> + <div class="flex items-center gap-3 border-b border-border px-4 py-3"> + <Icon name="ph:magnifying-glass" class="h-4 w-4 shrink-0 text-muted-foreground" /> + <input + data-search-input + role="combobox" + aria-expanded="false" + aria-haspopup="listbox" + aria-autocomplete="list" + aria-controls="search-listbox" + type="text" + placeholder="Search documentation…" + class="min-w-0 flex-1 border-0 bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none" + autocomplete="off" + spellcheck="false" + autocorrect="off" + autocapitalize="none" + /> + <DialogClose class="rounded border border-border bg-muted px-1.5 py-0.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"> + Esc + </DialogClose> + </div> + + <div + data-search-results + role="listbox" + id="search-listbox" + aria-orientation="vertical" + class="min-h-40 overflow-y-auto p-2" + > + <p data-search-empty class="py-8 text-center text-sm text-muted-foreground"> + Type to search… + </p> + </div> + </div> + + <div class="flex items-center gap-4 px-4 py-2.5 text-xs text-muted-foreground/80"> + <span>↑↓ navigate</span> + <span>↵ select</span> + <span>Esc close</span> + </div> + </DialogContent> +</Dialog> + +<script> + import { initSearch } from "./search.client"; + import { provider } from "./providers/pagefind"; + + type SearchDialogElement = HTMLDialogElement & { + __openSearchDialog?: () => void; + }; + + function primaryDialog(): SearchDialogElement | null { + return document.querySelector<SearchDialogElement>("[data-search-dialog][data-search-ready]"); + } + + function bindGlobals() { + if (document.documentElement.hasAttribute("data-search-globals")) return; + document.documentElement.setAttribute("data-search-globals", "true"); + + document.addEventListener("click", (event) => { + const trigger = (event.target as Element | null)?.closest("[data-search-trigger]"); + if (!trigger) return; + primaryDialog()?.__openSearchDialog?.(); + }); + + document.addEventListener("keydown", (event) => { + if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== "k") return; + const dialog = primaryDialog(); + if (!dialog) return; + event.preventDefault(); + if (dialog.open) dialog.close(); + else dialog.__openSearchDialog?.(); + }); + } + + function initSearchDialog() { + document.querySelectorAll<SearchDialogElement>("[data-search-dialog]").forEach((dialog) => { + if (dialog.hasAttribute("data-search-ready")) return; + dialog.setAttribute("data-search-ready", "true"); + + const input = dialog.querySelector<HTMLInputElement>("[data-search-input]"); + const resultsContainer = dialog.querySelector<HTMLElement>("[data-search-results]"); + const emptyState = dialog.querySelector<HTMLElement>("[data-search-empty]"); + if (!input || !resultsContainer || !emptyState) return; + + const search = initSearch({ + input, + resultsContainer, + emptyState, + provider, + onNavigate: () => dialog.close(), + }); + + dialog.__openSearchDialog = () => { + if (!dialog.open) dialog.showModal(); + void search.reset(); + }; + }); + + bindGlobals(); + } + + initSearchDialog(); + document.addEventListener("astro:page-load", initSearchDialog); +</script> diff --git a/src/nimbus/components/ui/search/SearchTrigger.astro b/src/nimbus/components/ui/search/SearchTrigger.astro new file mode 100644 index 00000000000..9d4d9878336 --- /dev/null +++ b/src/nimbus/components/ui/search/SearchTrigger.astro @@ -0,0 +1,41 @@ +--- +/** Button that opens the search dialog. Cmd+K on macOS, Ctrl+K elsewhere. */ +import { Icon } from "astro-icon/components"; +--- + +<button + data-search-trigger + type="button" + class="flex items-center gap-2 rounded-md border border-border bg-card px-2.5 py-1.5 text-sm text-muted-foreground transition-colors hover:border-border-strong hover:text-foreground" + aria-label="Search documentation" + aria-keyshortcuts="Control+K" +> + <Icon name="ph:magnifying-glass" class="h-4 w-4" /> + <span class="hidden md:inline">Search</span> + <kbd class="hidden sm:inline-flex items-center gap-0.5 rounded border border-border bg-muted px-1.5 py-0.5 text-[0.625rem] font-medium text-muted-foreground"> + <span data-shortcut-key>Ctrl</span>K + </kbd> +</button> + +<script> + function initSearchTrigger() { + document.querySelectorAll<HTMLElement>("[data-search-trigger]").forEach((btn) => { + if (btn.hasAttribute("data-search-trigger-ready")) return; + btn.setAttribute("data-search-trigger-ready", "true"); + + const nav = navigator as Navigator & { userAgentData?: { platform?: string } }; + const platform = nav.userAgentData?.platform ?? ""; + const isMac = platform + ? /mac/i.test(platform) + : /mac|iphone|ipod|ipad/i.test(navigator.userAgent); + if (isMac) { + btn.setAttribute("aria-keyshortcuts", "Meta+K"); + const key = btn.querySelector("[data-shortcut-key]"); + if (key) key.textContent = "⌘"; + } + }); + } + + initSearchTrigger(); + document.addEventListener("astro:page-load", initSearchTrigger); +</script> diff --git a/src/nimbus/components/ui/search/index.ts b/src/nimbus/components/ui/search/index.ts new file mode 100644 index 00000000000..9f7c86a13b4 --- /dev/null +++ b/src/nimbus/components/ui/search/index.ts @@ -0,0 +1,2 @@ +export { default as SearchDialog } from "./SearchDialog.astro"; +export { default as SearchTrigger } from "./SearchTrigger.astro"; diff --git a/src/nimbus/components/ui/search/providers/pagefind.ts b/src/nimbus/components/ui/search/providers/pagefind.ts new file mode 100644 index 00000000000..110bf77b4f0 --- /dev/null +++ b/src/nimbus/components/ui/search/providers/pagefind.ts @@ -0,0 +1,80 @@ +import type { SearchProvider, SearchResult } from "nimbus-docs/types"; +import { config } from "virtual:nimbus/config"; + +interface PagefindSubResult { + title?: string; + url?: string; +} + +interface PagefindResultData { + url: string; + excerpt?: string; + meta?: { title?: string }; + sub_results?: PagefindSubResult[]; +} + +interface PagefindSearchResponse { + results: Array<{ data(): Promise<PagefindResultData> }>; +} + +interface PagefindFilters { + [key: string]: string | string[] | { none?: string | string[]; any?: string | string[] }; +} + +interface PagefindApi { + init(): Promise<void>; + search(query: string, options?: { filters?: PagefindFilters }): Promise<PagefindSearchResponse>; +} + +let pagefind: PagefindApi | undefined; + +/** + * Default Pagefind filters applied to every search. + * + * Versioning P3: when the site has a `versions.deprecated` list, the + * layout emits `data-pagefind-filter="status:deprecated"` on every + * deprecated-version page. Search defaults to excluding those results + * (readers searching for "auth" want the current version's auth page, + * not the deprecated one). Future UI work can expose a "include + * deprecated" toggle; for now the default is current + non-deprecated. + * + * Versions are still searchable individually — readers on a v0 page + * who explicitly search from there can opt the UI into a version-scoped + * filter. The default exclusion is just for the top-level search. + * + * Computed at module-import time so we don't pay the config lookup on + * every keystroke. + */ +const defaultFilters: PagefindFilters | undefined = + config.versions && config.versions.deprecated && config.versions.deprecated.length > 0 + ? { status: { none: "deprecated" } } + : undefined; + +export const provider: SearchProvider = { + async init() { + if (pagefind) return; + const baseUrl = new URL(import.meta.env.BASE_URL ?? "/", window.location.origin); + const pagefindUrl = new URL("pagefind/pagefind.js", baseUrl); + pagefind = (await import(/* @vite-ignore */ pagefindUrl.href)) as PagefindApi; + await pagefind.init(); + }, + + async search(query) { + if (!pagefind) await this.init?.(); + if (!pagefind) return []; + + const search = await pagefind.search( + query, + defaultFilters ? { filters: defaultFilters } : undefined, + ); + const results = await Promise.all(search.results.slice(0, 10).map((result) => result.data())); + return results.map((result): SearchResult => ({ + title: result.meta?.title ?? "Untitled", + url: result.url, + snippet: result.excerpt, + subResults: result.sub_results + ?.filter((sub): sub is Required<PagefindSubResult> => Boolean(sub.title && sub.url)) + .map((sub) => ({ title: sub.title, url: sub.url })), + })); + }, +}; diff --git a/src/nimbus/components/ui/search/search.client.ts b/src/nimbus/components/ui/search/search.client.ts new file mode 100644 index 00000000000..05235c19263 --- /dev/null +++ b/src/nimbus/components/ui/search/search.client.ts @@ -0,0 +1,201 @@ +import type { SearchProvider, SearchResult } from "nimbus-docs/types"; + +export interface SearchConfig { + input: HTMLInputElement; + resultsContainer: HTMLElement; + emptyState: HTMLElement; + provider: SearchProvider; + onNavigate?: () => void; +} + +export interface SearchInstance { + reset(): Promise<void>; + destroy(): void; +} + +export function initSearch(config: SearchConfig): SearchInstance { + const { input, resultsContainer, emptyState, provider, onNavigate } = config; + + let initialized = false; + let activeIndex = -1; + let resultIdCounter = 0; + let debounceTimer: ReturnType<typeof setTimeout> | undefined; + let activeController: AbortController | undefined; + + function getOptions(): HTMLElement[] { + return Array.from(resultsContainer.querySelectorAll<HTMLElement>("[role='option']")); + } + + function updateActive(newIndex: number): void { + const options = getOptions(); + if (options.length === 0) { + activeIndex = -1; + input.removeAttribute("aria-activedescendant"); + return; + } + activeIndex = Math.max(-1, Math.min(newIndex, options.length - 1)); + options.forEach((option, index) => { + if (index === activeIndex) { + option.setAttribute("data-highlighted", ""); + option.scrollIntoView({ block: "nearest" }); + input.setAttribute("aria-activedescendant", option.id); + } else { + option.removeAttribute("data-highlighted"); + } + }); + if (activeIndex < 0) input.removeAttribute("aria-activedescendant"); + } + + function clearResults(): void { + for (const result of resultsContainer.querySelectorAll("[role='option']")) result.remove(); + input.setAttribute("aria-expanded", "false"); + input.removeAttribute("aria-activedescendant"); + } + + function resultLink(title: string, href: string, className: string): HTMLAnchorElement { + const link = document.createElement("a"); + link.href = href; + link.className = className; + link.textContent = title; + link.addEventListener("click", () => onNavigate?.()); + return link; + } + + function buildResult(result: SearchResult): HTMLElement { + const option = document.createElement("div"); + option.id = `search-result-${resultIdCounter++}`; + option.setAttribute("role", "option"); + option.className = "rounded-lg px-2 py-2 transition-colors cursor-pointer hover:bg-accent focus-within:bg-accent data-[highlighted]:bg-accent"; + + const link = resultLink(result.title, result.url, "block truncate text-sm font-medium text-foreground no-underline focus-visible:outline-none"); + option.appendChild(link); + + if (result.snippet) { + const snippet = document.createElement("p"); + snippet.className = "mt-1 line-clamp-2 text-xs leading-relaxed text-muted-foreground"; + snippet.innerHTML = result.snippet; + option.appendChild(snippet); + } + + if (result.subResults?.length) { + const subList = document.createElement("div"); + subList.className = "mt-2 border-l border-border pl-3"; + for (const sub of result.subResults.slice(0, 3)) { + subList.appendChild(resultLink(sub.title, sub.url, "block truncate py-0.5 text-xs text-muted-foreground no-underline hover:text-foreground")); + } + option.appendChild(subList); + } + + option.addEventListener("click", (event) => { + if ((event.target as Element | null)?.closest("a")) return; + link.click(); + }); + + return option; + } + + async function ensureInitialized(): Promise<boolean> { + if (initialized) return true; + try { + await provider.init?.(); + initialized = true; + return true; + } catch { + emptyState.textContent = "Search is available after a production build."; + return false; + } + } + + async function runSearch(query: string): Promise<void> { + activeController?.abort(); + activeController = new AbortController(); + const signal = activeController.signal; + + emptyState.style.display = ""; + emptyState.textContent = "Searching…"; + clearResults(); + + if (!(await ensureInitialized()) || signal.aborted) return; + + try { + const results = await provider.search(query, { signal }); + if (signal.aborted) return; + + clearResults(); + activeIndex = -1; + + if (results.length === 0) { + emptyState.style.display = ""; + emptyState.textContent = "No results found."; + return; + } + + emptyState.style.display = "none"; + input.setAttribute("aria-expanded", "true"); + for (const result of results) resultsContainer.appendChild(buildResult(result)); + } catch { + if (signal.aborted) return; + clearResults(); + emptyState.style.display = ""; + emptyState.textContent = "Search is temporarily unavailable."; + } + } + + function handleInput(): void { + if (debounceTimer) clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => { + const query = input.value.trim(); + if (!query) { + activeController?.abort(); + clearResults(); + emptyState.style.display = ""; + emptyState.textContent = "Type to search…"; + return; + } + void runSearch(query); + }, 150); + } + + function handleKeydown(event: KeyboardEvent): void { + const options = getOptions(); + if (event.key === "ArrowDown") { + event.preventDefault(); + updateActive(activeIndex + 1); + } else if (event.key === "ArrowUp") { + event.preventDefault(); + updateActive(activeIndex - 1); + } else if (event.key === "Home") { + event.preventDefault(); + updateActive(0); + } else if (event.key === "End") { + event.preventDefault(); + updateActive(options.length - 1); + } else if (event.key === "Enter" && activeIndex >= 0) { + event.preventDefault(); + options[activeIndex]?.querySelector<HTMLAnchorElement>("a")?.click(); + } + } + + input.addEventListener("input", handleInput); + input.closest("dialog")?.addEventListener("keydown", handleKeydown); + + return { + async reset() { + activeController?.abort(); + if (debounceTimer) clearTimeout(debounceTimer); + input.value = ""; + input.focus(); + activeIndex = -1; + clearResults(); + emptyState.style.display = ""; + emptyState.textContent = "Type to search…"; + await ensureInitialized(); + }, + destroy() { + activeController?.abort(); + if (debounceTimer) clearTimeout(debounceTimer); + input.removeEventListener("input", handleInput); + input.closest("dialog")?.removeEventListener("keydown", handleKeydown); + }, + }; +} diff --git a/src/nimbus/components/ui/sidebar/Sidebar.astro b/src/nimbus/components/ui/sidebar/Sidebar.astro new file mode 100644 index 00000000000..a3e11407b56 --- /dev/null +++ b/src/nimbus/components/ui/sidebar/Sidebar.astro @@ -0,0 +1,63 @@ +--- +/** + * Sidebar — recursive navigation tree from a `SidebarItem[]`. Composes + * SidebarGroup + SidebarLink. Pass `persist` to opt into sessionStorage + * for open/scroll state (desktop only). + */ +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; +import SidebarGroup from "./SidebarGroup.astro"; +import SidebarLink from "./SidebarLink.astro"; +import type { SidebarItem } from "nimbus-docs/types"; +import { sidebarHash } from "nimbus-docs"; + +interface Props extends HTMLAttributes<"div"> { + items: SidebarItem[]; + /** Persist open/scroll state to sessionStorage. Desktop sidebar only. */ + persist?: boolean; +} + +const { items, persist = false, class: className, ...attrs } = Astro.props; +const hash = sidebarHash(items); +--- + +<div + data-nb-sidebar + data-nb-sidebar-hash={hash} + data-nb-sidebar-persist={persist ? "" : undefined} + class={cn(className)} + {...attrs} +> + <ul class="top-level flex list-none flex-col gap-0.5 p-0"> + {items.map((item) => + item.type === "group" ? ( + <li> + <SidebarGroup + label={item.label} + items={item.children} + collapsed={item.collapsed} + badge={item.badge} + indexHref={item.indexHref} + indexIsCurrent={item.indexIsCurrent} + /> + </li> + ) : item.type === "external" ? ( + <li> + <SidebarLink label={item.label} href={item.href} badge={item.badge} target="_blank" rel="noopener" /> + </li> + ) : ( + <li> + <SidebarLink label={item.label} href={item.href} isCurrent={item.isCurrent} badge={item.badge} /> + </li> + ), + )} + </ul> +</div> + +<script> + import "./sidebar.client"; +</script> + +<style is:global> + [data-nb-sidebar-hidden] { display: none; } +</style> diff --git a/src/nimbus/components/ui/sidebar/SidebarFilter.astro b/src/nimbus/components/ui/sidebar/SidebarFilter.astro new file mode 100644 index 00000000000..6b8a4d3f264 --- /dev/null +++ b/src/nimbus/components/ui/sidebar/SidebarFilter.astro @@ -0,0 +1,26 @@ +--- +/** SidebarFilter — text input that filters the adjacent Sidebar. Press "/" to focus. */ +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"div"> { + placeholder?: string; +} + +const { + class: className, + placeholder = "Search sidebar...", + ...attrs +} = Astro.props; +--- + +<div class={cn("mb-5", className)} {...attrs}> + <input + data-nb-sidebar-filter-input + type="search" + placeholder={placeholder} + class="w-full rounded-md border border-border bg-card px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus-visible:border-brand focus-visible:outline-2 focus-visible:outline-ring" + aria-label="Filter navigation" + autocomplete="off" + /> +</div> diff --git a/src/nimbus/components/ui/sidebar/SidebarGroup.astro b/src/nimbus/components/ui/sidebar/SidebarGroup.astro new file mode 100644 index 00000000000..d8646efd2c6 --- /dev/null +++ b/src/nimbus/components/ui/sidebar/SidebarGroup.astro @@ -0,0 +1,164 @@ +--- +/** + * SidebarGroup — autogenerated section header in the sidebar rail. + * + * Renders in one of two shapes depending on whether the group has a + * landing page (`indexHref`): + * + * - Has landing: the label is an `<a>` linking to indexHref. The + * collapse caret sits next to it as a separate `<button>`. Matches + * the structural-separation pattern used by Fumadocs / Fern / + * Docusaurus: the group label IS the link to the landing page; + * children are listed separately below. + * - No landing: the entire row is a single `<button>` that toggles + * the collapse. Label is non-interactive. Used for directories + * without an `index.mdx`, where the group is a pure visual section + * divider over its children. + */ +import { Icon } from "astro-icon/components"; +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; +import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible"; +import SidebarLink from "./SidebarLink.astro"; +import { Badge } from "@/components/ui/badge"; +import type { SidebarItem, SidebarBadge } from "nimbus-docs/types"; + +interface Props extends HTMLAttributes<"div"> { + label: string; + items: SidebarItem[]; + collapsed?: boolean; + badge?: SidebarBadge; + /** Optional leading icon (astro-icon name, e.g. a product glyph). */ + icon?: string; + /** Landing-page URL when the group has an `index.mdx`. Renders the label as a link. */ + indexHref?: string; + /** True when the landing page is the current route. */ + indexIsCurrent?: boolean; +} + +const { + label, + items, + collapsed, + badge, + icon, + indexHref, + indexIsCurrent, + class: className, + ...attrs +} = Astro.props; + +function hasActiveDescendant(items: SidebarItem[]): boolean { + return items.some((item) => + item.type === "link" + ? Boolean(item.isCurrent) + : item.type === "group" + ? Boolean(item.indexIsCurrent) || hasActiveDescendant(item.children) + : false, + ); +} + +const hasActive = Boolean(indexIsCurrent) || hasActiveDescendant(items); +const isOpen = Boolean(hasActive || collapsed === false || collapsed === undefined); +--- + +<Collapsible + class={cn(className)} + open={isOpen} + data-nb-sidebar-group + {...attrs} +> + {indexHref ? ( + <div + data-nb-state={isOpen ? "open" : "closed"} + data-nb-sidebar-group-label + data-nb-active={hasActive ? "" : undefined} + data-nb-current={indexIsCurrent ? "" : undefined} + > + <a + href={indexHref} + aria-current={indexIsCurrent ? "page" : undefined} + data-nb-sidebar-group-link + > + <span data-nb-sidebar-group-text> + {icon && <Icon name={icon} class="shrink-0 w-4 h-4 text-muted-foreground" />} + <span class="break-words">{label}</span> + {badge && + (typeof badge === "string" ? ( + <Badge text={badge} /> + ) : ( + <Badge text={badge.text} variant={badge.variant} /> + ))} + </span> + </a> + <CollapsibleTrigger + class="w-auto" + data-nb-sidebar-group-toggle + data-nb-state={isOpen ? "open" : "closed"} + aria-expanded={isOpen ? "true" : "false"} + aria-label={`Toggle ${label} section`} + > + <span data-nb-caret aria-hidden="true"></span> + </CollapsibleTrigger> + </div> + ) : ( + <CollapsibleTrigger + data-nb-sidebar-group-label + data-nb-active={hasActive ? "" : undefined} + data-nb-state={isOpen ? "open" : "closed"} + aria-expanded={isOpen ? "true" : "false"} + > + <span data-nb-sidebar-group-text> + {icon && <Icon name={icon} class="shrink-0 w-4 h-4 text-muted-foreground" />} + <span class="break-words">{label}</span> + {badge && + (typeof badge === "string" ? ( + <Badge text={badge} /> + ) : ( + <Badge text={badge.text} variant={badge.variant} /> + ))} + </span> + <span data-nb-caret aria-hidden="true"></span> + </CollapsibleTrigger> + )} + + <CollapsibleContent data-nb-state={isOpen ? "open" : "closed"}> + <ul data-nb-sidebar-sublist> + {items.map((item) => ( + <li class="break-words"> + {item.type === "group" ? ( + <Astro.self + label={item.label} + items={item.children} + collapsed={item.collapsed} + badge={item.badge} + indexHref={item.indexHref} + indexIsCurrent={item.indexIsCurrent} + /> + ) : item.type === "external" ? ( + <SidebarLink label={item.label} href={item.href} badge={item.badge} target="_blank" rel="noopener" /> + ) : ( + <SidebarLink label={item.label} href={item.href} isCurrent={item.isCurrent} badge={item.badge} /> + )} + </li> + ))} + </ul> + </CollapsibleContent> +</Collapsible> + +<style is:global> + /* + * Chevron rotation, driven by the trigger's `data-nb-state`. The + * disclosure runtime (`@/components/ui/collapsible/collapsible.client`) + * keeps this attribute in sync on toggle; this rule turns the + * attribute change into the visual rotation. + * + * Direct CSS rather than Tailwind's `group-data-[...]/expander:rotate-90` + * variant — the variant compiled but didn't reliably fire in practice + * when paired with the named-group split between trigger and outer + * row container. + */ + [data-nb-collapsible-trigger][data-nb-state="open"] [data-nb-caret] { + rotate: 90deg; + } +</style> diff --git a/src/nimbus/components/ui/sidebar/SidebarLink.astro b/src/nimbus/components/ui/sidebar/SidebarLink.astro new file mode 100644 index 00000000000..5f0a96efb4b --- /dev/null +++ b/src/nimbus/components/ui/sidebar/SidebarLink.astro @@ -0,0 +1,31 @@ +--- +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; +import { Badge } from "@/components/ui/badge"; +import type { SidebarBadge } from "nimbus-docs/types"; + +interface Props extends HTMLAttributes<"a"> { + label: string; + href: string; + isCurrent?: boolean; + badge?: SidebarBadge; +} + +const { label, href, isCurrent, badge, class: className, ...attrs } = Astro.props; +--- + +<a + href={href} + aria-current={isCurrent ? "page" : undefined} + data-nb-sidebar-link + class={className ? cn(className) : undefined} + {...attrs} +> + <span class="break-words">{label}</span> + {badge && + (typeof badge === "string" ? ( + <Badge text={badge} /> + ) : ( + <Badge text={badge.text} variant={badge.variant} /> + ))} +</a> diff --git a/src/nimbus/components/ui/sidebar/SidebarProductNav.astro b/src/nimbus/components/ui/sidebar/SidebarProductNav.astro new file mode 100644 index 00000000000..377b44d9431 --- /dev/null +++ b/src/nimbus/components/ui/sidebar/SidebarProductNav.astro @@ -0,0 +1,37 @@ +--- +/** + * SidebarProductNav — sidebar header with two targets on one dense row: + * an "all products" button (→ home) and the current product title + * (→ product overview). Text-first: the title gets the full remaining width. + */ +import { Icon } from "astro-icon/components"; + +interface Props { + /** Current product name. */ + title: string; + /** Product overview URL — the title links here. */ + href: string; + /** Where the "all products" button goes. Defaults to the site home. */ + homeHref?: string; + class?: string; +} + +const { title, href, homeHref = "/", class: className } = Astro.props; +--- + +<div class:list={["flex items-center gap-1", className]}> + <a + href={homeHref} + aria-label="All products" + class="flex size-10 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-[background-color,color,scale] duration-150 ease-out hover:bg-accent hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-1 active:scale-[0.96]" + > + <Icon name="ph:squares-four" aria-hidden="true" class="h-[1.125rem] w-[1.125rem]" /> + </a> + <span class="mx-0.5 h-5 w-px shrink-0 bg-border" aria-hidden="true"></span> + <a + href={href} + class="flex min-h-10 min-w-0 flex-1 items-center rounded-lg px-2.5 no-underline transition-[background-color,scale] duration-150 hover:bg-accent focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-1 active:scale-[0.96]" + > + <span class="truncate text-sm font-semibold text-foreground">{title}</span> + </a> +</div> diff --git a/src/nimbus/components/ui/sidebar/index.ts b/src/nimbus/components/ui/sidebar/index.ts new file mode 100644 index 00000000000..bad628072c5 --- /dev/null +++ b/src/nimbus/components/ui/sidebar/index.ts @@ -0,0 +1,5 @@ +export { default as Sidebar } from "./Sidebar.astro"; +export { default as SidebarFilter } from "./SidebarFilter.astro"; +export { default as SidebarProductNav } from "./SidebarProductNav.astro"; +export { default as SidebarGroup } from "./SidebarGroup.astro"; +export { default as SidebarLink } from "./SidebarLink.astro"; diff --git a/src/nimbus/components/ui/sidebar/sidebar.client.ts b/src/nimbus/components/ui/sidebar/sidebar.client.ts new file mode 100644 index 00000000000..c170b2c3351 --- /dev/null +++ b/src/nimbus/components/ui/sidebar/sidebar.client.ts @@ -0,0 +1,213 @@ +/** Sidebar runtime: filter, persistence, "/" shortcut. */ + +import { mount } from "nimbus-docs/client"; + +const STORAGE_KEY = "sidebar-state"; + +interface SidebarState { + hash: string; + open: boolean[]; + scroll: number; +} + +function initSidebar(root: HTMLElement): () => void { + const teardowns: Array<() => void> = []; + const persist = root.hasAttribute("data-nb-sidebar-persist"); + + const filterTeardown = initFilter(root); + if (filterTeardown) teardowns.push(filterTeardown); + + if (persist) { + const persistTeardown = initPersistence(root); + if (persistTeardown) teardowns.push(persistTeardown); + } + + return () => teardowns.forEach((t) => t()); +} + +// --------------------------------------------------------------------------- +// Filter +// --------------------------------------------------------------------------- + +function initFilter(root: HTMLElement): (() => void) | null { + const input = root.querySelector<HTMLInputElement>("[data-nb-sidebar-filter-input]"); + // SidebarFilter is rendered *next to* Sidebar (sibling), so also look in + // the parent — preserves the existing layout where filter sits above. + const inputElement = + input ?? + root.closest("[data-shared-sidebar-nav]")?.querySelector<HTMLInputElement>("[data-nb-sidebar-filter-input]") ?? + root.parentElement?.querySelector<HTMLInputElement>("[data-nb-sidebar-filter-input]") ?? + null; + if (!inputElement) return null; + + function handleInput() { + const query = inputElement!.value.trim().toLowerCase(); + if (!query) { + resetFilter(root); + return; + } + applyFilter(root, query); + } + + function handleKeydown(e: KeyboardEvent) { + if (e.key === "Escape") { + inputElement!.value = ""; + handleInput(); + inputElement!.blur(); + } + } + + inputElement.addEventListener("input", handleInput); + inputElement.addEventListener("keydown", handleKeydown); + + return () => { + inputElement.removeEventListener("input", handleInput); + inputElement.removeEventListener("keydown", handleKeydown); + resetFilter(root); + }; +} + +function resetFilter(root: HTMLElement): void { + root.querySelectorAll<HTMLElement>("[data-nb-sidebar-hidden]").forEach((el) => { + el.removeAttribute("data-nb-sidebar-hidden"); + }); + // Reset groups opened by the filter back to their saved state. + root + .querySelectorAll<HTMLElement>("[data-nb-sidebar-group][data-nb-opened-by-filter]") + .forEach((group) => { + const trigger = group.querySelector<HTMLElement>("[data-nb-collapsible-trigger]"); + trigger?.click(); + group.removeAttribute("data-nb-opened-by-filter"); + }); +} + +function applyFilter(root: HTMLElement, query: string): void { + const links = root.querySelectorAll<HTMLElement>("[data-nb-sidebar-link]"); + const groups = root.querySelectorAll<HTMLElement>("[data-nb-sidebar-group]"); + + links.forEach((link) => link.setAttribute("data-nb-sidebar-hidden", "")); + groups.forEach((group) => group.setAttribute("data-nb-sidebar-hidden", "")); + + links.forEach((link) => { + const text = link.textContent?.toLowerCase() ?? ""; + if (!text.includes(query)) return; + link.removeAttribute("data-nb-sidebar-hidden"); + revealAncestors(link, root); + }); + + groups.forEach((group) => { + const label = group.querySelector("[data-nb-sidebar-group-label]"); + const text = label?.textContent?.toLowerCase() ?? ""; + if (!text.includes(query)) return; + group.removeAttribute("data-nb-sidebar-hidden"); + openGroup(group); + group.querySelectorAll<HTMLElement>("[data-nb-sidebar-link], [data-nb-sidebar-group]") + .forEach((child) => child.removeAttribute("data-nb-sidebar-hidden")); + }); +} + +function revealAncestors(el: HTMLElement, scope: Element): void { + let parent: HTMLElement | null = el.parentElement; + while (parent && parent !== scope) { + if (parent.hasAttribute("data-nb-sidebar-group")) { + parent.removeAttribute("data-nb-sidebar-hidden"); + openGroup(parent); + } + parent = parent.parentElement; + } +} + +function openGroup(group: HTMLElement): void { + const trigger = group.querySelector<HTMLElement>("[data-nb-collapsible-trigger]"); + if (!trigger) return; + if (trigger.getAttribute("data-nb-state") === "open") return; + group.setAttribute("data-nb-opened-by-filter", ""); + trigger.click(); +} + +// --------------------------------------------------------------------------- +// Persistence (open state + scroll) +// --------------------------------------------------------------------------- + +function initPersistence(root: HTMLElement): (() => void) | null { + // The scrollable container is the closest <aside> or the root itself. + const scrollHost: HTMLElement = root.closest("aside") ?? root; + const hash = root.dataset.nbSidebarHash ?? ""; + + function readState(): SidebarState { + const groups = root.querySelectorAll<HTMLElement>("[data-nb-sidebar-group]"); + const open: boolean[] = []; + groups.forEach((group) => { + const trigger = group.querySelector<HTMLElement>("[data-nb-collapsible-trigger]"); + open.push(trigger?.getAttribute("data-nb-state") === "open"); + }); + return { hash, open, scroll: scrollHost.scrollTop }; + } + + function save() { + if (root.closest("[data-mobile-sidebar]")) return; + try { + sessionStorage.setItem(STORAGE_KEY, JSON.stringify(readState())); + } catch {} + } + + // Observe state changes on each group's trigger. + const observer = new MutationObserver(save); + root.querySelectorAll<HTMLElement>("[data-nb-collapsible-trigger]").forEach((trigger) => { + observer.observe(trigger, { + attributes: true, + attributeFilter: ["data-nb-state"], + }); + }); + + function handleVisibility() { + if (document.visibilityState === "hidden") save(); + } + document.addEventListener("visibilitychange", handleVisibility); + window.addEventListener("pagehide", save); + + let raf = 0; + function handleScroll() { + cancelAnimationFrame(raf); + raf = requestAnimationFrame(save); + } + scrollHost.addEventListener("scroll", handleScroll); + + return () => { + observer.disconnect(); + document.removeEventListener("visibilitychange", handleVisibility); + window.removeEventListener("pagehide", save); + scrollHost.removeEventListener("scroll", handleScroll); + cancelAnimationFrame(raf); + }; +} + +// --------------------------------------------------------------------------- +// Global `/` shortcut — bound once at module load +// --------------------------------------------------------------------------- + +(function bindFilterShortcut() { + if (document.documentElement.hasAttribute("data-nb-sidebar-shortcut-bound")) return; + document.documentElement.setAttribute("data-nb-sidebar-shortcut-bound", ""); + + document.addEventListener("keydown", (e) => { + if (e.key !== "/") return; + const active = document.activeElement as HTMLElement | null; + if ( + active && + (active.tagName === "INPUT" || + active.tagName === "TEXTAREA" || + active.isContentEditable) + ) { + return; + } + const desktopInput = document.querySelector<HTMLInputElement>( + "[data-nb-sidebar-persist] ~ * [data-nb-sidebar-filter-input], [data-nb-desktop-sidebar] [data-nb-sidebar-filter-input]", + ); + if (!desktopInput) return; + e.preventDefault(); + desktopInput.focus(); + }); +})(); + +mount("[data-nb-sidebar]", initSidebar); diff --git a/src/nimbus/components/ui/steps/Step.astro b/src/nimbus/components/ui/steps/Step.astro new file mode 100644 index 00000000000..e63869dc272 --- /dev/null +++ b/src/nimbus/components/ui/steps/Step.astro @@ -0,0 +1,26 @@ +--- +/** + * Step — a single step inside <Steps>. Alternative to authoring an + * <ol><li> directly. + * + * <Steps> + * <Step title="Install">Run <code>npm install</code>.</Step> + * <Step title="Configure">Edit <code>config.json</code>.</Step> + * </Steps> + */ +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"div"> { + title?: string; +} + +const { title, class: className, ...attrs } = Astro.props; +--- + +<div data-step class={cn(className)} {...attrs}> + {title && <p class="m-0 font-semibold text-foreground">{title}</p>} + <div class="mt-1 text-sm leading-snug text-foreground [&_p:first-child]:mt-0 [&_p:last-child]:mb-0"> + <slot /> + </div> +</div> diff --git a/src/nimbus/components/ui/steps/Steps.astro b/src/nimbus/components/ui/steps/Steps.astro new file mode 100644 index 00000000000..7174b9f1583 --- /dev/null +++ b/src/nimbus/components/ui/steps/Steps.astro @@ -0,0 +1,115 @@ +--- +/** + * Steps — ordered list with numbered circles and connecting lines. + * Wrap a markdown ordered list. Set `start` to offset the counter. + */ +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"div"> { + start?: number; +} + +const { start, class: className, ...attrs } = Astro.props; +--- + +<div + data-nb-steps + class={cn("steps", className)} + style={start !== undefined ? `--steps-start: ${start};` : undefined} + {...attrs} +> + <slot /> +</div> + +<script> + import "./steps.client"; +</script> + +<style> + .steps { + --_start: var(--steps-start, 1); + margin: 1.5rem 0; + counter-reset: step calc(var(--_start) - 1); + } + + .steps :global(ol) { + counter-reset: step calc(var(--_start) - 1); + list-style: none; + margin: 0; + padding: 0; + } + + /* Markdown mode (ol > li) AND component mode ([data-step]) share styling. + Grid layout so the circle (col 1) lines up with the first row of column 2 + via `align-items: center` — no font-metric guesswork. */ + .steps :global(ol > li), + .steps :global([data-step]) { + display: grid; + grid-template-columns: 1.75rem 1fr; + column-gap: 0.75rem; + row-gap: 0.5rem; + align-items: center; + counter-increment: step; + position: relative; + min-height: 1.75rem; + /* Cancel the prose li margin — step rhythm comes from padding/row-gap. */ + margin: 0; + padding-bottom: 1.5rem; + } + + .steps :global(ol > li:last-child), + .steps :global([data-step]:last-child) { + padding-bottom: 0; + } + + /* All non-marker content lives in column 2 — prevents the second+ child + from falling into column 1 (which is reserved for the circle). The + child selector lives inside :global() so Astro's scope hash doesn't + skip elements coming from Step.astro. */ + .steps :global(ol > li > *), + .steps :global([data-step] > *) { + grid-column: 2; + min-width: 0; + } + + /* Number marker — auto-placed in row 1 col 1, centered with col 2's first row. */ + .steps :global(ol > li)::before, + .steps :global([data-step])::before { + content: counter(step); + grid-row: 1; + grid-column: 1; + width: 1.75rem; + height: 1.75rem; + border-radius: 0.5rem; + background: var(--nb-accent); + color: var(--nb-foreground); + border: 1px solid var(--nb-border-strong); + font-size: 0.6875rem; + font-weight: 600; + display: grid; + place-items: center; + z-index: 1; + } + + /* Connecting line — runs from below the circle to the bottom of the step. */ + .steps :global(ol > li:not(:last-child))::after, + .steps :global([data-step]:not(:last-child))::after { + content: ""; + position: absolute; + left: calc(1.75rem / 2 - 0.75px); + top: 1.75rem; + bottom: 0; + width: 1.5px; + background: var(--nb-border); + } + + .steps :global(ol > li > p:first-child > strong:only-child) { + color: var(--nb-foreground); + } + + /* Vertical rhythm comes from the grid row-gap, not p margins. */ + .steps :global(ol > li > p) { + margin: 0; + } +</style> diff --git a/src/nimbus/components/ui/steps/index.ts b/src/nimbus/components/ui/steps/index.ts new file mode 100644 index 00000000000..ae327b22e86 --- /dev/null +++ b/src/nimbus/components/ui/steps/index.ts @@ -0,0 +1,2 @@ +export { default as Steps } from "./Steps.astro"; +export { default as Step } from "./Step.astro"; diff --git a/src/nimbus/components/ui/steps/steps.client.ts b/src/nimbus/components/ui/steps/steps.client.ts new file mode 100644 index 00000000000..69f2f378884 --- /dev/null +++ b/src/nimbus/components/ui/steps/steps.client.ts @@ -0,0 +1,20 @@ +/** + * steps.client.ts — Safari list-role restoration. + * + * Safari strips list semantics when `list-style: none` is applied + * (which we do for the numbered counter styling). Restoring `role="list"` + * on the inner `<ol>` makes VoiceOver announce the item count again. + */ + +import { mount } from "nimbus-docs/client"; + +function initSteps(root: HTMLElement): () => void { + const lists = root.querySelectorAll<HTMLOListElement>("ol"); + lists.forEach((ol) => ol.setAttribute("role", "list")); + + return () => { + lists.forEach((ol) => ol.removeAttribute("role")); + }; +} + +mount("[data-nb-steps]", initSteps); diff --git a/src/nimbus/components/ui/tabs/TabItem.astro b/src/nimbus/components/ui/tabs/TabItem.astro new file mode 100644 index 00000000000..25fd399d9da --- /dev/null +++ b/src/nimbus/components/ui/tabs/TabItem.astro @@ -0,0 +1,30 @@ +--- +/** + * TabItem — single tab panel in the Starlight-compatible `<Tabs>` usage. + * + * The `label` prop is read by `tabs.client.ts` to synthesize the matching + * trigger button. Each TabItem becomes one `[data-nb-tabs-content]` panel. + */ +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"div"> { + label: string; +} + +const { label, class: className, ...attrs } = Astro.props; + +if (!label) { + throw new Error("Missing required `label` prop on `<TabItem>` component."); +} +--- + +<div + role="tabpanel" + data-nb-tabs-content + data-nb-tab-label={label} + class={cn(className)} + {...attrs} +> + <slot /> +</div> diff --git a/src/nimbus/components/ui/tabs/Tabs.astro b/src/nimbus/components/ui/tabs/Tabs.astro new file mode 100644 index 00000000000..e5c356fa049 --- /dev/null +++ b/src/nimbus/components/ui/tabs/Tabs.astro @@ -0,0 +1,36 @@ +--- +/** + * <Tabs syncKey="pkg"> + * <TabItem label="npm">npm install</TabItem> + * <TabItem label="pnpm">pnpm install</TabItem> + * </Tabs> + * + * For manual control: TabsList, TabsTrigger, TabsContent. + */ +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; +import TabsList from "./TabsList.astro"; + +interface Props extends HTMLAttributes<"div"> { + /** Sync tab selection across instances with the same key via localStorage */ + syncKey?: string; +} + +const { syncKey, class: className, ...attrs } = Astro.props; +--- + +<div + data-nb-tabs + data-nb-sync-key={syncKey} + class={cn(className)} + {...attrs} +> + <TabsList /> + <div class="mt-3"> + <slot /> + </div> +</div> + +<script> + import "./tabs.client"; +</script> diff --git a/src/nimbus/components/ui/tabs/TabsContent.astro b/src/nimbus/components/ui/tabs/TabsContent.astro new file mode 100644 index 00000000000..6e6d5c49a75 --- /dev/null +++ b/src/nimbus/components/ui/tabs/TabsContent.astro @@ -0,0 +1,24 @@ +--- +/** + * TabsContent — individual tab panel matched to a TabsTrigger by value. + */ +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"div"> { + /** Value matching a TabsTrigger. */ + value: string; +} + +const { value, class: className, ...attrs } = Astro.props; +--- + +<div + role="tabpanel" + data-nb-tabs-content + data-nb-value={value} + class={cn(className)} + {...attrs} +> + <slot /> +</div> diff --git a/src/nimbus/components/ui/tabs/TabsList.astro b/src/nimbus/components/ui/tabs/TabsList.astro new file mode 100644 index 00000000000..44db8ee8241 --- /dev/null +++ b/src/nimbus/components/ui/tabs/TabsList.astro @@ -0,0 +1,26 @@ +--- +/** + * TabsList — container for tab triggers with animated indicator bar. + * Renders `role="tablist"` with a sliding underline indicator. + */ +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"div"> {} + +const { class: className, ...attrs } = Astro.props; +--- + +<div + class={cn("relative flex border-b border-border", className)} + role="tablist" + data-nb-tabs-list + {...attrs} +> + <span + class="pointer-events-none absolute -bottom-px h-0.5 rounded-t-sm bg-primary transition-[left,width] duration-200 ease-out" + data-nb-tabs-indicator + aria-hidden="true" + ></span> + <slot /> +</div> diff --git a/src/nimbus/components/ui/tabs/TabsTrigger.astro b/src/nimbus/components/ui/tabs/TabsTrigger.astro new file mode 100644 index 00000000000..ce0060cc820 --- /dev/null +++ b/src/nimbus/components/ui/tabs/TabsTrigger.astro @@ -0,0 +1,31 @@ +--- +/** + * TabsTrigger — individual tab button within a TabsList. + * Use `value` to match with a TabsContent of the same value. + */ +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"button"> { + /** Value matching a TabsContent. */ + value: string; +} + +const { value, class: className, ...attrs } = Astro.props; +--- + +<button + role="tab" + type="button" + data-nb-tabs-trigger + data-nb-value={value} + class={cn( + "cursor-pointer px-4 py-2 text-sm font-medium leading-6 whitespace-nowrap text-muted-foreground transition-colors", + "hover:text-foreground aria-selected:text-primary", + "focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-[-2px]", + className, + )} + {...attrs} +> + <slot /> +</button> diff --git a/src/nimbus/components/ui/tabs/index.ts b/src/nimbus/components/ui/tabs/index.ts new file mode 100644 index 00000000000..07214f69b06 --- /dev/null +++ b/src/nimbus/components/ui/tabs/index.ts @@ -0,0 +1,5 @@ +export { default as Tabs } from "./Tabs.astro"; +export { default as TabItem } from "./TabItem.astro"; +export { default as TabsList } from "./TabsList.astro"; +export { default as TabsTrigger } from "./TabsTrigger.astro"; +export { default as TabsContent } from "./TabsContent.astro"; diff --git a/src/nimbus/components/ui/tabs/tabs.client.ts b/src/nimbus/components/ui/tabs/tabs.client.ts new file mode 100644 index 00000000000..0d9bcf6de1e --- /dev/null +++ b/src/nimbus/components/ui/tabs/tabs.client.ts @@ -0,0 +1,72 @@ +/** Wires <Tabs>; auto-detects manual triggers vs. synthesized-from-TabItem mode. */ + +import { mount, initTabs } from "nimbus-docs/client"; + +const TRIGGER_CLASS = + "cursor-pointer px-4 py-2 text-sm font-medium leading-6 whitespace-nowrap text-muted-foreground transition-colors hover:text-foreground aria-selected:text-primary focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-[-2px]"; + +let counter = 0; + +function initTabContainer(container: HTMLElement): () => void { + const id = `nb-tabs-${counter++}`; + const syncKey = container.dataset.nbSyncKey; + const tablist = container.querySelector<HTMLElement>("[role=tablist]"); + const indicator = container.querySelector<HTMLElement>("[data-nb-tabs-indicator]"); + + // Scope to this container so a nested <Tabs>'s triggers don't flip the + // parent into manual mode (or vice-versa), independent of mount order. + const existingTriggers = Array.from( + container.querySelectorAll("[data-nb-tabs-trigger]"), + ).filter((t) => (t as HTMLElement).closest("[data-nb-tabs]") === container); + const synthesize = existingTriggers.length === 0; + + if (synthesize && tablist) { + // Only this container's own panels — exclude a nested <Tabs>'s panels, + // whose nearest [data-nb-tabs] ancestor is the inner container. + const panels = Array.from( + container.querySelectorAll<HTMLElement>("[data-nb-tabs-content]"), + ).filter((p) => p.closest("[data-nb-tabs]") === container); + + panels.forEach((panel, i) => { + const label = panel.dataset.nbTabLabel ?? "Tab"; + const btn = document.createElement("button"); + btn.role = "tab"; + btn.type = "button"; + btn.className = TRIGGER_CLASS; + btn.textContent = label; + btn.setAttribute("data-nb-tabs-trigger", ""); + + const panelId = `${id}-panel-${i}`; + const tabId = `${id}-tab-${i}`; + btn.id = tabId; + btn.setAttribute("aria-controls", panelId); + panel.id = panelId; + panel.setAttribute("aria-labelledby", tabId); + + if (indicator) { + tablist.insertBefore(btn, indicator); + } else { + tablist.appendChild(btn); + } + }); + } + + const instance = initTabs({ + container, + tabSelector: "[data-nb-tabs-trigger]", + panelSelector: "[data-nb-tabs-content]", + boundarySelector: "[data-nb-tabs]", + indicator, + sync: syncKey ? { key: `ui-synced-tabs__${syncKey}` } : undefined, + }); + + return () => { + instance.destroy(); + // Remove synthesized triggers so re-mount doesn't double up. + if (synthesize && tablist) { + tablist.querySelectorAll("[data-nb-tabs-trigger]").forEach((b) => b.remove()); + } + }; +} + +mount("[data-nb-tabs]", initTabContainer); diff --git a/src/nimbus/components/ui/theme-toggle/ThemeToggle.astro b/src/nimbus/components/ui/theme-toggle/ThemeToggle.astro new file mode 100644 index 00000000000..1aded43b716 --- /dev/null +++ b/src/nimbus/components/ui/theme-toggle/ThemeToggle.astro @@ -0,0 +1,32 @@ +--- +/** + * ThemeToggle — light/dark switcher. Persists to localStorage; theme + * applies to `<html data-mode="dark">` via the inline script in BaseLayout + * (no FOUC). + */ +import { Icon } from "astro-icon/components"; +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"button"> {} + +const { class: className, ...attrs } = Astro.props; +--- + +<button + data-nb-theme-toggle + class={cn( + "group flex items-center justify-center w-8 h-8 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors", + className, + )} + {...attrs} + aria-label="Toggle dark mode" + data-nb-state="light" +> + <Icon name="ph:sun" class="w-[1.125rem] h-[1.125rem] hidden group-data-[nb-state=dark]:block" /> + <Icon name="ph:moon" class="w-[1.125rem] h-[1.125rem] group-data-[nb-state=dark]:hidden" /> +</button> + +<script> + import "./theme-toggle.client"; +</script> diff --git a/src/nimbus/components/ui/theme-toggle/index.ts b/src/nimbus/components/ui/theme-toggle/index.ts new file mode 100644 index 00000000000..23685f52b65 --- /dev/null +++ b/src/nimbus/components/ui/theme-toggle/index.ts @@ -0,0 +1 @@ +export { default as ThemeToggle } from "./ThemeToggle.astro"; diff --git a/src/nimbus/components/ui/theme-toggle/theme-toggle.client.ts b/src/nimbus/components/ui/theme-toggle/theme-toggle.client.ts new file mode 100644 index 00000000000..f9624f64f98 --- /dev/null +++ b/src/nimbus/components/ui/theme-toggle/theme-toggle.client.ts @@ -0,0 +1,31 @@ +/** + * theme-toggle.client.ts — light/dark toggle. Writes pref to localStorage + * ("ui-mode"); BaseLayout's pre-paint script owns DOM application so view + * transitions, OS changes, and cross-tab edits stay in sync. + */ + +import { mount } from "nimbus-docs/client"; + +declare global { + interface Window { + __nbApplyTheme?: () => void; + } +} + +function initThemeToggle(button: HTMLElement): () => void { + function handleClick() { + const isDark = document.documentElement.getAttribute("data-mode") === "dark"; + try { + localStorage.setItem("ui-mode", isDark ? "light" : "dark"); + } catch { + // Ignore storage errors (private mode / restricted contexts). + } + window.__nbApplyTheme?.(); + } + + window.__nbApplyTheme?.(); + button.addEventListener("click", handleClick); + return () => button.removeEventListener("click", handleClick); +} + +mount("[data-nb-theme-toggle]", initThemeToggle); diff --git a/src/nimbus/components/ui/toc/TOC.astro b/src/nimbus/components/ui/toc/TOC.astro new file mode 100644 index 00000000000..084dfd783be --- /dev/null +++ b/src/nimbus/components/ui/toc/TOC.astro @@ -0,0 +1,95 @@ +--- +/** + * TOC — on-this-page nav with scroll-spy. Static rail rendered inline + * (border-l + S-curve SVGs at indent changes); animated active indicator + * lives in toc.client.ts. + */ +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; +import type { TOCItem } from "nimbus-docs/types"; + +interface Props extends HTMLAttributes<"div"> { + headings: TOCItem[]; +} + +const { headings, class: className, ...attrs } = Astro.props; +--- + +{headings.length > 0 && ( + <div data-nb-toc class={cn("toc-container", className)} {...attrs}> + <h2 class="mb-2 text-sm font-semibold text-foreground">On this page</h2> + <nav aria-label="Table of contents" class="relative"> + <svg + data-nb-toc-rail + aria-hidden="true" + class="pointer-events-none absolute inset-0 h-full w-full overflow-visible" + fill="none" + > + <path + data-nb-toc-rail-active + class="stroke-primary opacity-0 transition-[stroke-dasharray,stroke-dashoffset,opacity] duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] data-[ready=true]:opacity-100 data-[initial=true]:transition-opacity motion-reduce:transition-opacity" + stroke-width="2" + stroke-linecap="round" + /> + </svg> + + <ul class="flex list-none flex-col m-0 p-0"> + {headings.map((h, i) => { + const prevDepth = i > 0 ? headings[i - 1].depth : h.depth; + const nextDepth = i < headings.length - 1 ? headings[i + 1].depth : h.depth; + const indent = h.depth - 2; + const goingDeeper = h.depth > prevDepth; + const goingShallower = nextDepth < h.depth; + + return ( + <li> + {goingDeeper && ( + <svg + aria-hidden="true" + class="block h-2 text-border" + style={`margin-left: calc(${prevDepth - 2}rem - 0.0625rem); width: ${h.depth - prevDepth}rem;`} + viewBox="0 0 1 1" + preserveAspectRatio="none" + fill="none" + stroke="currentColor" + stroke-width="2" + overflow="visible" + > + <path d="M 0 0 C 0 0.5, 1 0.5, 1 1" vector-effect="non-scaling-stroke" /> + </svg> + )} + <a + href={`#${h.slug}`} + data-nb-toc-link + data-nb-slug={h.slug} + class="block border-l-2 border-border pl-5 py-1.5 text-[0.8125rem] leading-snug text-muted-foreground no-underline transition-colors duration-150 hover:border-foreground/20 hover:text-foreground aria-[current=true]:font-medium aria-[current=true]:text-foreground" + style={`margin-left: calc(${indent}rem - 0.125rem);`} + > + {h.text} + </a> + {goingShallower && ( + <svg + aria-hidden="true" + class="block h-2 text-border" + style={`margin-left: calc(${nextDepth - 2}rem - 0.0625rem); width: ${h.depth - nextDepth}rem;`} + viewBox="0 0 1 1" + preserveAspectRatio="none" + fill="none" + stroke="currentColor" + stroke-width="2" + overflow="visible" + > + <path d="M 1 0 C 1 0.5, 0 0.5, 0 1" vector-effect="non-scaling-stroke" /> + </svg> + )} + </li> + ); + })} + </ul> + </nav> + </div> +)} + +<script> + import "./toc.client"; +</script> diff --git a/src/nimbus/components/ui/toc/index.ts b/src/nimbus/components/ui/toc/index.ts new file mode 100644 index 00000000000..8df10555a8d --- /dev/null +++ b/src/nimbus/components/ui/toc/index.ts @@ -0,0 +1 @@ +export { default as TOC } from "./TOC.astro"; diff --git a/src/nimbus/components/ui/toc/toc.client.ts b/src/nimbus/components/ui/toc/toc.client.ts new file mode 100644 index 00000000000..e146d662d9a --- /dev/null +++ b/src/nimbus/components/ui/toc/toc.client.ts @@ -0,0 +1,178 @@ +/** + * Scroll-spy + animated rail indicator. Dash slides via arc-length + * so it weaves through curves cleanly instead of cutting across. + */ + +import { mount } from "nimbus-docs/client"; + +const SCROLL_OFFSET = 100; + +function initToc(root: HTMLElement): () => void { + const nav = root.querySelector<HTMLElement>("nav"); + const activePath = root.querySelector<SVGPathElement>("[data-nb-toc-rail-active]"); + const links = root.querySelectorAll<HTMLElement>("[data-nb-toc-link]"); + if (!nav || !activePath || links.length === 0) return () => {}; + + const slugs = Array.from(links).map((l) => l.dataset.nbSlug!); + const headingEls = slugs + .map((s) => document.getElementById(s)) + .filter(Boolean) as HTMLElement[]; + if (headingEls.length === 0) return () => {}; + + // Per-link arc-length along the rail path. Computed from real DOM + // measurements so the path is always pixel-perfect over the static + // gray rail (border-lefts + curve SVGs). + let segments: { start: number; length: number }[] = []; + let totalLength = 0; + let currentIndex = -1; + let hasApplied = false; + + function buildRail() { + const navRect = nav!.getBoundingClientRect(); + + // Rail centerline x for each link: link.left + 1 (center of the 2px + // border-left stroke). yTop / yBot bracket the link's vertical extent. + const m = Array.from(links).map((link) => { + const r = link.getBoundingClientRect(); + return { + x: r.left - navRect.left + 1, + yTop: r.top - navRect.top, + yBot: r.top - navRect.top + r.height, + }; + }); + + // Build the path incrementally so we can measure per-link arc lengths + // via getTotalLength() at each step. + let d = ""; + const newSegments: { start: number; length: number }[] = []; + + for (let i = 0; i < m.length; i++) { + const cur = m[i]; + + // Connector from previous link's bottom to this link's top. + if (i === 0) { + d += `M ${cur.x} ${cur.yTop} `; + } else { + const prev = m[i - 1]; + if (Math.abs(cur.x - prev.x) < 0.5) { + // Same indent → straight line through the gap. + d += `L ${cur.x} ${cur.yTop} `; + } else { + // Different indent → smooth S-curve matching the static curve + // SVGs already in the gap (M 0 0 C 0 0.5, 1 0.5, 1 1 stretched + // to the gap rectangle). + const midY = (prev.yBot + cur.yTop) / 2; + d += `C ${prev.x} ${midY}, ${cur.x} ${midY}, ${cur.x} ${cur.yTop} `; + } + } + + // The link's own vertical segment. + activePath!.setAttribute("d", d); + const start = activePath!.getTotalLength(); + + d += `L ${cur.x} ${cur.yBot} `; + activePath!.setAttribute("d", d); + const end = activePath!.getTotalLength(); + + newSegments.push({ start, length: end - start }); + } + + segments = newSegments; + totalLength = activePath!.getTotalLength(); + } + + function applyActive(index: number, instant: boolean) { + const seg = segments[index]; + if (!seg) return; + + if (instant) { + activePath!.setAttribute("data-initial", "true"); + // Force style recalc so transition-opacity-only takes effect before + // we write the new dash values (otherwise the dash transitions on + // first paint, producing a visible sweep). + void activePath!.getBoundingClientRect(); + } + + // A single dash of length seg.length, followed by a gap longer than + // the whole path so nothing else is drawn. Shift its starting point + // along the path with stroke-dashoffset. Both transition smoothly, + // so the highlight slides along arc length — through curves and all. + activePath!.style.strokeDasharray = `${seg.length} ${totalLength + 1}`; + activePath!.style.strokeDashoffset = `${-seg.start}`; + + if (instant) { + requestAnimationFrame(() => { + activePath!.setAttribute("data-ready", "true"); + requestAnimationFrame(() => { + activePath!.removeAttribute("data-initial"); + }); + }); + } + } + + function setActive(index: number) { + if (index === currentIndex) return; + currentIndex = index; + + links.forEach((l) => l.removeAttribute("aria-current")); + root + .querySelector(`[data-nb-toc-link][data-nb-slug="${slugs[index]}"]`) + ?.setAttribute("aria-current", "true"); + + applyActive(index, !hasApplied); + hasApplied = true; + } + + function updateActive() { + // Walk headings in document order. The last one whose top is at or + // above the offset is the "current" section the reader is in. + let activeIndex = 0; + for (let i = 0; i < headingEls.length; i++) { + if (headingEls[i].getBoundingClientRect().top <= SCROLL_OFFSET) { + activeIndex = i; + } + } + setActive(activeIndex); + } + + let ticking = false; + function onScroll() { + if (!ticking) { + requestAnimationFrame(() => { + updateActive(); + ticking = false; + }); + ticking = true; + } + } + + function onLayoutChange() { + // Rebuild the path geometry and snap (no animation) to the current + // active segment — the user isn't navigating, layout just shifted. + buildRail(); + if (currentIndex >= 0) applyActive(currentIndex, true); + } + + const controller = new AbortController(); + window.addEventListener("scroll", onScroll, { + passive: true, + signal: controller.signal, + }); + window.addEventListener("resize", onLayoutChange, { + passive: true, + signal: controller.signal, + }); + + const ro = new ResizeObserver(onLayoutChange); + ro.observe(nav); + + buildRail(); + updateActive(); + + return () => { + controller.abort(); + ro.disconnect(); + }; +} + +mount("[data-nb-toc]", initToc); diff --git a/src/nimbus/content.config.ts b/src/nimbus/content.config.ts new file mode 100644 index 00000000000..808e859d473 --- /dev/null +++ b/src/nimbus/content.config.ts @@ -0,0 +1,420 @@ +import { defineCollection, reference, z } from "astro:content"; +import { glob } from "astro/loaders"; +import { docsCollection, partialsCollection } from "nimbus-docs/content"; +import { warpReleasesSchema } from "~/schemas/warp-releases"; +import { compatibilityFlagsSchema } from "~/schemas/compatibility-flags"; +import { fieldsSchema } from "~/schemas/fields"; +// Remote (middlecache) data collections — no local content files exist, so we +// reuse the shared collection configs in place (which carry the middlecache +// loader + schema) rather than duplicating the fetcher on the Nimbus side. +import { productAvailabilityCollectionConfig } from "~/content/collections/product-availability"; +import { granularControlApplicationsCollectionConfig } from "~/content/collections/granular-control-applications"; + +// Extend the default docs schema with the CF-specific frontmatter keys the +// content uses. The schema is permissive — these fields just need to validate +// so content is ingested unmodified; add new keys here as the build surfaces +// them. +// +// CF frontmatter keys: +// - pcx_content_type string enum (CF taxonomy: concept/reference/tutorial/...) +// - products array of product slugs (sometimes empty list) +// - reviewed ISO date string (last human review) +// - order top-level sort key; Nimbus reads sidebar.order +// instead, so this is pass-through only +// - results tutorial result-list shape; permissive z.any() until +// there's a reason to validate it +// - hideChildren Starlight shorthand for `sidebar.hideChildren`. +// Passed through so content validates; Nimbus only +// reads the nested `sidebar.hideChildren` form. +export const collections = { + docs: defineCollection( + docsCollection({ + // CF content carries ~40+ framework-agnostic frontmatter keys (Starlight + // + CF taxonomy). Accept them as-is; keys Nimbus acts on are typed below. + strictFrontmatter: false, + schemaFields: { + // Nimbus docs are agent-friendly by default. Set `audience: human` + // to flag a page that's written primarily for human readers. + audience: z.literal("human").optional(), + + // Opt a page into the wide content column (forwarded to DocsLayout by + // `[...slug].astro`). Used by the model catalog content pages. + wide: z.boolean().optional(), + + // --- CF frontmatter passthrough --------------------- + // Schema is intentionally permissive — the framework doesn't act + // on these fields, it just needs them to validate so the content + // can be ingested unmodified. Nav/sidebar semantics still come + // from Nimbus's own keys (`sidebar.order`, `sidebar.hideChildren`). + pcx_content_type: z.string().optional(), + content_type: z.string().optional(), + products: z.array(z.string()).optional(), + reviewed: z.union([z.string(), z.date()]).optional(), + order: z.number().optional(), + results: z.any().optional(), + difficulty: z.string().optional(), + summary: z.string().optional(), + tags: z.array(z.string()).nullable().optional(), + release_notes_file_name: z.array(z.string()).nullable().optional(), + hideChildren: z.boolean().optional(), + // CF attaches a commit-feed `.atom` URL to some pages (e.g. the Pages + // build-image + compatibility-flags pages). Pass-through only — Nimbus + // doesn't act on it. + rss: z.string().optional(), + + // --- D10 frontmatter shim (learning-paths Phase B) ------------------- + // Five upstream pages carry frontmatter the strict nimbus schema + // rejects; rather than edit byte-identical content we re-declare the + // keys here. `defineDocSchema` does `baseDocSchema().extend(fields)` + // and Zod `.extend()` OVERWRITES same-named base keys, and + // `withStrictKeys` reads `shape` AFTER the extend — so re-declaring + // `prev`/`next` widens the accepted shape with no nimbus-docs change. + // + // - `weight: null` on + // replace-vpn/connect-private-network/overlapping-ips.mdx — an + // unknown top-level key; `z.any().optional()` tolerates it + // (pass-through; nimbus reads `sidebar.order`, never `weight`). + // - `prev: true` / `next: true` on the 4 + // sase-overview-course/series/*.mdx pages. nimbus's prevNextSchema + // accepts string | {link,label} | false but NOT `true` + // (Starlight treats `true` = "auto label"). A literal `true` + // routes into resolveOverride's object branch and THROWS for + // first/last-in-rail pages, so we map `true -> undefined` + // (== Starlight "auto", the safe no-op) instead of passing it + // through. (Upstream nimbus gap: prevNextSchema lacks `true`.) + weight: z.any().optional(), + prev: z + .union([ + z.string(), + z.object({ link: z.string().optional(), label: z.string().optional() }), + z.literal(false), + z.literal(true).transform(() => undefined), + ]) + .optional(), + next: z + .union([ + z.string(), + z.object({ link: z.string().optional(), label: z.string().optional() }), + z.literal(false), + z.literal(true).transform(() => undefined), + ]) + .optional(), + }, + }), + ), + // CF glossary data. Read by the Glossary table and GlossaryTooltip components. + glossary: defineCollection({ + loader: glob({ pattern: "*.yaml", base: "./src/content/glossary" }), + schema: z.object({ + productName: z.string(), + entries: z.array( + z.object({ + term: z.string(), + general_definition: z.string(), + associated_products: z.array(z.string()).optional(), + }), + ), + }), + }), + // CF release-notes data. Read by ProductReleaseNotes on the changelog page. + "release-notes": defineCollection({ + loader: glob({ pattern: "*.yaml", base: "./src/content/release-notes" }), + schema: z.object({ + link: z.string(), + productName: z.string(), + productLink: z.string(), + entries: z.array( + z.object({ + publish_date: z.string(), + title: z.string().optional(), + description: z.string().optional(), + individual_page: z.boolean().optional(), + link: z.string().optional(), + scheduled: z.boolean().optional(), + scheduled_date: z.string().optional(), + }), + ), + }), + }), + // CF unified changelog — one MD/MDX entry per post under + // `src/content/changelog/<product>/<YYYY-MM-DD>-slug.mdx`. The parent + // folder names the product (must match a `directory` id); `products` may + // add more. Read by getChangelogs (`~/util/changelog`) and the + // `/changelog/*` pages + RSS feeds. + changelog: defineCollection({ + loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/changelog" }), + schema: z.object({ + title: z.string(), + description: z.string(), + date: z.coerce.date(), + // If false (default), a future-dated entry stays hidden until its date. + publish_future_dated_entry: z.boolean().default(false), + // Directory entries this post is attributed to. The folder product is + // auto-added by getChangelogs, so it may be omitted here. + products: z.array(reference("directory")).default([]), + // Hidden entries are excluded from /changelog/ and the RSS feeds. + hidden: z.boolean().default(false), + }), + }), + // CF product directory — product metadata + group membership. Id is the + // filename (e.g. `queues`). Used by the changelog system to resolve + // product names/links and to build the per-product / per-group views. + // Schema is permissive (passthrough); only the fields below are read. + directory: defineCollection({ + loader: glob({ + pattern: "**/*.{json,yml,yaml}", + base: "./src/content/directory", + generateId: ({ entry }) => entry.replace(/\.(json|ya?ml)$/, ""), + }), + schema: z + .object({ + id: z.string().optional(), + // CF's directory collection is schemaless; non-product entries (e.g. + // home.yaml) omit `name`/`entry`. Keep these optional to match. + name: z.string().optional(), + entry: z + .object({ + title: z.string().optional(), + url: z.string().optional(), + group: z.string().optional(), + additional_groups: z.array(z.string()).optional(), + }) + .passthrough() + .optional(), + // Product page metadata. Read by DirectoryCatalog (`/directory`) + // for the per-card description blurb. + meta: z + .object({ + title: z.string().optional(), + description: z.string().optional(), + }) + .passthrough() + .optional(), + }) + .passthrough(), + }), + // CF learning-paths data — one JSON per path under + // `src/content/learning-paths/<module>.json`. Id is the filename (e.g. + // `workers`), which is also the path's `<module>` URL segment. Read by + // ResourcesBySelector (aggregated into /resources) + the per-path sidebar + // title lookup. `products` are `directory` references (resolved to names). + "learning-paths": defineCollection({ + loader: glob({ + pattern: "**/*.{json,yml,yaml}", + base: "./src/content/learning-paths", + }), + schema: z + .object({ + title: z.string(), + uid: z.string().optional(), + path: z.string(), + description: z.string(), + pcx_content_type: z.string().default("learning-path"), + products: z.array(reference("directory")).default([]), + tags: z.string().array().optional(), + reviewed: z.coerce.date().optional(), + }) + .strict(), + }), + // CF plans data (single index.json). Read by FeatureTable + ProductFeatures + // via getEntry("plans", "index"). Untyped — the shape is a deep nested + // object addressed by dot-path id, so the schema is left permissive. + plans: defineCollection({ + loader: glob({ pattern: "*.json", base: "./src/content/plans" }), + }), + // CF Pages framework presets (single index.yaml). Read by PagesBuildPreset. + "pages-framework-presets": defineCollection({ + loader: glob({ pattern: "*.yaml", base: "./src/content/pages-framework-presets" }), + schema: z.object({ build_configs: z.record(z.string(), z.any()) }), + }), + // CF product availability + granular control application data, fetched from + // middlecache at build. Read by ProductAvailabilityText / + // GranularControlApplicationsList. + "product-availability": defineCollection(productAvailabilityCollectionConfig), + "granular-control-applications": defineCollection( + granularControlApplicationsCollectionConfig, + ), + // CF notification catalog (single index.yaml). Read by AvailableNotifications. + notifications: defineCollection({ + loader: glob({ pattern: "*.yaml", base: "./src/content/notifications" }), + schema: z.object({ entries: z.array(z.any()) }), + }), + // CF Pages build-image versions (v1/v2/v3.yaml). Read by the + // PagesBuildEnvironment* components. + "pages-build-environment": defineCollection({ + loader: glob({ pattern: "*.yaml", base: "./src/content/pages-build-environment" }), + schema: z + .object({ + build_environment: z + .object({ operating_system: z.string(), architecture: z.string() }) + .optional(), + languages: z.array(z.any()).default([]), + tools: z.array(z.any()).default([]), + }) + .passthrough(), + }), + // CF Stream video metadata (one <slug>/index.yaml per video). Read by the + // Stream component's `file` variant. Id is the directory slug. `products` + // (directory references) is left as passthrough — not resolved here. + stream: defineCollection({ + loader: glob({ + pattern: "**/*.{yaml,yml}", + base: "./src/content/stream", + generateId: ({ entry }) => entry.replace(/\/index\.(ya?ml)$/, ""), + }), + schema: z + .object({ + id: z.string(), + title: z.string(), + url: z.string().optional(), + description: z.string().optional(), + chapters: z.record(z.string(), z.string()).optional(), + thumbnail: z + .object({ url: z.string() }) + .or(z.object({ timestamp: z.string() })) + .optional(), + }) + .passthrough(), + }), + // CF WARP client releases — one YAML per release under + // `src/content/warp-releases/<os>/<track>/<version>.yaml`. The glob id is the + // path-minus-extension (e.g. `macos/ga/2026.1.150.0`), which WARPReleases + // filters on via `id.startsWith("<os>/<track>")`. Read by the WARPReleases / + // WARPRelease components on the WARP download page. + "warp-releases": defineCollection({ + loader: glob({ pattern: "**/*.{json,yml,yaml}", base: "./src/content/warp-releases" }), + schema: warpReleasesSchema, + }), + // CF Workers runtime compatibility flags — one markdown file per flag under + // `src/content/compatibility-flags/<flag>.md` (frontmatter + a body note). + // Not route-generating: the whole set renders as per-flag tables + notes on a + // single page via the CompatibilityFlags component (the 3a data-feeds-a- + // component pattern). The upstream `_build` frontmatter directives are + // non-canonical here and stripped by the (non-strict) zod schema. + "compatibility-flags": defineCollection({ + loader: glob({ pattern: "*.md", base: "./src/content/compatibility-flags" }), + schema: compatibilityFlagsSchema, + }), + // CF rules-language field catalog (single index.yaml). Read by FieldCatalog + // on /ruleset-engine/rules-language/fields/reference/ via getEntry. + fields: defineCollection({ + loader: glob({ + pattern: "**/*.{json,yml,yaml}", + base: "./src/content/fields", + }), + schema: fieldsSchema, + }), + partials: defineCollection( + partialsCollection({ + // CF partials use `inputParameters` (semicolon-list of param tokens). + // Permissive passthrough — Nimbus's Render reads `params` for + // validation; this key is just carried through. + schemaFields: { + inputParameters: z.string().optional(), + }, + }), + ), + // Workers AI model catalog — one JSON per model. Id is the filename + // (e.g. `llama-3.1-8b-instruct-fast`), which is also the per-model URL slug. + // Schema is permissive (passthrough) so the files validate as-is; only the + // fields the catalog/per-model pages read are declared. The big + // `schema.input`/`output` JSON-Schema blob is carried through untyped. + "workers-ai-models": defineCollection({ + loader: glob({ + pattern: "*.json", + base: "./src/content/workers-ai-models", + generateId: ({ entry }) => entry.replace(/\.json$/, ""), + }), + schema: z + .object({ + id: z.string(), + name: z.string(), + description: z.string(), + source: z.number().optional(), + task: z + .object({ + id: z.string().optional(), + name: z.string(), + description: z.string().optional(), + }) + .passthrough(), + tags: z.array(z.string()).default([]), + properties: z + .array( + z + .object({ + property_id: z.string(), + value: z.any(), + }) + .passthrough(), + ) + .default([]), + schema: z.any().optional(), + }) + .passthrough(), + }), + // Unified AI model catalog — one JSON per model. Id is the filename + // (e.g. `openai-tts-1`); the per-model URL slug is the `model_id` + // (e.g. `openai/tts-1`), NOT the filename. The resolver-read fields are + // declared (so JSON validates and reads never go `undefined`) and the object + // is `.passthrough()` so heavy/extra fields (full examples, code_snippets, + // raw_response, etc.) carry through untyped. + "catalog-models": defineCollection({ + loader: glob({ + pattern: "*.json", + base: "./src/content/catalog-models", + generateId: ({ entry }) => entry.replace(/\.json$/, ""), + }), + schema: z + .object({ + // Identification + model_id: z.string(), + provider_id: z.string().nullable(), + name: z.string(), + + // Content + description: z.string(), + task: z.string(), + tags: z.string().array(), + + // Capabilities + context_length: z.number().nullable(), + max_output_tokens: z.number().nullable(), + supports_async: z.boolean(), + + // Zero Data Retention (optional — older API rows omit it). + zdr: z.boolean().optional(), + zdr_comment: z.string().nullable().optional(), + + // In-page notice + request formats (optional/nullable). + banner: z.any().nullable().optional(), + request_formats: z.string().array().nullable().optional(), + + // Examples + snippets (required `examples`, optional rest). + examples: z.array(z.any()), + default_example: z.any().nullable().optional(), + code_snippets: z.array(z.any()).optional(), + + // JSON-Schema blob (input/output), carried through untyped. + schema: z + .object({ + input: z.record(z.string(), z.unknown()).optional(), + output: z.record(z.string(), z.unknown()).optional(), + }) + .optional(), + + // Metadata & links + metadata: z.record(z.string(), z.unknown()), + external_info: z.string().nullable(), + terms: z.string().nullable(), + cover_image_url: z.string().nullable(), + schema_version: z.string().nullable(), + private: z.boolean().optional(), + + // Timestamps + created_at: z.string().optional(), + updated_at: z.string().optional(), + }) + .passthrough(), + }), +}; diff --git a/src/nimbus/layouts/BaseLayout.astro b/src/nimbus/layouts/BaseLayout.astro new file mode 100644 index 00000000000..1bf42ba9dce --- /dev/null +++ b/src/nimbus/layouts/BaseLayout.astro @@ -0,0 +1,124 @@ +--- +import "@fontsource-variable/inter"; +import "@fontsource-variable/jetbrains-mono"; +import "../styles/globals.css"; +import "../styles/prose.css"; +import "../styles/markdown-pipeline.css"; +import { ClientRouter } from "astro:transitions"; +import { config } from "virtual:nimbus/config"; +import { getCollectionLlmsUrl, getVersionStatus } from "nimbus-docs"; +import AgentDirective from "@/components/AgentDirective.astro"; +import { SearchDialog } from "@/components/ui/search"; +import NimbusHead from "nimbus-docs/components/NimbusHead.astro"; +import type { BasePageProps } from "nimbus-docs/types"; + +type Props = BasePageProps; + +const { + title, + description, + noindex, + markdownUrl, + socialImage, + lastUpdated, + head: pageHead = [], + collection, + entryId, +} = Astro.props; + +const lang = config.locale ?? "en"; +// Per-page agent-index pointer. For pages in non-primary or version +// collections, this resolves to `/<prefix>/llms.txt` (e.g. `/v0/llms.txt` +// for a v0 docs page, `/blog/llms.txt` for a blog post). Falls back to +// the root `/llms.txt` when no collection is provided. +// Hidden-version pages don't advertise an agent index (the per-version +// llms.txt isn't emitted for them); suppress the AgentDirective entirely. +const versionStatus = collection ? await getVersionStatus(collection) : null; +const isHiddenVersion = versionStatus?.isHidden === true; +const llmsIndexPath = collection + ? await getCollectionLlmsUrl(collection) + : "/llms.txt"; +const llmsUrl = Astro.site + ? new URL(llmsIndexPath, Astro.site).href + : llmsIndexPath; +--- + +<!doctype html> +<html lang={lang}> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + + <script is:inline> + (() => { + const KEY = "ui-mode"; + const media = window.matchMedia("(prefers-color-scheme: dark)"); + + const readPref = () => { + try { + const v = localStorage.getItem(KEY); + return v === "dark" || v === "light" ? v : null; + } catch { + return null; + } + }; + + const resolveMode = () => readPref() ?? (media.matches ? "dark" : "light"); + + const applyTheme = () => { + const root = document.documentElement; + const mode = resolveMode(); + if (mode === "dark") root.setAttribute("data-mode", "dark"); + else root.removeAttribute("data-mode"); + root.style.colorScheme = mode; + document.querySelectorAll("[data-nb-theme-toggle]").forEach((b) => { + b.setAttribute("data-nb-state", mode); + }); + }; + + applyTheme(); + document.addEventListener("astro:after-swap", applyTheme); + media.addEventListener("change", () => { + if (!readPref()) applyTheme(); + }); + window.addEventListener("storage", (e) => { + if (e.key === KEY) applyTheme(); + }); + + window.__nbApplyTheme = applyTheme; + })(); + </script> + + <NimbusHead + title={title} + description={description} + noindex={noindex} + markdownUrl={markdownUrl} + socialImage={socialImage} + lastUpdated={lastUpdated} + head={pageHead} + collection={collection} + entryId={entryId} + /> + + {/* SPA-like navigation. Page-level animation in globals.css. */} + <ClientRouter /> + </head> + <body class="min-h-screen bg-background text-foreground antialiased"> + <a + href="#main-content" + class="sr-only focus:not-sr-only focus:absolute focus:left-2 focus:top-2 focus:z-100 focus:rounded-md focus:bg-primary focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-primary-foreground focus:no-underline focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2" + > + Skip to content + </a> + {markdownUrl && !isHiddenVersion && <AgentDirective markdownUrl={markdownUrl} llmsUrl={llmsUrl} />} + <slot /> + {config.search !== false && <SearchDialog />} + + <script> + import { codeCopy } from "nimbus-docs/client"; + import "../scripts/mermaid.client"; + codeCopy(); + </script> + </body> +</html> diff --git a/src/nimbus/layouts/ChangelogLayout.astro b/src/nimbus/layouts/ChangelogLayout.astro new file mode 100644 index 00000000000..e351a549e3a --- /dev/null +++ b/src/nimbus/layouts/ChangelogLayout.astro @@ -0,0 +1,138 @@ +--- +/** + * ChangelogLayout — single-column chrome for the changelog feed and its + * per-entry permalink pages. A changelog is a feed, not a doc tree: no sidebar, + * no TOC. Just the header and a centered reading column. + * + * The column sits on an asymmetric blueprint backdrop: the LEFT margin is a + * single dashed rule that the timeline co-opts as its spine (ChangelogFeed + * draws a solid overlay + nodes on the exact same axis); the RIGHT margin keeps + * the full decorative treatment (inner rule + dot-grid gutter + outer rule). + * Both the backdrop lines and the column derive from one width var (--nb-cl-col) + * so the spine and the feed's nodes always share the column's left edge. + * + * Design: Nimbus changelog feature (monorepo registry/features/changelog.md, 5a). + */ +import BaseLayout from "./BaseLayout.astro"; +import Header from "@/components/Header.astro"; +import type { BasePageProps } from "nimbus-docs/types"; + +type Props = BasePageProps; + +const baseProps = Astro.props; +--- + +<BaseLayout {...baseProps}> + <div class="flex min-h-screen flex-col"> + <Header showSidebar={false} /> + <main id="main-content" transition:name="nb-content" class="flex-1"> + <div class="nb-cl-shell"> + <div class="nb-cl-backdrop" aria-hidden="true"> + {/* Left: just the spine line (co-opted by the timeline). */} + <span class="nb-cl-rule nb-cl-rule-left"></span> + {/* Right: inner rule + dot-grid gutter + outer rule. */} + <span class="nb-cl-rule nb-cl-rule-right"></span> + <span class="nb-cl-dots"></span> + <span class="nb-cl-rule nb-cl-rule-outer"></span> + </div> + <div class="nb-cl-col"> + <slot /> + </div> + </div> + </main> + </div> +</BaseLayout> + +<style> + .nb-cl-shell { + position: relative; + /* Reading column width; the backdrop's left rule (the spine) sits on the + column's left edge (= the feed's node axis). */ + --nb-cl-col: min(768px, calc(100vw - 3rem)); + /* Decorative outer bound — half of the directory's 1360px envelope, so the + outer rule lines up across both pages even though the reading column is + narrower. The dot-grid fills from the column edge out to this bound. */ + --nb-cl-outer: 680px; + } + + .nb-cl-col { + position: relative; + z-index: 1; + width: var(--nb-cl-col); + margin-inline: auto; + padding-top: 3.5rem; + padding-bottom: 8rem; + } + + /* Decorative backdrop — desktop only, purely decorative, animation-free. */ + .nb-cl-backdrop { + position: absolute; + inset: 0; + z-index: 0; + overflow: hidden; + pointer-events: none; + display: none; + } + @media (min-width: 768px) { + .nb-cl-backdrop { + display: block; + } + } + + .nb-cl-rule { + position: absolute; + top: 0; + bottom: 0; + width: 1px; + /* center the 1px rule on its x so it coincides exactly with the feed's + solid spine (also center-anchored) — no fuzzy doubled line in the feed. */ + transform: translateX(-50%); + background-image: linear-gradient( + to bottom, + var(--nb-border) 50%, + transparent 50% + ); + background-size: 1px 32px; + background-repeat: repeat-y; + } + .nb-cl-rule-left { + left: calc(50% - var(--nb-cl-col) / 2); + } + .nb-cl-rule-right { + left: calc(50% + var(--nb-cl-col) / 2); + } + .nb-cl-rule-outer { + left: calc(50% + var(--nb-cl-outer)); + } + .nb-cl-dots { + position: absolute; + top: 0; + bottom: 0; + left: calc(50% + var(--nb-cl-col) / 2); + width: calc(var(--nb-cl-outer) - var(--nb-cl-col) / 2); + background-image: radial-gradient( + var(--nb-grid-line) 0.75px, + transparent 0.75px + ); + background-size: 12px 12px; + background-position: left top; + } +</style> + +{/* These target MDX-rendered content (entry bodies marked `.nb-cl-prose`), + which has no class hooks for Tailwind utilities — so they stay global. */} +<style is:global> + .nb-cl-prose p { + text-wrap: pretty; + } + + /* Subtle 1px outline on entry images for consistent depth — pure + black/white at 10%, never a tinted neutral. */ + .nb-cl-prose img { + outline: 1px solid rgba(0, 0, 0, 0.1); + outline-offset: -1px; + } + [data-mode="dark"] .nb-cl-prose img { + outline-color: rgba(255, 255, 255, 0.1); + } +</style> diff --git a/src/nimbus/layouts/DocsLayout.astro b/src/nimbus/layouts/DocsLayout.astro new file mode 100644 index 00000000000..866a71e9d1d --- /dev/null +++ b/src/nimbus/layouts/DocsLayout.astro @@ -0,0 +1,455 @@ +--- +/** + * DocsLayout — three-column docs layout. + * + * Named slots for overrides (all optional, defaults render otherwise): + * header, sidebar, toc, page-title, content-footer, pagination + */ +import { Icon } from "astro-icon/components"; +import BaseLayout from "./BaseLayout.astro"; +import Header from "@/components/Header.astro"; +import { Banner } from "@/components/ui/banner"; +import { Sidebar, SidebarFilter, SidebarProductNav } from "@/components/ui/sidebar"; +import { TOC } from "@/components/ui/toc"; +import { Breadcrumbs } from "@/components/ui/breadcrumbs"; +import { Pagination } from "@/components/ui/pagination"; +import { PageActions } from "@/components/ui/page-actions"; +import { Badge } from "@/components/ui/badge"; +import type { DocsPageProps } from "nimbus-docs/types"; +import { config } from "virtual:nimbus/config"; +import { getVersionStatus, getVersionAlternates, getSectionTitle } from "nimbus-docs"; +import { sectionTitleResolver } from "@/util/sidebar"; + +type Props = DocsPageProps & { audience?: "human"; wide?: boolean }; + +const { title, description, sidebar, headings, breadcrumbs, prevNext, mode = "doc", banner, head = [], searchable, noindex, markdownUrl, socialImage, lastUpdated, editUrl, draft, audience, collection, entryId, wide = false } = Astro.props; + +// `searchable` derives from `noindex` when omitted: a non-crawlable page is +// by default not in the site search either. Explicit `searchable: true` +// overrides for the edge case of "no search engines, yes internal search." +const effectiveSearchable = searchable ?? !noindex; +const isCustom = mode === "custom"; + +// `sidebar: false` / `tableOfContents: false` in frontmatter come through +// from the route as literal `false` (not coerced to `[]`). They drive +// per-column suppression — rail, mobile dialog, header menu button. +const showSidebar = sidebar !== false; +const showToc = headings !== false; + +// Versioning P3: per-page status drives Pagefind faceting, deprecation +// banner, and the data-pagefind-ignore exclusion for hidden versions. +const versionStatus = collection ? await getVersionStatus(collection) : null; +const versionAlternates = collection && entryId + ? await getVersionAlternates(collection, entryId) + : null; +const currentSiblingUrl = versionAlternates?.canonical?.url ?? null; + +// Page-body attribute resolution: +// - If the user opted out via `searchable: false` (or `noindex: true` +// with no explicit override) → ignore. +// - If the page is in a hidden version → ignore unconditionally +// (hidden takes precedence; you can't index hidden content even +// if the page frontmatter says searchable=true). +// - Otherwise → mark as a Pagefind body, plus emit per-version and +// per-status filters so the search UI can scope by them. +const pagefindAttrs: Record<string, string> = {}; +if (!effectiveSearchable || versionStatus?.isHidden) { + pagefindAttrs["data-pagefind-ignore"] = ""; +} else { + pagefindAttrs["data-pagefind-body"] = ""; + if (versionStatus) { + pagefindAttrs["data-pagefind-filter"] = `version:${versionStatus.version}`; + if (versionStatus.isDeprecated) { + // Pagefind supports multiple filter attributes on the same element + // via `data-pagefind-filter` repeated as a comma-separated list in + // the value. The status filter pairs with the version filter so a + // search UI can offer "exclude deprecated" as a default toggle. + pagefindAttrs["data-pagefind-filter"] += `, status:deprecated`; + } + } +} + +// Sidebar header above the filter — an "all products" button plus the +// current product title. The rail title can diverge from the breadcrumb +// (learning paths suffix the module title with "(Learning Paths)"), so it +// comes from `getSectionTitle`; the breadcrumb labels come from the nav tree. +const productSlug = Astro.url.pathname.split("/").filter(Boolean)[0] ?? null; +const sectionTitle = await getSectionTitle(Astro.url.pathname, sectionTitleResolver); +// Link the product (seg0) crumb to the product root when the tree leaves it +// non-interactive (a section landing with no resolvable page entry). +const breadcrumbItems = (breadcrumbs ?? []).map((c, i) => + i === 1 && !c.href && productSlug ? { ...c, href: `/${productSlug}/` } : c, +); +const product = productSlug + ? { + title: sectionTitle?.rail ?? breadcrumbItems[1]?.label ?? productSlug, + href: `/${productSlug}/`, + } + : null; + +const hasHeader = Astro.slots.has("header"); +const hasSidebar = Astro.slots.has("sidebar"); +const hasToc = Astro.slots.has("toc"); +const hasPageTitle = Astro.slots.has("page-title"); +const hasContentFooter = Astro.slots.has("content-footer"); +const hasPagination = Astro.slots.has("pagination"); +--- + +<BaseLayout title={title} description={description} noindex={noindex || draft} markdownUrl={markdownUrl} socialImage={socialImage} lastUpdated={lastUpdated} head={head} collection={collection} entryId={entryId}> + <div class="flex flex-col min-h-screen"> + {hasHeader ? <slot name="header" /> : <Header collection={collection} entryId={entryId} showSidebar={showSidebar} />} + + {isCustom ? ( + <main id="main-content" transition:name="nb-content" class="flex-1"> + <slot /> + </main> + ) : ( + <div class="flex-1 mx-auto w-full"> + <div class="flex"> + {showSidebar && ( + <aside id="desktop-sidebar" data-nb-desktop-sidebar class="hidden lg:flex w-(--nb-sidebar-width) shrink-0 border-r border-border sticky top-14 h-[calc(100vh-3.5rem)] flex-col overflow-y-auto relative"> + {hasSidebar ? ( + <div data-sidebar-home class="flex-1"> + <div data-shared-sidebar-nav class="flex-1 pb-12 max-lg:pb-8 max-lg:pt-5"> + <slot name="sidebar" /> + </div> + </div> + ) : ( + <div data-sidebar-home class="flex-1"> + <nav data-shared-sidebar-nav class="flex-1 pb-12 max-lg:pb-8 max-lg:pt-5"> + <div class="z-10 bg-background px-4 pb-3 lg:sticky lg:top-0"> + {product && <SidebarProductNav title={product.title} href={product.href} class="py-2.5" />} + <SidebarFilter class="mb-0" /> + </div> + <div class="px-4 pt-2"> + <Sidebar items={sidebar} persist /> + </div> + </nav> + </div> + )} + <div class="pointer-events-none sticky bottom-0 h-8 bg-gradient-to-t from-base to-transparent" /> + </aside> + )} + + {/* Sidebar state restore — pre-sets data attributes before the + disclosure module loads so collapsed/scrolled state survives nav. + Only emit when the sidebar is rendered; otherwise the script is + dead code (the `desktop-sidebar` element doesn't exist). */} + {showSidebar && ( + <script is:inline aria-hidden="true"> + (function () { + try { + if (!matchMedia("(min-width: 64rem)").matches) return; + const sidebar = document.getElementById("desktop-sidebar"); + if (!sidebar) return; + const content = sidebar.querySelector("[data-nb-sidebar]"); + if (!content) return; + + function setGroupOpen(group, open) { + group.setAttribute("data-nb-default-open", open ? "true" : "false"); + const trigger = group.querySelector("[data-nb-collapsible-trigger]"); + const panel = group.querySelector("[data-nb-collapsible-content]"); + const state = open ? "open" : "closed"; + if (trigger) { + trigger.setAttribute("data-nb-state", state); + trigger.setAttribute("aria-expanded", String(open)); + } + if (panel) panel.setAttribute("data-nb-state", state); + } + + const raw = sessionStorage.getItem("sidebar-state"); + let state; + if (raw) { + state = JSON.parse(raw); + if (state && content.dataset.nbSidebarHash === state.hash) { + const groups = content.querySelectorAll("[data-nb-sidebar-group]"); + for (let i = 0; i < groups.length; i++) { + if (typeof state.open[i] === "boolean") { + setGroupOpen(groups[i], state.open[i]); + } + } + } + } + + // Active page's group must always be open, regardless of stored state + const active = sidebar.querySelector("[aria-current='page']"); + if (active) { + let d = active.closest("[data-nb-sidebar-group]"); + while (d) { + setGroupOpen(d, true); + d = d.parentElement && d.parentElement.closest("[data-nb-sidebar-group]"); + } + } + + // Restore scroll or center active item + if (raw && state && state.scroll) { + sidebar.scrollTop = state.scroll; + } else if (active) { + const sr = sidebar.getBoundingClientRect(); + const ar = active.getBoundingClientRect(); + sidebar.scrollTop += ar.top - sr.top - sr.height / 2 + ar.height / 2; + } + } catch (_) {} + })(); + </script> + )} + + {/* Named view-transition group — only the content fades+rises on nav. + Header / sidebar / TOC stay still via the root rule in globals.css. */} + <main id="main-content" transition:name="nb-content" class="flex-1 min-w-0"> + <div class={`mx-auto px-4 lg:px-6 pt-6 pb-12 ${wide ? "max-w-[1400px]" : "max-w-(--nb-content-max)"}`}> + {versionStatus?.isDeprecated && ( + <Banner + type="caution" + content={ + currentSiblingUrl + ? `This is the <strong>${versionStatus.version}</strong> version of the docs and is no longer maintained. The latest version of this page is at <a href="${currentSiblingUrl}">${currentSiblingUrl}</a>.` + : `This is the <strong>${versionStatus.version}</strong> version of the docs and is no longer maintained. See the <a href="/">current docs</a> for up-to-date content.` + } + /> + )} + {banner && <Banner content={banner.content} type={banner.type} dismissible={banner.dismissible} />} + <Breadcrumbs items={breadcrumbItems} class="pb-2" /> + <div {...pagefindAttrs}> + {hasPageTitle ? ( + <slot name="page-title" /> + ) : ( + <Fragment> + <div class="flex items-center gap-3 flex-wrap mt-6"> + <h1 class="text-foreground mb-0 leading-tight" style={`font-size:var(--nb-h1-size);font-weight:var(--nb-h1-weight);letter-spacing:var(--nb-h1-tracking)`}>{title}</h1> + {draft && <Badge text="Draft" variant="warning" size="medium" />} + </div> + {/* `description` is meta-only (head tags via BaseLayout), matching + upstream cloudflare-docs which never renders it on the page. */} + <PageActions markdownUrl={markdownUrl} lastUpdated={lastUpdated} class="mt-3" /> + {(markdownUrl || audience === "human") && ( + audience === "human" ? ( + <div class="mt-3 mb-6 flex items-center gap-3" role="none"> + <div class="h-px flex-1 bg-border"></div> + <Badge text="For humans" variant="success" size="small" class="font-mono uppercase border border-success/15" /> + <div class="h-px flex-1 bg-border"></div> + </div> + ) : ( + <div class="mt-3 mb-6 h-px w-full bg-border" role="none" /> + ) + )} + {!markdownUrl && audience !== "human" && <div class="mb-4" />} + </Fragment> + )} + <article class="docs-content max-w-none"> + <slot /> + </article> + {hasContentFooter ? ( + <slot name="content-footer" /> + ) : editUrl && ( + <div class="mt-8 mb-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground"> + <a href={editUrl} class="inline-flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors no-underline" target="_blank" rel="noopener"> + <Icon name="ph:pencil-simple" class="w-3.5 h-3.5" /> + Edit this page + </a> + </div> + )} + </div> + {hasPagination ? <slot name="pagination" /> : <Pagination prevNext={prevNext} />} + </div> + </main> + + {showToc && ( + <aside class="hidden xl:block w-(--nb-toc-width) shrink-0 sticky top-14 h-[calc(100vh-3.5rem)] overflow-y-auto"> + <div class="pt-6 pb-8 pl-8 pr-6"> + {hasToc ? <slot name="toc" /> : <TOC headings={headings} />} + </div> + </aside> + )} + </div> + </div> + )} + + </div> + + {showSidebar && ( + /* Mobile sidebar — native <dialog> for free focus trap, escape, backdrop. */ + <dialog + data-mobile-sidebar + data-state="closed" + class="group fixed inset-0 m-0 h-full w-full max-h-full max-w-full border-0 bg-transparent p-0 data-[state=open]:bg-black/40 data-[state=closing]:bg-black/40" + aria-label="Site navigation" + > + <div data-mobile-sidebar-panel class="h-full w-full -translate-x-full overflow-y-auto bg-card shadow-lg transition-transform duration-250 ease-out group-data-[state=open]:translate-x-0 motion-reduce:transition-none"> + <div class="sticky top-0 z-[1] flex items-center justify-between border-b border-border bg-card px-4 py-3"> + <span class="font-semibold text-sm text-foreground">{config.title}</span> + <button data-close-sidebar class="flex items-center justify-center w-8 h-8 rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors" aria-label="Close sidebar"> + <Icon name="ph:x" class="w-5 h-5" /> + </button> + </div> + <div data-mobile-sidebar-slot></div> + </div> + </dialog> + )} + + <script> + import { lockScroll, unlockScroll } from "nimbus-docs/client"; + + const dialog = document.querySelector<HTMLDialogElement>("[data-mobile-sidebar]"); + const menuBtn = document.querySelector<HTMLElement>("[data-menu-btn]"); + const sidebarHome = document.querySelector<HTMLElement>("[data-sidebar-home]"); + const mobileSidebarSlot = dialog?.querySelector<HTMLElement>("[data-mobile-sidebar-slot]"); + const sharedSidebarNav = sidebarHome?.querySelector<HTMLElement>("[data-shared-sidebar-nav]") ?? null; + const CLOSE_DURATION_MS = 250; + + if (dialog && menuBtn) { + let closeTimer: ReturnType<typeof setTimeout> | undefined; + let desktopOpenState: boolean[] | null = null; + let mobileOpenState: boolean[] | null = null; + let sidebarLocked = false; + let suppressNextFocus = false; + + function readOpenState() { + if (!sharedSidebarNav) return []; + return [...sharedSidebarNav.querySelectorAll<HTMLElement>("[data-nb-sidebar-group]")] + .map((group) => group.querySelector<HTMLElement>("[data-nb-collapsible-trigger]")?.getAttribute("data-nb-state") === "open"); + } + + function setGroupOpen(group: HTMLElement, open: boolean) { + const disclosure = (group as HTMLElement & { + __nbDisclosure?: { open(): void; close(): void; isOpen(): boolean }; + }).__nbDisclosure; + if (disclosure) { + if (disclosure.isOpen() !== open) { + open ? disclosure.open() : disclosure.close(); + } + return; + } + group.setAttribute("data-nb-default-open", open ? "true" : "false"); + const trigger = group.querySelector<HTMLElement>("[data-nb-collapsible-trigger]"); + const panel = group.querySelector<HTMLElement>("[data-nb-collapsible-content]"); + const state = open ? "open" : "closed"; + if (trigger) { + trigger.setAttribute("data-nb-state", state); + trigger.setAttribute("aria-expanded", String(open)); + } + if (panel) panel.setAttribute("data-nb-state", state); + } + + function applyOpenState(state: boolean[] | null) { + if (!sharedSidebarNav || !state) return; + [...sharedSidebarNav.querySelectorAll<HTMLElement>("[data-nb-sidebar-group]")] + .forEach((group, index) => { + if (typeof state[index] === "boolean") setGroupOpen(group, state[index]); + }); + } + + function moveSidebarToMobile() { + if (!sharedSidebarNav || !mobileSidebarSlot) return; + desktopOpenState = readOpenState(); + mobileSidebarSlot.appendChild(sharedSidebarNav); + applyOpenState(mobileOpenState); + } + + function restoreSidebarToDesktop() { + if (!sharedSidebarNav || !sidebarHome) return; + if (sharedSidebarNav.parentElement === mobileSidebarSlot) { + mobileOpenState = readOpenState(); + const filter = sharedSidebarNav.querySelector<HTMLInputElement>("[data-nb-sidebar-filter-input]"); + if (filter) filter.value = ""; + sharedSidebarNav + .querySelectorAll<HTMLElement>("[data-nb-sidebar-hidden], [data-nb-opened-by-filter]") + .forEach((el) => { + el.removeAttribute("data-nb-sidebar-hidden"); + el.removeAttribute("data-nb-opened-by-filter"); + }); + sidebarHome.appendChild(sharedSidebarNav); + applyOpenState(desktopOpenState); + desktopOpenState = null; + } + } + + function finishClose(restoreFocus = true) { + if (closeTimer) { + clearTimeout(closeTimer); + closeTimer = undefined; + } + + dialog.dataset.state = "closed"; + restoreSidebarToDesktop(); + if (sidebarLocked) { + unlockScroll(); + sidebarLocked = false; + } + if (restoreFocus && !desktopMq.matches) menuBtn.focus(); + } + + const openSidebar = () => { + if (closeTimer) { + clearTimeout(closeTimer); + closeTimer = undefined; + } + if (dialog.open) return; + + moveSidebarToMobile(); + dialog.showModal(); + dialog.dataset.state = "closed"; + lockScroll(); + sidebarLocked = true; + + requestAnimationFrame(() => { + requestAnimationFrame(() => { + dialog.dataset.state = "open"; + }); + }); + + dialog.querySelector<HTMLElement>("[data-close-sidebar]")?.focus(); + }; + + const closeSidebar = () => { + if (!dialog.open || dialog.dataset.state === "closing") return; + + dialog.dataset.state = "closing"; + closeTimer = setTimeout(() => { + dialog.close(); + }, CLOSE_DURATION_MS); + }; + + menuBtn.addEventListener("click", openSidebar); + + dialog.addEventListener("cancel", (event) => { + event.preventDefault(); + closeSidebar(); + }); + + // Cleanup on close — fires for Escape, close button, backdrop click + dialog.addEventListener("close", () => { + finishClose(!suppressNextFocus); + suppressNextFocus = false; + }); + + document.addEventListener("astro:before-swap", () => { + if (closeTimer) { + clearTimeout(closeTimer); + closeTimer = undefined; + } + suppressNextFocus = true; + if (dialog.open) dialog.close(); + finishClose(false); + }); + + // Close button + dialog.querySelector("[data-close-sidebar]") + ?.addEventListener("click", closeSidebar); + + // Backdrop click (clicks on <dialog> itself, outside the panel) + dialog.addEventListener("click", (e) => { + if (e.target === dialog) closeSidebar(); + }); + + // The drawer is a mobile-only affordance (the burger is `lg:hidden` and + // the sidebar lives inline on desktop). If it's open when the viewport + // grows to desktop, close it so it doesn't linger over the page. + const desktopMq = window.matchMedia("(min-width: 1024px)"); + desktopMq.addEventListener("change", (e) => { + if (e.matches && dialog.open) dialog.close(); + }); + } + </script> + +</BaseLayout> diff --git a/src/nimbus/layouts/SplashLayout.astro b/src/nimbus/layouts/SplashLayout.astro new file mode 100644 index 00000000000..fe375a2daf5 --- /dev/null +++ b/src/nimbus/layouts/SplashLayout.astro @@ -0,0 +1,23 @@ +--- +/** + * Splash layout — full-width landing shell with no sidebar, for the custom + * `src/pages` routes that CF rendered via Starlight's `template: "splash"` + * (agent-setup, glossary, plans, sponsorships, videos, …). Pages supply their + * own inner container/width; this just provides the BaseLayout + sidebar-less + * Header + main shell. + */ +import BaseLayout from "./BaseLayout.astro"; +import Header from "../components/Header.astro"; +import type { BasePageProps } from "nimbus-docs/types"; + +type Props = BasePageProps; +--- + +<BaseLayout {...Astro.props}> + <div class="flex min-h-screen flex-col"> + <Header showSidebar={false} /> + <main id="main-content" transition:name="nb-content" class="flex-1"> + <slot /> + </main> + </div> +</BaseLayout> diff --git a/src/nimbus/lib/cn.ts b/src/nimbus/lib/cn.ts new file mode 100644 index 00000000000..75424d66a7e --- /dev/null +++ b/src/nimbus/lib/cn.ts @@ -0,0 +1,7 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +/** Compose class names with Tailwind conflict resolution. Consumer class (last arg) wins. */ +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/src/nimbus/mdx-components.ts b/src/nimbus/mdx-components.ts new file mode 100644 index 00000000000..3fec011f844 --- /dev/null +++ b/src/nimbus/mdx-components.ts @@ -0,0 +1,103 @@ +import { Aside } from "./components/ui/aside"; +import { Badge } from "./components/ui/badge"; +import { Card } from "./components/ui/card"; +import { CardGrid } from "./components/ui/card-grid"; +import { Code } from "./components/ui/code"; +import { FileTree } from "./components/ui/file-tree"; +import { PackageManagers } from "./components/ui/package-managers"; +import { Step, Steps } from "./components/ui/steps"; +import { TabItem, Tabs } from "./components/ui/tabs"; +import LinkButton from "./components/ui/link-button/LinkButton.astro"; +import Render from "./components/Render.astro"; +import APIRequest from "./components/cf/APIRequest.astro"; +import DashButton from "./components/cf/DashButton.astro"; +import DirectoryListing from "./components/cf/DirectoryListing.astro"; +import Description from "./components/cf/Description.astro"; +import Details from "./components/cf/Details.astro"; +import MetaInfo from "./components/cf/MetaInfo.astro"; +import Type from "./components/cf/Type.astro"; +import WranglerConfig from "./components/cf/WranglerConfig.astro"; +import WranglerNamespace from "./components/cf/WranglerNamespace.astro"; +import Feature from "./components/cf/Feature.astro"; +import Glossary from "./components/cf/Glossary.astro"; +import GlossaryTooltip from "./components/cf/GlossaryTooltip.astro"; +import LinkCard from "./components/cf/LinkCard.astro"; +import LinkTitleCard from "./components/cf/LinkTitleCard.astro"; +import ListTutorials from "./components/cf/ListTutorials.astro"; +import Plan from "./components/cf/Plan.astro"; +import ProductReleaseNotes from "./components/cf/ProductReleaseNotes.astro"; +import ProductChangelog from "./components/cf/ProductChangelog.astro"; +import RelatedProduct from "./components/cf/RelatedProduct.astro"; +import TypeScriptExample from "./components/cf/TypeScriptExample.astro"; +import InlineBadge from "./components/cf/InlineBadge.astro"; +import YouTube from "./components/cf/YouTube.astro"; +import Example from "./components/cf/Example.astro"; +import Markdown from "./components/cf/Markdown.astro"; +import CURL from "./components/cf/CURL.astro"; +import GitHubCode from "./components/cf/GitHubCode.astro"; +import Width from "./components/cf/Width.astro"; +import RuleID from "./components/cf/RuleID.astro"; +import PublicStats from "./components/cf/PublicStats.astro"; +import RSSButton from "./components/cf/RSSButton.astro"; +import GlossaryDefinition from "./components/cf/GlossaryDefinition.astro"; +import WranglerCommand from "./components/cf/WranglerCommand.astro"; +import AnchorHeading from "./components/cf/AnchorHeading.astro"; +import FeatureTable from "./components/cf/FeatureTable.astro"; +import PagesBuildPreset from "./components/cf/PagesBuildPreset.astro"; +import AvailableNotifications from "./components/cf/AvailableNotifications.astro"; +import Stream from "./components/cf/Stream.astro"; +import ResourcesBySelector from "./components/cf/ResourcesBySelector.astro"; + +export const components = { + APIRequest, + AnchorHeading, + Aside, + AvailableNotifications, + Badge, + CURL, + Card, + CardGrid, + Code, + DashButton, + Description, + Details, + DirectoryListing, + Example, + Feature, + FeatureTable, + FileTree, + GitHubCode, + Glossary, + GlossaryDefinition, + GlossaryTooltip, + InlineBadge, + LinkButton, + LinkCard, + LinkTitleCard, + ListTutorials, + Markdown, + MetaInfo, + PackageManagers, + PagesBuildPreset, + Plan, + ProductChangelog, + ProductReleaseNotes, + PublicStats, + RelatedProduct, + Render, + ResourcesBySelector, + RSSButton, + RuleID, + Step, + Steps, + Stream, + TabItem, + Tabs, + Type, + TypeScriptExample, + Width, + WranglerCommand, + WranglerConfig, + WranglerNamespace, + YouTube, +}; diff --git a/src/nimbus/pages/[...changelog].xml.ts b/src/nimbus/pages/[...changelog].xml.ts new file mode 100644 index 00000000000..8912e1b7070 --- /dev/null +++ b/src/nimbus/pages/[...changelog].xml.ts @@ -0,0 +1,153 @@ +/** + * Per-product changelog RSS — one feed per docs page tagged + * `pcx_content_type: changelog` that has a `release_notes_file_name`. + * Served at `/<docs-page-id>/index.xml`, which is exactly where the + * <RSSButton /> rendered by ProductReleaseNotes points. + * + * CF source: cloudflare-docs/src/pages/[...changelog].xml.ts + * + * Faithful port with two adaptations to this app's conventions: + * - Site origin comes from `virtual:nimbus/config` (`config.site`) — the + * same source the llms.txt routes use — rather than `context.site`. + * - Heading anchors use a local slugify matching `AnchorHeading.astro` + * instead of `github-slugger`, so no extra dependency is pulled in. + */ +import rss from "@astrojs/rss"; +import { getCollection, getEntry } from "astro:content"; +import type { APIRoute } from "astro"; +import { marked, type Token } from "marked"; +import { config } from "virtual:nimbus/config"; +import { entryToString } from "~/util/container"; + +export const prerender = true; + +// Mirrors src/components/cf/AnchorHeading.astro so RSS anchors line up with +// the ids rendered on the changelog page. +function slugify(input: string): string { + return input + .trim() + .toLowerCase() + .replace(/[^\w\s-]/g, "") + .replace(/\s+/g, "-") + .replace(/-+/g, "-"); +} + +export async function getStaticPaths() { + const releaseNotes = await getCollection("docs", (entry) => { + return ( + entry.data.pcx_content_type === "changelog" && + Boolean(entry.data.release_notes_file_name) + ); + }); + + return releaseNotes.map((entry) => { + return { + params: { + changelog: entry.id + `/index`, + }, + props: { + entry, + }, + }; + }); +} + +export const GET: APIRoute = async (context) => { + function walkTokens(token: Token) { + if (token.type === "image" || token.type === "link") { + if (token.href.startsWith("/")) { + token.href = new URL(token.href, config.site).href; + } + } + } + + marked.use({ walkTokens }); + + const entry = context.props.entry; + + if (!entry.data.release_notes_file_name) { + throw new Error( + `release_notes_file_name is required on ${entry.id}, to generate RSS feeds.`, + ); + } + + const releaseNotes = await getCollection("release-notes", (releaseNote) => { + return entry.data.release_notes_file_name?.includes(releaseNote.id); + }); + + const mapped = await Promise.all( + releaseNotes.flatMap((product) => { + return product.data.entries.map(async (entry) => { + let description; + if (entry.individual_page) { + const link = entry.link; + + if (!link) + throw new Error( + `Changelog entry points to individual page but no link is provided`, + ); + + const page = await getEntry("docs", link.slice(1, -1)); + + if (!page) + throw new Error( + `Changelog entry points to ${link.slice(1, -1)} but unable to find entry with that slug`, + ); + + description = (await entryToString(page, context.locals)) ?? page.body; + } else { + description = entry.description; + } + + let link; + if (entry.link) { + link = entry.link; + } else { + const anchor = slugify(entry.title ?? entry.publish_date); + link = product.data.link.concat(`#${anchor}`); + } + + let title; + if (entry.scheduled) { + title = `Scheduled for ${entry.scheduled_date}`; + } else { + title = entry.title; + } + + return { + product: product.data.productName, + link, + date: entry.publish_date, + description, + title, + }; + }); + }), + ); + + const entries = mapped.sort((a, b) => { + return a.date < b.date ? 1 : a.date > b.date ? -1 : 0; + }); + + const rssName = releaseNotes[0].data.productName; + + const site = new URL(config.site); + site.pathname = entry.id.concat("/"); + + return rss({ + title: `Changelog | ${rssName}`, + description: `Updates to ${rssName}`, + site, + trailingSlash: false, + items: entries.map((entry) => { + return { + title: `${entry.product} - ${entry.title ?? entry.date}`, + description: marked.parse(entry.description ?? "", { + async: false, + }) as string, + pubDate: new Date(entry.date), + link: entry.link, + }; + }), + }); +}; diff --git a/src/nimbus/pages/[...slug].astro b/src/nimbus/pages/[...slug].astro new file mode 100644 index 00000000000..cb21d213e7a --- /dev/null +++ b/src/nimbus/pages/[...slug].astro @@ -0,0 +1,110 @@ +--- +import DocsLayout from "../layouts/DocsLayout.astro"; +import { + getDocsStaticPaths, + getDocsPageProps, + getSidebar, + getPrevNext, + getBreadcrumbs, + getEditUrl, + getLastUpdated, + getTOC, +} from "nimbus-docs"; +import { config } from "virtual:nimbus/config"; +import { docsSidebarTransform } from "../util/sidebar"; +import { components } from "../mdx-components"; + +export const prerender = true; +export const getStaticPaths = getDocsStaticPaths; + +const { entry, Content, headings } = await getDocsPageProps(Astro); + +const currentSlug = Astro.url.pathname.replace(/\/$/, "") || "/"; +const sectionSegment = currentSlug.split("/").filter(Boolean)[0]; + +// Column renders only when not custom mode AND site-wide flag on AND page hasn't opted out. +const isCustom = entry.data.mode === "custom"; +const sidebarOn = + !isCustom && + config.features?.sidebar !== false && + entry.data.sidebar !== false; +const tocOn = + !isCustom && + config.features?.tableOfContents !== false && + entry.data.tableOfContents !== false; + +const sidebar = sidebarOn + ? await getSidebar(currentSlug, { + collection: entry.collection, + transform: docsSidebarTransform, + }) + : false; + +const prevNext = await getPrevNext(currentSlug, { + sidebarTree: sidebar === false ? [] : sidebar, + overrides: { prev: entry.data.prev, next: entry.data.next }, +}); +const rawBreadcrumbs = await getBreadcrumbs(currentSlug, { + collection: entry.collection, +}); +const lastCrumb = rawBreadcrumbs[rawBreadcrumbs.length - 1]; +const breadcrumbs = + rawBreadcrumbs.length > 2 && + lastCrumb?.href && + lastCrumb.href.replace(/\/$/, "") === `/${sectionSegment}` + ? rawBreadcrumbs.slice(0, -1) + : rawBreadcrumbs; +const editUrl = await getEditUrl(entry); +// Frontmatter wins; git is the fallback. +const lastUpdated = entry.data.lastUpdated ?? await getLastUpdated(entry); +const toc = tocOn ? getTOC(headings, entry.data.tableOfContents) : false; +const markdownPath = `/${entry.id}/index.md`; +const markdownUrl = Astro.site ? new URL(markdownPath, Astro.site).href : markdownPath; +// Per-page OG cards (programmatic generation) were dropped — not worth the +// ~34s build cost without incremental caching. Frontmatter can still set a +// per-page image; otherwise NimbusHead falls back to the site-wide `/og.png`. +const socialImage = entry.data.socialImage; + +// `external_link` pages (e.g. the per-product "REST API" stubs pointing to +// `/api/`) build at their own path but redirect to the target. The sidebar +// already links straight to `external_link` via nimbus-docs; this meta-refresh +// covers a direct hit on the stub's own URL, matching Starlight's +// `Head.astro` (`0; url=…`). Body stays empty — the browser leaves immediately. +const externalLink = entry.data.external_link; +const head = [ + ...(entry.data.head ?? []), + ...(externalLink + ? [ + { + tag: "meta" as const, + attrs: { "http-equiv": "refresh", content: `0; url=${externalLink}` }, + }, + ] + : []), +]; +--- + +<DocsLayout + title={entry.data.title} + description={entry.data.description} + sidebar={sidebar} + headings={toc} + breadcrumbs={breadcrumbs} + prevNext={prevNext} + mode={entry.data.mode} + banner={entry.data.banner} + head={head} + searchable={entry.data.searchable} + noindex={entry.data.noindex} + markdownUrl={markdownUrl} + socialImage={socialImage} + lastUpdated={lastUpdated} + editUrl={editUrl} + draft={entry.data.draft} + audience={entry.data.audience} + collection={entry.collection} + entryId={entry.id} + wide={entry.data.wide} +> + <Content components={components} /> +</DocsLayout> diff --git a/src/nimbus/pages/[...slug]/index.md.ts b/src/nimbus/pages/[...slug]/index.md.ts new file mode 100644 index 00000000000..0e838551023 --- /dev/null +++ b/src/nimbus/pages/[...slug]/index.md.ts @@ -0,0 +1,79 @@ +/** + * TODO(migration): replace this build-time `.md` route with edge "Markdown for + * Agents" (HTML→Markdown at request time) at cutover. See the migration plan. + */ +/** + * Per-page `/<slug>/index.md` — the clean-markdown alternate for every + * indexable entry of the primary `docs` collection. + * + * Non-primary collections (`api`, `blog`, …) mount under their own + * URL namespace by convention; their `.md` alternates live at the + * sibling route `pages/<collection>/[...slug]/index.md.ts`. This route + * filters to the primary collection so multi-collection sites don't + * generate conflicting `[...slug]` paths at root. + */ + +import { getIndexedEntries, renderEntryAsMarkdown, type IndexedEntry } from "nimbus-docs"; +import { config } from "virtual:nimbus/config"; + +export const prerender = true; + +const PRIMARY_COLLECTION = "docs"; + +interface SlugProps { + item: IndexedEntry; +} + +export async function getStaticPaths() { + const indexed = await getIndexedEntries(); + return indexed + .filter((item) => item.collection === PRIMARY_COLLECTION) + .map((item) => ({ + // Root index (`entry.id === "index"`) emits at `/index.md`; Astro's + // rest-segment treats `undefined` as "no segment" so the URL is + // `/index.md` rather than `/index/index.md`. Every other entry emits + // at `/<entry.id>/index.md` — the convention `<page>/index.md`. + params: { + slug: item.entry.id === "index" ? undefined : item.entry.id, + }, + props: { item } as SlugProps, + })); +} + +export async function GET({ props }: { props: SlugProps }) { + const { item } = props; + const { entry, title, description, markdownUrl } = item; + const data = (entry.data ?? {}) as Record<string, unknown>; + const rawImage = data.socialImage; + const socialImage = + typeof rawImage === "string" && rawImage.length > 0 + ? rawImage + : config.socialImage; + + const markdown = renderEntryAsMarkdown(entry); + + const body = [ + "---", + `title: ${JSON.stringify(title)}`, + ...(description ? [`description: ${JSON.stringify(description)}`] : []), + ...(socialImage + ? [`image: ${JSON.stringify(new URL(socialImage, config.site).href)}`] + : []), + "---", + "", + "> Documentation Index", + `> Fetch the complete documentation index at: ${new URL("/llms.txt", config.site).href}`, + "> Use this file to discover all available pages before exploring further.", + "", + `# ${title}`, + "", + markdown, + "", + `Source: ${new URL(markdownUrl, config.site).href}`, + "", + ].join("\n"); + + return new Response(body, { + headers: { "Content-Type": "text/markdown; charset=utf-8" }, + }); +} diff --git a/src/nimbus/pages/[section]/llms.txt.ts b/src/nimbus/pages/[section]/llms.txt.ts new file mode 100644 index 00000000000..a6d90f3d669 --- /dev/null +++ b/src/nimbus/pages/[section]/llms.txt.ts @@ -0,0 +1,64 @@ +/** + * Per-section /<section>/llms.txt — sub-index files that drill down + * from the root `/llms.txt` into a named slice of the site's docs. + * + * A "section" is one of two things: + * 1. A folder inside the primary `docs` collection with more than + * one page (e.g. `src/content/docs/<folder>/*` → `/<folder>/llms.txt`). + * 2. A whole non-primary collection — `api`, `blog`, etc. — which + * becomes a single section mounted at `/<collection>/llms.txt`. + * + * Both cases produce the same shape at the same URL pattern, so + * agents follow one rule: every link in `/llms.txt` that ends in + * `.llms.txt` resolves here. + * + * `getIndexedTopLevel()` decides which sections exist and what they + * contain; this route just renders one file per section it returns. + */ + +import { getIndexedTopLevel, type IndexedEntry } from "nimbus-docs"; +import { config } from "virtual:nimbus/config"; + +export const prerender = true; + +interface SectionProps { + slug: string; + label: string; + members: IndexedEntry[]; +} + +export async function getStaticPaths() { + const { groups } = await getIndexedTopLevel(); + return groups + // Versioning P3: hidden versions don't get a per-section llms.txt + // index. They're URL-reachable for direct navigation, but every + // agent-discovery surface should treat them as if they don't exist. + .filter((group) => !group.hidden) + .map((group) => ({ + params: { section: group.slug }, + props: { + slug: group.slug, + label: group.label, + members: group.members, + } as SectionProps, + })); +} + +export async function GET({ props }: { props: SectionProps }) { + const { label, members } = props; + + const lines = [`# ${label}`, "", "## Pages", ""]; + + for (const item of members) { + const description = item.description ? ` — ${item.description}` : ""; + lines.push( + `- [${item.title}](${new URL(item.markdownUrl, config.site).href})${description}`, + ); + } + + lines.push(""); + + return new Response(lines.join("\n"), { + headers: { "Content-Type": "text/plain; charset=utf-8" }, + }); +} diff --git a/src/nimbus/pages/agent-setup/index.astro b/src/nimbus/pages/agent-setup/index.astro new file mode 100644 index 00000000000..33ae6addbd4 --- /dev/null +++ b/src/nimbus/pages/agent-setup/index.astro @@ -0,0 +1,185 @@ +--- +/** + * /agent-setup/ — splash landing for installing Cloudflare agent tooling. + * + * CF source: src/pages/agent-setup/index.astro (StarlightPage `template: + * "splash"`). Ported to the Nimbus splash pattern (BaseLayout + sidebar-less + * Header + centered container, as in resources/index.astro); the body, + * components, and styles are unchanged. + */ +import SplashLayout from "~/layouts/SplashLayout.astro"; +import CatalogWithFilter from "~/components/agent-setup/CatalogWithFilter.astro"; +import AgentComparison from "~/components/agent-setup/AgentComparison.astro"; +import AgentPrimer from "~/components/agent-setup/AgentPrimer.astro"; +import PromptCopyBlock from "~/components/agent-setup/PromptCopyBlock.astro"; +import { AGENTS } from "~/components/agent-setup/agents"; + +import "~/styles/agent-setup.css"; + +export const prerender = true; + +const agents = AGENTS; +--- + +<SplashLayout + title="Agent setup" + description="Cloudflare provides Skills and MCP servers so your agent can seamlessly build on the Cloudflare platform. Pick an agent below to get started." +> + <div class="agent-setup-page mx-auto w-full px-[max(1.5rem,4vw)] py-14"> + <p class="agent-setup-lede"> + Install an agent of your choice, connect Cloudflare <a + href="https://github.com/cloudflare/skills" + target="_blank" + rel="noopener noreferrer">Skills ↗</a + > and <a + href="https://blog.cloudflare.com/code-mode-mcp/" + target="_blank" + rel="noopener noreferrer">Code Mode API ↗</a + > and <a + href="https://github.com/cloudflare/mcp-server-cloudflare" + target="_blank" + rel="noopener noreferrer">domain-specific ↗</a + > MCP servers, and start deploying to Cloudflare from your editor or terminal. + </p> + + <div class="agent-setup-paths not-content"> + <!-- Left: quick path --> + <div + class="agent-setup-path agent-setup-path--quick" + style="flex-basis: 60%;" + > + <p class="agent-setup-path-eyebrow">Quick setup</p> + <p class="agent-setup-path-title">Already have an agent?</p> + <p class="agent-setup-path-desc"> + Paste this into any AI coding agent to install Cloudflare agent + tooling in one step. + </p> + <PromptCopyBlock + text="Fetch https://developers.cloudflare.com/agent-setup/prompt.md" + hideLabel + /> + </div> + <!-- Right: manual path --> + <div class="agent-setup-path agent-setup-path--manual"> + <p class="agent-setup-path-eyebrow">Manual setup</p> + <p class="agent-setup-path-title">New to AI agents?</p> + <p class="agent-setup-path-desc"> + Pick an agent, follow step-by-step setup instructions, compare + capabilities, and learn about the Cloudflare tools available to you. + </p> + <a href="#pick-your-agent" class="agent-setup-path-cta" + >Browse agents ↓</a + > + </div> + </div> + + <hr /> + + <div + class="agent-setup-section-header" + style="margin-top: 3.25rem;" + id="pick-your-agent" + > + <h2>Pick your agent</h2> + <p>Select an agent to get step-by-step setup instructions.</p> + </div> + + <CatalogWithFilter agents={agents} /> + + <div class="agent-setup-section" style="margin-top: 5.5rem;"> + <div class="agent-setup-section-header"> + <h2>Compare agents</h2> + <p>Capabilities, pricing, and context approaches compared.</p> + </div> + <AgentComparison agents={agents} /> + </div> + + <div class="agent-setup-section" style="margin-top: 5.5rem;"> + <div class="agent-setup-section-header"> + <h2>Understanding agents</h2> + <p>Common types, concepts, and tradeoffs.</p> + </div> + <AgentPrimer /> + </div> + </div> +</SplashLayout> + +<style> + .agent-setup-page { + max-width: 1200px; + } + + .agent-setup-paths { + display: flex; + flex-wrap: wrap; + gap: 1.5rem; + margin: 3rem 0 3.25rem; + align-items: stretch; + } + + .agent-setup-path { + flex: 1 1 340px; + min-width: 0; + align-self: stretch; + } + + /* Remove PromptCopyBlock's own margin when nested inside a card */ + .agent-setup-path :global(.pcb-root) { + margin: 0.5rem 0 0; + } + + .agent-setup-path { + padding: 1.75rem; + border: 1px solid var(--color-cl1-gray-8); + border-radius: 12px; + display: flex; + flex-direction: column; + gap: 0.75rem; + box-sizing: border-box; + } + + :root[data-theme="dark"] .agent-setup-path { + border-color: var(--color-cl1-gray-2); + } + + .agent-setup-path-eyebrow { + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--color-cl1-brand-orange); + margin: 0; + } + + .agent-setup-path-title { + font-size: 1.25rem; + font-weight: 700; + letter-spacing: -0.01em; + margin: 0; + line-height: 1.2; + } + + .agent-setup-path-desc { + font-size: 0.9375rem; + line-height: 1.6; + color: var(--sl-color-gray-3); + margin: 0; + } + + .agent-setup-path-cta { + display: inline-flex; + align-items: center; + gap: 0.375rem; + margin-top: auto; + padding-top: 0.5rem; + font-size: 0.9375rem; + font-weight: 500; + color: var(--color-cl1-brand-orange) !important; + text-decoration: none; + } + + .agent-setup-path-cta:hover { + text-decoration: underline; + text-underline-offset: 2px; + } +</style> diff --git a/src/nimbus/pages/agent-setup/prompt.md.ts b/src/nimbus/pages/agent-setup/prompt.md.ts new file mode 100644 index 00000000000..a69b13770fb --- /dev/null +++ b/src/nimbus/pages/agent-setup/prompt.md.ts @@ -0,0 +1,10 @@ +import type { APIRoute } from "astro"; +import promptText from "~/content/agent-setup/prompt.md?raw"; + +export const GET: APIRoute = () => { + return new Response(promptText, { + headers: { + "Content-Type": "text/markdown; charset=utf-8", + }, + }); +}; diff --git a/src/nimbus/pages/ai/models/[...name].astro b/src/nimbus/pages/ai/models/[...name].astro new file mode 100644 index 00000000000..df5ebe605b5 --- /dev/null +++ b/src/nimbus/pages/ai/models/[...name].astro @@ -0,0 +1,33 @@ +--- +/** + * /ai/models/<slug>/ — a single model page in the unified catalog. + * + * Rest param because `/ai` slugs are the full `model_id` (multi-segment, e.g. + * `openai/tts-1`) for catalog models and the full `@cf/...` name for legacy + * models. Bound to `getResolvedModels` with `model.slug`. + * + * Legacy-not-in-catalog models are intentionally emitted at BOTH this route + * (full slug) and `/workers-ai/models/[...name]` (short slug) — dual URLs, not + * canonical-deduped. + */ +import type { GetStaticPaths } from "astro"; +import ModelDetailPage from "~/components/models/ModelDetailPage.astro"; +import { getResolvedModels } from "~/util/models"; + +export const prerender = true; + +export const getStaticPaths = (async () => { + const models = await getResolvedModels(); + return models.map((model) => ({ + params: { name: model.slug }, + props: { model, slug: model.slug }, + })); +}) satisfies GetStaticPaths; + +const { model, slug } = Astro.props as { + model: Awaited<ReturnType<typeof getResolvedModels>>[number]; + slug: string; +}; +--- + +<ModelDetailPage model={model} basePath="/ai/models" slug={slug} /> diff --git a/src/nimbus/pages/ai/models/[...name].md.ts b/src/nimbus/pages/ai/models/[...name].md.ts new file mode 100644 index 00000000000..30cba9a7f9f --- /dev/null +++ b/src/nimbus/pages/ai/models/[...name].md.ts @@ -0,0 +1,30 @@ +/** + * TODO(migration): replace this build-time `.md` route with edge "Markdown for + * Agents" (HTML→Markdown at request time) at cutover. See the migration plan. + */ +/** + * /ai/models/<slug>.md — markdown rendering of a unified-catalog model page. + * + * Backs the page-actions row (Copy page / View as Markdown). Same per-route + * binding as the `/ai` `[...name].astro`: `getResolvedModels` + `model.slug`. + */ +import type { APIRoute, GetStaticPaths } from "astro"; +import { getResolvedModels, type ModelView } from "~/util/models"; +import { renderModelMarkdown } from "~/util/model-markdown"; + +export const prerender = true; + +export const getStaticPaths = (async () => { + const models = await getResolvedModels(); + return models.map((model) => ({ + params: { name: model.slug }, + props: { model, slug: model.slug }, + })); +}) satisfies GetStaticPaths; + +export const GET: APIRoute = ({ props }) => { + const { model, slug } = props as { model: ModelView; slug: string }; + return new Response(renderModelMarkdown(model, "/ai/models", slug), { + headers: { "content-type": "text/markdown; charset=utf-8" }, + }); +}; diff --git a/src/nimbus/pages/ai/models/[...schema].json.ts b/src/nimbus/pages/ai/models/[...schema].json.ts new file mode 100644 index 00000000000..7fc063431a5 --- /dev/null +++ b/src/nimbus/pages/ai/models/[...schema].json.ts @@ -0,0 +1,49 @@ +/** + * /ai/models/<slug>/<schema>.json — raw JSON-Schema endpoints (unified catalog). + * + * `detectApiModes` splits the schema into per-mode `{mode.id}-input/-output.json` + * when it has modes, else a single `schema-input/-output.json`. Bound to + * `getResolvedModels` with `model.slug` (full/multi-segment). + */ +import type { APIRoute, GetStaticPaths, InferGetStaticPropsType } from "astro"; +import { getResolvedModels, detectApiModes } from "~/util/models"; + +export const prerender = true; + +export const getStaticPaths = (async () => { + const models = await getResolvedModels(); + const paths: { params: { schema: string }; props: { schema: unknown } }[] = []; + + for (const model of models) { + const slug = model.slug; + const modes = detectApiModes(model.schema); + + if (modes) { + for (const mode of modes) { + paths.push({ + params: { schema: `${slug}/${mode.id}-input` }, + props: { schema: mode.input }, + }); + paths.push({ + params: { schema: `${slug}/${mode.id}-output` }, + props: { schema: mode.output }, + }); + } + } else { + paths.push({ + params: { schema: `${slug}/schema-input` }, + props: { schema: model.schema.input }, + }); + paths.push({ + params: { schema: `${slug}/schema-output` }, + props: { schema: model.schema.output }, + }); + } + } + + return paths; +}) satisfies GetStaticPaths; + +type Props = InferGetStaticPropsType<typeof getStaticPaths>; + +export const GET: APIRoute<Props> = ({ props }) => Response.json(props.schema); diff --git a/src/nimbus/pages/ai/models/index.astro b/src/nimbus/pages/ai/models/index.astro new file mode 100644 index 00000000000..aac5412eec2 --- /dev/null +++ b/src/nimbus/pages/ai/models/index.astro @@ -0,0 +1,37 @@ +--- +/** + * /ai/models/ — the unified AI model catalog (Cloudflare-hosted + third-party). + * + * Rendered through DocsLayout: title "Models", TOC off, and the model slice + * resolved via `getResolvedModels` → `toModelCardData`. `basePath` defaults to + * "/ai/models". The Providers facet appears because this slice mixes hosted + * (legacy) + proxied (catalog) models. + */ +import { getSidebar, getBreadcrumbs } from "nimbus-docs"; +import DocsLayout from "~/layouts/DocsLayout.astro"; +import { externalAppLinksTransform } from "~/util/sidebar"; +import ModelCatalog from "~/components/models/ModelCatalog.astro"; +import { getResolvedModels, toModelCardData } from "~/util/models"; + +export const prerender = true; + +const models = (await getResolvedModels()).map(toModelCardData); + +const sectionSlug = "/ai/models/"; +const sidebar = await getSidebar(sectionSlug, { collection: "docs", transform: externalAppLinksTransform }); +const breadcrumbs = await getBreadcrumbs(sectionSlug); +--- + +<DocsLayout + title="Models" + description="Browse the unified catalog of AI models, including Cloudflare-hosted and third-party provider models." + sidebar={sidebar} + headings={false} + breadcrumbs={breadcrumbs} + prevNext={{}} + collection="docs" + wide +> + <Fragment slot="pagination" /> + <ModelCatalog models={models} /> +</DocsLayout> diff --git a/src/nimbus/pages/changelog/[...page].astro b/src/nimbus/pages/changelog/[...page].astro new file mode 100644 index 00000000000..3687d34041c --- /dev/null +++ b/src/nimbus/pages/changelog/[...page].astro @@ -0,0 +1,55 @@ +--- +/** + * /changelog/ — paginated index of all (non-hidden) changelog entries. + * CF source: cloudflare-docs/src/pages/changelog/[...page].astro + * Design: Nimbus changelog feature (timeline feed); product model + server + * pagination kept. + */ +import type { GetStaticPaths, Page } from "astro"; +import type { CollectionEntry } from "astro:content"; + +import ChangelogLayout from "~/layouts/ChangelogLayout.astro"; +import Header from "~/components/changelog/Header.astro"; +import Pagination from "~/components/changelog/Pagination.astro"; +import ChangelogFeed from "~/components/changelog/ChangelogFeed.astro"; + +import { getChangelogs } from "~/util/changelog"; + +export const prerender = true; + +export const getStaticPaths = (async ({ paginate }) => { + const visibleNotes = await getChangelogs({ + filter: (entry) => !entry.data.hidden, + }); + + return paginate(visibleNotes, { pageSize: 20 }); +}) satisfies GetStaticPaths; + +type Props = { page: Page<CollectionEntry<"changelog">> }; + +const { page } = Astro.props as Props; +--- + +<ChangelogLayout + title="Changelog" + description="New features, improvements, and fixes." + head={[ + { + tag: "link", + attrs: { + rel: "alternate", + type: "application/rss+xml", + title: "Changelog RSS", + href: "/changelog/rss/index.xml", + }, + }, + ]} +> + <Header rssHref="/changelog/rss/index.xml" /> + <ChangelogFeed entries={page.data} /> + <Pagination + currentPage={page.currentPage} + lastPage={page.lastPage} + baseUrl="/changelog/" + /> +</ChangelogLayout> diff --git a/src/nimbus/pages/changelog/post/[...slug].astro b/src/nimbus/pages/changelog/post/[...slug].astro new file mode 100644 index 00000000000..67cdd01b146 --- /dev/null +++ b/src/nimbus/pages/changelog/post/[...slug].astro @@ -0,0 +1,85 @@ +--- +/** + * /changelog/post/<id>/ — a single changelog entry (permalink). + * CF source: cloudflare-docs/src/pages/changelog/post/[...slug].astro + * Design: Nimbus changelog feature (registry/features/changelog.md, 5h); + * product model kept (product pills instead of tag pills). + */ +import type { GetStaticPaths } from "astro"; +import { Icon } from "astro-icon/components"; +import { render } from "astro:content"; + +import ChangelogLayout from "~/layouts/ChangelogLayout.astro"; +import ProductPills from "~/components/changelog/ProductPills.astro"; +import { components } from "~/mdx-components"; + +export const prerender = true; + +export const getStaticPaths = (async () => { + const { getChangelogs } = await import("~/util/changelog"); + const notes = await getChangelogs({}); + + return notes.map((note) => ({ + params: { slug: note.id }, + props: { note }, + })); +}) satisfies GetStaticPaths; + +const { note } = Astro.props; +const { title, description, date, products } = note.data; + +const iso = date.toISOString().slice(0, 10); +const dateLabel = date.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + timeZone: "UTC", +}); + +const { Content } = await render(note); +--- + +<ChangelogLayout + title={title} + description={description} + noindex={note.data.hidden} +> + <a + href="/changelog" + class="group mb-9 -ml-0.5 inline-flex items-center gap-1.5 text-[0.8125rem] font-medium text-muted-foreground no-underline transition-[color,transform] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] hover:text-foreground active:scale-[0.98] motion-reduce:active:scale-100" + > + <Icon + name="ph:arrow-left" + class="h-4 w-4 transition-transform duration-[160ms] ease-[cubic-bezier(0.23,1,0.32,1)] group-hover:-translate-x-0.5 motion-reduce:transform-none" + /> + <span>Changelog</span> + </a> + + <article> + <time + datetime={iso} + class="block text-[0.8125rem] font-medium tabular-nums text-muted-foreground" + > + {dateLabel} + </time> + {/* h1 token trio via inline style — site convention. */} + <h1 + class="mt-2 text-balance leading-[1.15] text-foreground" + style="font-size:var(--nb-h1-size);font-weight:var(--nb-h1-weight);letter-spacing:var(--nb-h1-tracking)" + > + {title} + </h1> + + { + products.length > 0 && ( + <div class="mt-4"> + <ProductPills products={products} /> + </div> + ) + } + + <div class="docs-content nb-cl-prose mt-9 max-w-none"> + <Content components={components} /> + </div> + </article> +</ChangelogLayout> diff --git a/src/nimbus/pages/changelog/product-group/[group]/[...page].astro b/src/nimbus/pages/changelog/product-group/[group]/[...page].astro new file mode 100644 index 00000000000..3155b3c6d09 --- /dev/null +++ b/src/nimbus/pages/changelog/product-group/[group]/[...page].astro @@ -0,0 +1,58 @@ +--- +/** + * /changelog/product-group/<slug>/ — paginated per-group changelog. + * CF source: cloudflare-docs/src/pages/changelog/product-group/[group]/[...page].astro + * Design: Nimbus changelog feature (timeline feed); product model kept. + */ +import type { GetStaticPaths, Page } from "astro"; +import type { CollectionEntry } from "astro:content"; + +import ChangelogLayout from "~/layouts/ChangelogLayout.astro"; +import Header from "~/components/changelog/Header.astro"; +import Pagination from "~/components/changelog/Pagination.astro"; +import ChangelogFeed from "~/components/changelog/ChangelogFeed.astro"; + +import { getChangelogs } from "~/util/changelog"; +import { directoryByGroup } from "~/util/directory"; + +export const prerender = true; + +export const getStaticPaths = (async ({ paginate }) => { + const allNotes = await getChangelogs({ + filter: (entry) => !entry.data.hidden, + }); + + return directoryByGroup.flatMap(([groupName, groupProducts]) => { + const groupProductIds = groupProducts.map((p) => p.id); + const notes = allNotes.filter((note) => + note.data.products.some((p) => groupProductIds.includes(p.id)), + ); + const slug = groupName.replaceAll(" ", "-").toLowerCase(); + + if (notes.length === 0) return []; + + return paginate(notes, { + params: { group: slug }, + props: { groupName }, + pageSize: 20, + }); + }); +}) satisfies GetStaticPaths; + +type Props = { page: Page<CollectionEntry<"changelog">>; groupName: string }; + +const { page, groupName } = Astro.props as Props; +--- + +<ChangelogLayout + title={`${groupName} Changelog`} + description={`New updates and improvements for ${groupName}.`} +> + <Header rssHref="/changelog/rss/index.xml" /> + <ChangelogFeed entries={page.data} /> + <Pagination + currentPage={page.currentPage} + lastPage={page.lastPage} + baseUrl={`/changelog/product-group/${Astro.params.group}/`} + /> +</ChangelogLayout> diff --git a/src/nimbus/pages/changelog/product/[product]/[...page].astro b/src/nimbus/pages/changelog/product/[product]/[...page].astro new file mode 100644 index 00000000000..1368ff2ed61 --- /dev/null +++ b/src/nimbus/pages/changelog/product/[product]/[...page].astro @@ -0,0 +1,71 @@ +--- +/** + * /changelog/product/<id>/ — paginated per-product changelog. + * CF source: cloudflare-docs/src/pages/changelog/product/[product]/[...page].astro + * Design: Nimbus changelog feature (timeline feed); product model kept. + */ +import type { GetStaticPaths, Page } from "astro"; +import { getEntry, type CollectionEntry } from "astro:content"; + +import ChangelogLayout from "~/layouts/ChangelogLayout.astro"; +import Header from "~/components/changelog/Header.astro"; +import Pagination from "~/components/changelog/Pagination.astro"; +import ChangelogFeed from "~/components/changelog/ChangelogFeed.astro"; + +import { getChangelogs } from "~/util/changelog"; + +export const prerender = true; + +export const getStaticPaths = (async ({ paginate }) => { + const allNotes = await getChangelogs({ + filter: (entry) => !entry.data.hidden, + }); + + const productIds = [ + ...new Set(allNotes.flatMap((note) => note.data.products.map((p) => p.id))), + ]; + + return productIds.flatMap((productId) => { + const notes = allNotes.filter((note) => + note.data.products.some((p) => p.id === productId), + ); + + return paginate(notes, { + params: { product: productId }, + pageSize: 20, + }); + }); +}) satisfies GetStaticPaths; + +type Props = { page: Page<CollectionEntry<"changelog">> }; + +const { page } = Astro.props as Props; +const productId = Astro.params.product as string; + +const productEntry = await getEntry("directory", productId); +const productTitle = productEntry?.data.entry.title ?? productId; +--- + +<ChangelogLayout + title={`${productTitle} Changelog`} + description={`New updates and improvements for ${productTitle}.`} + head={[ + { + tag: "link", + attrs: { + rel: "alternate", + type: "application/rss+xml", + title: `${productTitle} Changelog RSS`, + href: `/changelog/rss/${productId}.xml`, + }, + }, + ]} +> + <Header rssHref={`/changelog/rss/${productId}.xml`} /> + <ChangelogFeed entries={page.data} /> + <Pagination + currentPage={page.currentPage} + lastPage={page.lastPage} + baseUrl={`/changelog/product/${productId}/`} + /> +</ChangelogLayout> diff --git a/src/nimbus/pages/changelog/rss/[area].xml.ts b/src/nimbus/pages/changelog/rss/[area].xml.ts new file mode 100644 index 00000000000..5aec6193dff --- /dev/null +++ b/src/nimbus/pages/changelog/rss/[area].xml.ts @@ -0,0 +1,63 @@ +/** + * /changelog/rss/<group-slug>.xml — per product-group ("area") RSS feed. + * CF source: cloudflare-docs/src/pages/changelog/rss/[area].xml.ts + * Adapted: area slug uses the same `lower + spaces→hyphens` rule as the + * product-group pages (no github-slugger dependency). + */ +import rss from "@astrojs/rss"; +import { getCollection } from "astro:content"; +import { config } from "virtual:nimbus/config"; +import { getChangelogs, getRSSItems } from "~/util/changelog"; + +import type { + APIRoute, + InferGetStaticPropsType, + InferGetStaticParamsType, + GetStaticPaths, +} from "astro"; + +export const prerender = true; + +const slugifyArea = (value: string) => value.replaceAll(" ", "-").toLowerCase(); + +export const getStaticPaths = (async () => { + const products = await getCollection("directory", (e) => + Boolean(e.data.entry.group), + ); + + const areas = Object.entries( + Object.groupBy(products, (p) => p.data.entry.group!), + ); + + return areas.map(([area, products]) => { + if (!products) + throw new Error(`[Changelog] No products attributed to "${area}"`); + + return { + params: { area: slugifyArea(area) }, + props: { title: area, products }, + }; + }); +}) satisfies GetStaticPaths; + +type Props = InferGetStaticPropsType<typeof getStaticPaths>; +type Params = InferGetStaticParamsType<typeof getStaticPaths>; + +export const GET: APIRoute<Props, Params> = async ({ props, locals }) => { + const { title, products } = props; + + const notes = await getChangelogs({ + filter: (e) => { + return e.data.products.some((x) => products.some((y) => x.id === y.id)); + }, + }); + + const items = await getRSSItems({ notes, locals }); + + return rss({ + title: `Cloudflare changelogs | ${title}`, + description: `Cloudflare changelogs for ${title} products`, + site: new URL("/changelog/", config.site).href, + items, + }); +}; diff --git a/src/nimbus/pages/changelog/rss/[product].xml.ts b/src/nimbus/pages/changelog/rss/[product].xml.ts new file mode 100644 index 00000000000..eeb589543f6 --- /dev/null +++ b/src/nimbus/pages/changelog/rss/[product].xml.ts @@ -0,0 +1,54 @@ +/** + * /changelog/rss/<product-id>.xml — per-product RSS feed. + * CF source: cloudflare-docs/src/pages/changelog/rss/[product].xml.ts + */ +import rss from "@astrojs/rss"; +import { getCollection } from "astro:content"; +import { config } from "virtual:nimbus/config"; +import { getChangelogs, getRSSItems } from "~/util/changelog"; + +import type { + APIRoute, + InferGetStaticPropsType, + InferGetStaticParamsType, + GetStaticPaths, +} from "astro"; + +export const prerender = true; + +export const getStaticPaths = (async () => { + const directory = await getCollection("directory"); + + return directory.map((entry) => { + return { + params: { product: entry.id }, + props: { product: entry }, + }; + }); +}) satisfies GetStaticPaths; + +type Props = InferGetStaticPropsType<typeof getStaticPaths>; +type Params = InferGetStaticParamsType<typeof getStaticPaths>; + +export const GET: APIRoute<Props, Params> = async ({ + params, + props, + locals, +}) => { + const { data } = props.product; + + const notes = await getChangelogs({ + filter: (e) => { + return e.data.products.some(({ id }) => id === params.product); + }, + }); + + const items = await getRSSItems({ notes, locals }); + + return rss({ + title: `Cloudflare changelogs | ${data.name}`, + description: `Cloudflare changelogs for ${data.name}`, + site: new URL("/changelog/", config.site).href, + items, + }); +}; diff --git a/src/nimbus/pages/changelog/rss/index.md.xml.ts b/src/nimbus/pages/changelog/rss/index.md.xml.ts new file mode 100644 index 00000000000..1735c23499d --- /dev/null +++ b/src/nimbus/pages/changelog/rss/index.md.xml.ts @@ -0,0 +1,22 @@ +import rss from "@astrojs/rss"; +import type { APIRoute } from "astro"; +import { getChangelogs, getRSSItems } from "~/util/changelog"; + +export const GET: APIRoute = async ({ locals }) => { + const notes = await getChangelogs({ + filter: (entry) => !entry.data.hidden, + }); + + const items = await getRSSItems({ + notes, + locals, + markdown: true, + }); + + return rss({ + title: "Cloudflare changelogs", + description: `Variant of the Cloudflare changelog with Markdown content rather than HTML`, + site: "https://developers.cloudflare.com/changelog/", + items, + }); +}; diff --git a/src/nimbus/pages/changelog/rss/index.xml.ts b/src/nimbus/pages/changelog/rss/index.xml.ts new file mode 100644 index 00000000000..7f7de4affae --- /dev/null +++ b/src/nimbus/pages/changelog/rss/index.xml.ts @@ -0,0 +1,25 @@ +/** + * /changelog/rss/index.xml — RSS feed of all (non-hidden) changelog entries. + * CF source: cloudflare-docs/src/pages/changelog/rss/index.xml.ts + */ +import rss from "@astrojs/rss"; +import type { APIRoute } from "astro"; +import { config } from "virtual:nimbus/config"; +import { getChangelogs, getRSSItems } from "~/util/changelog"; + +export const prerender = true; + +export const GET: APIRoute = async ({ locals }) => { + const notes = await getChangelogs({ + filter: (entry) => !entry.data.hidden, + }); + + const items = await getRSSItems({ notes, locals }); + + return rss({ + title: "Cloudflare changelogs", + description: "Updates to various Cloudflare products", + site: new URL("/changelog/", config.site).href, + items, + }); +}; diff --git a/src/nimbus/pages/directory.astro b/src/nimbus/pages/directory.astro new file mode 100644 index 00000000000..af9249f71f5 --- /dev/null +++ b/src/nimbus/pages/directory.astro @@ -0,0 +1,58 @@ +--- +/** + * /directory — filterable catalog of every documentation product folder. + * + * CF source: cloudflare-docs/src/pages/directory.astro (faithful port). The + * Starlight splash wrapper is swapped for Nimbus's BaseLayout + Header + * chrome; the data pipeline (directory collection → local icon lookup → + * grouped, sorted product list) is unchanged. + */ +import { getCollection, type CollectionEntry } from "astro:content"; +// @ts-expect-error virtual module provided by astro-icon +import iconCollection from "virtual:astro-icon"; +import { getIconData, iconToSVG } from "@iconify/utils"; +import BaseLayout from "~/layouts/BaseLayout.astro"; +import Header from "~/components/Header.astro"; +import Directory from "~/components/directory/Directory.astro"; +import type { ProductData } from "~/components/directory/grid"; + +export const prerender = true; + +const products: CollectionEntry<"directory">[] = await getCollection( + "directory", + (entry: CollectionEntry<"directory">) => { + return Boolean(entry.data.entry?.show ?? true); + }, +); + +const productData: ProductData[] = products + .map((product) => { + const iconData = getIconData(iconCollection.local, product.id); + let icon = undefined; + if (iconData) { + icon = iconToSVG(iconData); + } + + return { + ...product, + icon, + groups: [ + product.data.entry.group, + ...(product.data.entry.additional_groups || []), + ].filter((val): val is string => Boolean(val)), + }; + }) + .sort((a, b) => a.id.localeCompare(b.id)); +--- + +<BaseLayout + title="Docs directory" + description="Explore the different areas of our documentation site." +> + <div class="flex min-h-screen flex-col"> + <Header showSidebar={false} /> + <main id="main-content" transition:name="nb-content" class="flex-1"> + <Directory products={productData} /> + </main> + </div> +</BaseLayout> diff --git a/src/nimbus/pages/glossary.astro b/src/nimbus/pages/glossary.astro new file mode 100644 index 00000000000..e6b3bde8519 --- /dev/null +++ b/src/nimbus/pages/glossary.astro @@ -0,0 +1,43 @@ +--- +/** + * /glossary/ — full glossary catalog. + * + * CF source: src/pages/glossary.astro (StarlightPage `template: "splash"`). + * Nimbus redesign: shared splash shell + directory/changelog-style blueprint + * framing and centered hero. Glossary behavior remains the original search + + * table + view-more flow. + */ +import SplashLayout from "~/layouts/SplashLayout.astro"; +import BackgroundLines from "~/components/BackgroundLines.astro"; +import { Glossary as GlossaryComponent } from "~/components"; +import { Badge } from "~/components"; + +export const prerender = true; +--- + +<SplashLayout + title="Glossary" + description="Search Cloudflare terms and definitions across products, security concepts, and developer platform primitives." +> + <div class="relative"> + <BackgroundLines inner={1120} outer={1360} /> + <div + class="relative z-10 mx-auto w-full max-w-[1120px] px-[max(1.5rem,4vw)] pt-14 pb-24" + > + <header class="mx-auto max-w-2xl text-center"> + <h1 + class="text-[clamp(2.75rem,5vw,3.5rem)] leading-[1.05] font-medium text-balance text-foreground" + > + Glossary + </h1> + <div class="mb-5 flex items-center justify-center gap-2"> + <Badge text="Beta" variant="caution" size="medium" /> + </div> + </header> + + <section class="mt-12 pt-10" aria-label="Glossary terms"> + <GlossaryComponent /> + </section> + </div> + </div> +</SplashLayout> diff --git a/src/nimbus/pages/index.astro b/src/nimbus/pages/index.astro new file mode 100644 index 00000000000..3d1a423e4ae --- /dev/null +++ b/src/nimbus/pages/index.astro @@ -0,0 +1,205 @@ +--- +import { Icon } from "astro-icon/components"; +import BaseLayout from "../layouts/BaseLayout.astro"; +import Header from "../components/Header.astro"; +import { SidebarFilter } from "@/components/ui/sidebar"; +import Sidebar from "../components/landing/Sidebar.astro"; +import Hero from "../components/landing/Hero.astro"; +import BuildFromScratch from "../components/landing/BuildFromScratch.astro"; +import AgentSetup from "../components/landing/AgentSetup.astro"; +import ChangelogSection from "../components/landing/ChangelogSection.astro"; +import SecureSection from "../components/landing/SecureSection.astro"; +import AccelerateSection from "../components/landing/AccelerateSection.astro"; +import CommunityGrid from "../components/landing/CommunityGrid.astro"; +import StructuralGrid from "../components/landing/StructuralGrid.astro"; +import FullPageLines from "../components/landing/FullPageLines.astro"; +import { config } from "virtual:nimbus/config"; + +const currentPath = Astro.url.pathname; +--- + +<BaseLayout + title="Cloudflare Developer Docs" + description="Connect, protect, and build everywhere." +> + <div class="flex flex-col h-full"> + <Header showSidebar /> + + <div + class="flex flex-col w-full overflow-x-clip bg-background text-foreground" + > + <div class="flex"> + <aside + data-nb-desktop-sidebar + class="hidden lg:flex w-(--nb-sidebar-width) shrink-0 border-r border-border sticky top-14 h-[calc(100vh-3.5rem)] flex-col overflow-y-auto" + > + <nav class="flex-1 px-4 pt-5 pb-12" aria-label="Documentation"> + <SidebarFilter placeholder="Search products..." /> + <Sidebar currentPath={currentPath} /> + </nav> + <div + class="pointer-events-none sticky bottom-0 h-8 bg-gradient-to-t from-background to-transparent" + > + </div> + </aside> + + <main + id="main-content" + transition:name="nb-content" + class="flex-1 min-w-0" + > + <div class="relative"> + <FullPageLines /> + <StructuralGrid /> + <div + class="relative z-10 mx-auto max-w-[1280px] px-[max(2rem,4vw)] pt-16 min-[800px]:pt-[max(2rem,4vw)]" + > + <Hero /> + <BuildFromScratch /> + <AgentSetup /> + <ChangelogSection /> + <SecureSection /> + <AccelerateSection /> + <CommunityGrid /> + </div> + </div> + </main> + </div> + </div> + </div> + + <dialog + data-mobile-sidebar + data-state="closed" + class="group fixed inset-0 m-0 h-full w-full max-h-full max-w-full border-0 bg-transparent p-0 data-[state=open]:bg-black/40 data-[state=closing]:bg-black/40" + aria-label="Site navigation" + > + <div + data-mobile-sidebar-panel + class="h-full w-(--nb-sidebar-width) max-w-[85vw] -translate-x-full overflow-y-auto bg-background shadow-lg transition-transform duration-250 ease-out group-data-[state=open]:translate-x-0 motion-reduce:transition-none" + > + <div + class="sticky top-0 z-[1] flex items-center justify-between border-b border-border bg-background px-4 py-3" + > + <span class="font-semibold text-sm text-foreground">{config.title}</span> + <button + data-close-sidebar + class="flex items-center justify-center w-8 h-8 rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors" + aria-label="Close sidebar" + > + <Icon name="ph:x" class="w-5 h-5" /> + </button> + </div> + <nav class="px-4 pt-5 pb-8" aria-label="Documentation"> + <SidebarFilter placeholder="Search products..." /> + <Sidebar currentPath={currentPath} /> + </nav> + </div> + </dialog> + + <script> + import { lockScroll, unlockScroll } from "nimbus-docs/client"; + + const dialog = document.querySelector<HTMLDialogElement>("[data-mobile-sidebar]"); + const menuBtn = document.querySelector<HTMLElement>("[data-menu-btn]"); + const CLOSE_DURATION_MS = 250; + + if (dialog && menuBtn) { + let closeTimer: ReturnType<typeof setTimeout> | undefined; + + const openSidebar = () => { + if (closeTimer) { + clearTimeout(closeTimer); + closeTimer = undefined; + } + if (dialog.open) return; + + dialog.showModal(); + dialog.dataset.state = "closed"; + lockScroll(); + + requestAnimationFrame(() => { + requestAnimationFrame(() => { + dialog.dataset.state = "open"; + }); + }); + + dialog.querySelector<HTMLElement>("[data-close-sidebar]")?.focus(); + }; + + const closeSidebar = () => { + if (!dialog.open || dialog.dataset.state === "closing") return; + + dialog.dataset.state = "closing"; + closeTimer = setTimeout(() => { + dialog.close(); + }, CLOSE_DURATION_MS); + }; + + menuBtn.addEventListener("click", openSidebar); + + dialog.addEventListener("cancel", (event) => { + event.preventDefault(); + closeSidebar(); + }); + + dialog.addEventListener("close", () => { + if (closeTimer) { + clearTimeout(closeTimer); + closeTimer = undefined; + } + dialog.dataset.state = "closed"; + unlockScroll(); + menuBtn.focus(); + }); + + dialog.querySelector("[data-close-sidebar]") + ?.addEventListener("click", closeSidebar); + + dialog.addEventListener("click", (e) => { + if (e.target === dialog) closeSidebar(); + }); + + // The drawer is a mobile-only affordance (the burger is `lg:hidden` and + // the sidebar lives inline on desktop). If it's open when the viewport + // grows to desktop, close it so it doesn't linger over the page. + const desktopMq = window.matchMedia("(min-width: 1024px)"); + desktopMq.addEventListener("change", (e) => { + if (e.matches && dialog.open) dialog.close(); + }); + } + </script> + + <script> + // Instant jump to a hash target on load; in-page clicks stay smooth. + { + const hash = window.location.hash; + if (hash.length > 1) { + let target: Element | null = null; + let id = hash.slice(1); + try { + id = decodeURIComponent(id); + } catch {} + target = document.getElementById(id); + if (!target) { + try { + target = document.querySelector(hash); + } catch { + target = null; + } + } + if (target) { + const html = document.documentElement; + const prev = html.style.scrollBehavior; + html.style.scrollBehavior = "auto"; + requestAnimationFrame(() => { + target!.scrollIntoView(); + requestAnimationFrame(() => { + html.style.scrollBehavior = prev; + }); + }); + } + } + } + </script> +</BaseLayout> diff --git a/src/nimbus/pages/llms.txt.ts b/src/nimbus/pages/llms.txt.ts new file mode 100644 index 00000000000..d9682427311 --- /dev/null +++ b/src/nimbus/pages/llms.txt.ts @@ -0,0 +1,48 @@ +// Root /llms.txt — sectioned index for AI agents. +import { getIndexedTopLevel } from "nimbus-docs"; +import { config } from "virtual:nimbus/config"; + +export const prerender = true; + +export async function GET() { + const { leaves, groups } = await getIndexedTopLevel(); + + const lines = [ + `# ${config.title}`, + "", + config.description ?? "Documentation index for AI agents.", + "", + "## Pages", + "", + ]; + + // Sort leaves + groups alphabetically into a single stable list. + type Row = { key: string; line: string }; + const rows: Row[] = []; + + for (const leaf of leaves) { + const description = leaf.description ? ` — ${leaf.description}` : ""; + rows.push({ + key: leaf.url, + line: `- [${leaf.title}](${new URL(leaf.markdownUrl, config.site).href})${description}`, + }); + } + + for (const group of groups) { + // Older doc versions have their own /<v>/llms.txt; don't list them here. + if (group.kind === "version") continue; + rows.push({ + key: `/${group.slug}`, + line: `- [${group.label}](${new URL(`/${group.slug}/llms.txt`, config.site).href})`, + }); + } + + rows.sort((a, b) => a.key.localeCompare(b.key)); + for (const row of rows) lines.push(row.line); + + lines.push(""); + + return new Response(lines.join("\n"), { + headers: { "Content-Type": "text/plain; charset=utf-8" }, + }); +} diff --git a/src/nimbus/pages/pages/platform/build-configuration.json.ts b/src/nimbus/pages/pages/platform/build-configuration.json.ts new file mode 100644 index 00000000000..27902ed7e98 --- /dev/null +++ b/src/nimbus/pages/pages/platform/build-configuration.json.ts @@ -0,0 +1,15 @@ +import { getEntry } from "astro:content"; + +export async function GET() { + const entries = await getEntry("pages-framework-presets", "index"); + + if (!entries) { + throw new Error("Failed to load data"); + } + + const sorted = Object.fromEntries( + Object.entries(entries.data.build_configs).sort(), + ); + + return Response.json(sorted); +} diff --git a/src/nimbus/pages/pages/platform/language-support-and-tools.json.ts b/src/nimbus/pages/pages/platform/language-support-and-tools.json.ts new file mode 100644 index 00000000000..155adcacfb6 --- /dev/null +++ b/src/nimbus/pages/pages/platform/language-support-and-tools.json.ts @@ -0,0 +1,16 @@ +import { getCollection } from "astro:content"; + +export async function GET() { + const entries = await getCollection("pages-build-environment"); + + const data = entries.flatMap((x) => { + x.data.enable_date = new Date(x.data.enable_date).toISOString(); + + return { + ...x.data, + status: x.data.status ?? null, + }; + }); + + return Response.json(data); +} diff --git a/src/nimbus/pages/plans.astro b/src/nimbus/pages/plans.astro new file mode 100644 index 00000000000..424ad73e00f --- /dev/null +++ b/src/nimbus/pages/plans.astro @@ -0,0 +1,140 @@ +--- +// CF source: src/pages/plans.astro (StarlightPage `template: "splash"`). +// Ported to SplashLayout; body unchanged. +import SplashLayout from "~/layouts/SplashLayout.astro"; +import { ProductFeatures } from "~/components"; + +export const prerender = true; +--- + +<SplashLayout + title="Plans" + description="View the products and features available to you based on your Cloudflare plan." +> + <div class="mx-auto w-full max-w-(--nb-content-max) px-[max(1.5rem,4vw)] py-14"> + <p> + A feature-level companion to our <a href="https://www.cloudflare.com/plans/" + >Plans and Pricing page</a + > + </p> + <p style="font-size:.85em;margin-top:-.25em;"> + (may not include <a href="#missing-features">all features</a>) + </p> + + <h2 id="account">Account</h2> + <div class="grid grid-cols-2 *:mt-0!"> + <ProductFeatures id="account" additional_descriptions /> + </div> + + <h2 id="analytics">Analytics</h2> + <div class="grid grid-cols-2 *:mt-0!"> + <ProductFeatures id="analytics" additional_descriptions /> + </div> + + <h2 id="cache">Cache</h2> + <div class="grid grid-cols-2 *:mt-0!"> + <ProductFeatures id="cache" additional_descriptions /> + </div> + + <h2 id="dns">DNS</h2> + <div class="grid grid-cols-2 *:mt-0!"> + <ProductFeatures id="dns" additional_descriptions /> + </div> + + <h2 id="email">Email</h2> + <div class="grid grid-cols-2 *:mt-0!"> + <ProductFeatures id="email" additional_descriptions /> + </div> + + <h2 id="global_configurations">Global configurations</h2> + <div class="grid grid-cols-2 *:mt-0!"> + <ProductFeatures id="global_configurations" additional_descriptions /> + </div> + + <h2 id="network">Network</h2> + <div class="grid grid-cols-2 *:mt-0!"> + <ProductFeatures id="network" additional_descriptions /> + </div> + + <h2 id="rules">Rules</h2> + <div class="grid grid-cols-2 *:mt-0!"> + <ProductFeatures id="rules" additional_descriptions /> + </div> + + <h2 id="scrapeshield">Scrape Shield</h2> + <div class="grid grid-cols-2 *:mt-0!"> + <ProductFeatures id="scrape_shield" additional_descriptions /> + </div> + + <h2 id="security">Security</h2> + <div class="grid grid-cols-2 *:mt-0!"> + <ProductFeatures id="security" additional_descriptions /> + </div> + + <h2 id="spectrum">Spectrum</h2> + <div class="grid grid-cols-2 *:mt-0!"> + <ProductFeatures id="spectrum" additional_descriptions /> + </div> + + <h2 id="speed">Speed</h2> + <div class="grid grid-cols-2 *:mt-0!"> + <ProductFeatures id="speed" additional_descriptions /> + </div> + + <h2 id="ssl">SSL/TLS</h2> + <div class="grid grid-cols-2 *:mt-0!"> + <ProductFeatures id="ssl" additional_descriptions /> + </div> + + <h2 id="support">Support</h2> + <div class="grid grid-cols-2 *:mt-0!"> + <ProductFeatures id="support" additional_descriptions /> + </div> + + <h2 id="traffic">Traffic</h2> + <div class="grid grid-cols-2 *:mt-0!"> + <ProductFeatures id="traffic" additional_descriptions /> + </div> + + <h2 id="web3">Web3</h2> + <div class="grid grid-cols-2 *:mt-0!"> + <ProductFeatures id="web3" additional_descriptions /> + </div> + + <h2 id="zaraz">Zaraz</h2> + <div class="grid grid-cols-2 *:mt-0!"> + <ProductFeatures id="zaraz" additional_descriptions /> + </div> + + <h2 id="missing-features">Missing features</h2> + + <p> + This page is meant to be a companion to our <a + href="https://www.cloudflare.com/plans/">Plans and Pricing page</a + >, but is not meant to be comprehensive. In the event of any inconsistency + between this page and the Plans and Pricing page, the Plans and Pricing page + controls. + </p> + <p>Refer to the following locations for more information:</p> + <ul> + <li> + <strong>Developer products</strong>: Refer to the <a + href="https://www.cloudflare.com/plans/developer-platform/" + >Developer Platform pricing page</a + > or individual <a href="/directory/">product docs</a> to learn more about plans + and pricing. + </li> + <li> + <strong>Zero Trust products</strong>: Refer to the <a + href="https://www.cloudflare.com/plans/zero-trust-services/" + >Zero Trust pricing</a + > page. + </li> + <li> + <strong>Other products</strong>: Refer to the individual <a + href="/directory/">product docs</a + > to learn more about plans and pricing. + </li> + </ul> + </div> +</SplashLayout> diff --git a/src/nimbus/pages/resources/index.astro b/src/nimbus/pages/resources/index.astro new file mode 100644 index 00000000000..165b063436e --- /dev/null +++ b/src/nimbus/pages/resources/index.astro @@ -0,0 +1,66 @@ +--- +/** + * /resources — filterable catalog aggregating docs + videos + learning-paths. + * + * + * NOTE (Phase A): learning-path cards link to `${path}/` → + * `/learning-paths/<module>/`, which 404s until Phase B copies the content + * pages. Documented, expected. + */ +import BaseLayout from "~/layouts/BaseLayout.astro"; +import Header from "~/components/Header.astro"; +import BackgroundLines from "~/components/BackgroundLines.astro"; +import ResourcesBySelector from "~/components/cf/ResourcesBySelector.astro"; + +export const prerender = true; +--- + +<BaseLayout + title="Resources" + description="Explore tutorials, videos, reference architectures, and learning paths for every Cloudflare product." +> + <div class="flex min-h-screen flex-col"> + <Header showSidebar={false} /> + <main id="main-content" transition:name="nb-content" class="flex-1"> + <div class="relative"> + <BackgroundLines inner={1120} outer={1360} /> + <div + class="relative z-10 mx-auto w-full max-w-[1120px] px-[max(1.5rem,4vw)] pt-14 pb-24" + > + <header class="mx-auto max-w-2xl text-center"> + <h1 + class="text-[clamp(2.75rem,5vw,3.5rem)] leading-[1.05] font-medium text-balance text-foreground" + > + Resources + </h1> + <p + class="mx-auto mt-4 max-w-md text-base leading-relaxed text-pretty text-muted-foreground sm:text-lg" + > + Tutorials, videos, reference architectures, and learning paths for + building on Cloudflare. + </p> + </header> + + <div class="mt-12 pt-10"> + <ResourcesBySelector + directory="" + types={[ + "tutorial", + "video", + "reference-architecture", + "reference-architecture-diagram", + "implementation-guide", + "design-guide", + "learning-path", + "solution-guide", + ]} + filterables={["pcx_content_type", "products"]} + filterPlacement="left" + columns={1} + /> + </div> + </div> + </div> + </main> + </div> +</BaseLayout> diff --git a/src/nimbus/pages/robots.txt.ts b/src/nimbus/pages/robots.txt.ts new file mode 100644 index 00000000000..3659d962419 --- /dev/null +++ b/src/nimbus/pages/robots.txt.ts @@ -0,0 +1,17 @@ +import { config } from "virtual:nimbus/config"; + +export const prerender = true; + +export function GET() { + const body = [ + "User-agent: *", + "Allow: /", + "", + `Sitemap: ${new URL("/sitemap-index.xml", config.site).href}`, + "", + ].join("\n"); + + return new Response(body, { + headers: { "Content-Type": "text/plain; charset=utf-8" }, + }); +} diff --git a/src/nimbus/pages/ruleset-engine/rules-language/fields/reference/[name].astro b/src/nimbus/pages/ruleset-engine/rules-language/fields/reference/[name].astro new file mode 100644 index 00000000000..f2965f8d98f --- /dev/null +++ b/src/nimbus/pages/ruleset-engine/rules-language/fields/reference/[name].astro @@ -0,0 +1,148 @@ +--- +/** + * /ruleset-engine/rules-language/fields/reference/<name>/ — one page per + * rules-language field. CF source: the same path's `[name].astro` + * (StarlightPage `hideTitle` → DocsLayout with an empty `page-title` slot so + * the body's custom `<h1>` stands in). Body unchanged. + */ +import type { GetStaticPaths } from "astro"; +import { marked } from "marked"; +import { getEntry } from "astro:content"; +import { getSidebar, getBreadcrumbs } from "nimbus-docs"; +import DocsLayout from "~/layouts/DocsLayout.astro"; +import { externalAppLinksTransform } from "~/util/sidebar"; +import { Code, Aside, Type } from "~/components"; +import FieldBadges from "~/components/fields/FieldBadges.tsx"; + +export const prerender = true; + +export const getStaticPaths = (async () => { + const fields = await getEntry("fields", "index"); + + if (!fields) { + throw new Error("Failed to load fields"); + } + + return fields.data.entries.map((entry) => { + return { + params: { + name: entry.name, + }, + props: { field: entry }, + }; + }); +}) satisfies GetStaticPaths; + +const { name } = Astro.params; +const { field } = Astro.props; + +// disable conversion of relative links to absolute links +marked.use({ walkTokens: null }); + +const description = field.description; + +const sectionSlug = `/ruleset-engine/rules-language/fields/reference/${field.name}/`; +const sidebar = await getSidebar(sectionSlug, { collection: "docs", transform: externalAppLinksTransform }); +const breadcrumbs = await getBreadcrumbs(sectionSlug); +const editUrl = + "https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/fields/index.yaml"; +--- + +<DocsLayout + title={field.name} + description={field.summary} + sidebar={sidebar} + headings={false} + breadcrumbs={breadcrumbs} + prevNext={{}} + collection="docs" + editUrl={editUrl} +> + <Fragment slot="page-title" /> + <Fragment slot="pagination" /> + <div class="align-center flex"> + <div> + <h1 + id="_top" + class="-mt-4! mb-2! flex items-center text-2xl! leading-none! font-bold!" + > + {name} + </h1> + </div> + </div> + + <code>{field.name}</code> + <Type text={field.data_type} /> + + <p class="mt-3" set:html={marked.parseInline(field.summary)} /> + + { + description && ( + <> + <div set:html={marked.parse(description)} /> + </> + ) + } + + { + field.example_value && ( + <> + <p id="example-value">Example value:</p> + <Code code={field.example_value} lang="txt" /> + </> + ) + } + + { + field.example_block && ( + <> + <p id="example-block">Example usage:</p> + <Code code={field.example_block} lang="txt" /> + </> + ) + } + + { + field.name.startsWith("http.request.body") && + !(field.name === "http.request.body.size") && ( + <Aside type="caution"> + <p> + All <code>http.request.body.*</code> fields (except{" "} + <a href="/ruleset-engine/rules-language/fields/reference/http.request.body.size/"> + {" "} + <code>http.request.body.size</code> + </a> + ) handle a given maximum body size, which varies per plan. For + Enterprise customers, the maximum body size is 128 KB. For other + paid plans, the limit is lower by default — reach out to your + account team or to Cloudflare Support to increase the limit. For + users in the Free plan, the limit is 1 MB. + </p> + <p> + You cannot define expressions that rely on request body data beyond + the maximum size set for your plan. If the request body is larger, + the body fields will contain a truncated value and the{" "} + <a href="/ruleset-engine/rules-language/fields/reference/http.request.body.truncated/"> + <code>http.request.body.truncated</code> + </a>{" "} + field will be set to <code>true</code>. The{" "} + <a href="/ruleset-engine/rules-language/fields/reference/http.request.body.size/"> + <code>http.request.body.size</code> + </a>{" "} + field will contain the full size of the request without any + truncation. + </p> + <p> + The maximum body size applies only to the values of HTTP body fields + — the origin server will still receive the complete request body. + </p> + </Aside> + ) + } + + <div class="mt-8!"> + <span class="text-xs" + >Categories: <FieldBadges badges={field.categories as string[]} /></span + > + </div> +</DocsLayout> diff --git a/src/nimbus/pages/ruleset-engine/rules-language/fields/reference/index.astro b/src/nimbus/pages/ruleset-engine/rules-language/fields/reference/index.astro new file mode 100644 index 00000000000..5476d783d93 --- /dev/null +++ b/src/nimbus/pages/ruleset-engine/rules-language/fields/reference/index.astro @@ -0,0 +1,37 @@ +--- +/** + * /ruleset-engine/rules-language/fields/reference/ — the rules-language field + * catalog. Same generator pattern as ai/models: an explicit page (DocsLayout + + * client island) that shadows the content route; data read from the `fields` + * collection. CF source: cloudflare-docs/src/pages/.../reference/index.astro + * (StarlightPage → DocsLayout). + */ +import { getEntry } from "astro:content"; +import { getSidebar, getBreadcrumbs } from "nimbus-docs"; +import DocsLayout from "~/layouts/DocsLayout.astro"; +import { externalAppLinksTransform } from "~/util/sidebar"; +import FieldCatalog from "~/components/FieldCatalog.tsx"; + +export const prerender = true; + +const entry = await getEntry("fields", "index"); +if (!entry) throw new Error("Failed to load fields data"); +const fields = entry.data.entries; + +const sectionSlug = "/ruleset-engine/rules-language/fields/reference/"; +const sidebar = await getSidebar(sectionSlug, { collection: "docs", transform: externalAppLinksTransform }); +const breadcrumbs = await getBreadcrumbs(sectionSlug); +--- + +<DocsLayout + title="Fields reference" + sidebar={sidebar} + headings={false} + breadcrumbs={breadcrumbs} + prevNext={{}} + collection="docs" + wide +> + <Fragment slot="pagination" /> + <FieldCatalog fields={fields} client:load /> +</DocsLayout> diff --git a/src/nimbus/pages/sponsorships.astro b/src/nimbus/pages/sponsorships.astro new file mode 100644 index 00000000000..f7d7cba6d4e --- /dev/null +++ b/src/nimbus/pages/sponsorships.astro @@ -0,0 +1,1666 @@ +--- +// CF source: src/pages/sponsorships.astro (StarlightPage `template: "splash"`). +// Ported to SplashLayout; body unchanged. +import SplashLayout from "~/layouts/SplashLayout.astro"; + +export const prerender = true; +--- + +<SplashLayout title="Sponsorships"> + <div class="mx-auto w-full max-w-(--nb-content-max) px-[max(1.5rem,4vw)] py-14"> + <p> + Cloudflare powers over 20 million Internet properties, including these + world-changing open-source projects. + </p> + <div class="sponsorship-grid"> + <div class="sponsorship"> + <a + class="sponsorship-icon" + target="_blank" + rel="noopener noreferrer" + href="https://yarnpkg.com/en/" + > + <svg viewBox="0 0 1154.8 518" xmlns="http://www.w3.org/2000/svg" + ><g fill="#2c8ebb" + ><path + d="m718.6 257.8c-8 27.6-20.8 47.6-35.2 63.6v-140.4c0-9.6-8.4-17.6-21.6-17.6-5.6 0-10.4 2.8-10.4 6.8 0 2.8 1.6 5.2 1.6 12.8v64.4c-4.8 28-16.8 54-32.8 54-11.6 0-18.4-11.6-18.4-33.2 0-33.6 4.4-51.2 11.6-80.8 1.6-6 13.2-22-6.4-22-21.2 0-18.4 8-21.2 14.8 0 0-13.4 47.6-13.4 90 0 34.8 14.6 57.6 41.4 57.6 17.2 0 29.6-11.6 39.2-27.6v50.8c-26.4 23.2-49.6 43.6-49.6 84 0 25.6 16 46 38.4 46 20.4 0 41.6-14.8 41.6-56.8v-69.2c21.6-18.8 44.8-42.4 58.4-88.8.4-1.6.4-3.6.4-4 0-7.6-7.6-16.4-14-16.4-4 0-7.2 3.6-9.6 12zm-76.8 198c-6.4 0-10.4-9.6-10.4-22 0-24 8.8-39.2 21.6-52.4v42.8c0 7.6 1.6 31.6-11.2 31.6z" + ></path><path + d="m833.4 301c-9.6 0-13.6-9.6-13.6-18.4v-66c0-9.6-8.4-17.6-21.6-17.6-5.6 0-10.4 2.8-10.4 6.8 0 2.8 1.6 5.2 1.6 12.8v61.6c-4.4 11.2-11.6 20.8-22.4 20.8-14 0-22.8-12-22.8-32.8 0-57.6 35.6-83.6 66-83.6 4 0 8 .8 11.6.8 4 0 5.2-2.4 5.2-9.2 0-10.4-7.6-16.8-18.4-16.8-48.8 0-95.2 40.8-95.2 107.6 0 34 16.4 60.4 47.6 60.4 15.2 0 26.4-7.2 34.4-16.4 6 9.6 16.8 16.4 30.8 16.4 34.4 0 50.4-36 57.2-62.4.4-1.6.4-2.4.4-2.8 0-7.6-7.6-16.4-14-16.4-4 0-8 3.6-9.6 12-3.6 17.6-10.8 43.2-26.8 43.2z" + ></path><path + d="m949 327.4c34.4 0 50-36 57.2-62.4 0-.8.4-1.6.4-2.8 0-7.6-7.6-16.4-14-16.4-4 0-8 3.6-9.6 12-3.6 17.6-10.4 43.2-28.8 43.2-10.8 0-16-10.4-16-21.6 0-40 18-87.2 18-92 1.6-9.2-14.4-22.4-19.2-22.4h-20.8c-4 0-8 0-21.2-1.6-4.4-16.4-15.6-21.2-25.2-21.2-10.4 0-20 7.2-20 18.4 0 11.6 7.2 20 17.2 25.6-.4 20.4-2 53.6-6.4 69.6-3.6 13.6 17.2 28 22.4 11.2 7.2-23.2 9.6-58 10-73.6h34.8c-12.8 34.4-20 62.8-20 88.4 0 35.2 22.4 45.6 41.2 45.6z" + ></path><path + d="m984.6 309.8c0 14.8 11.2 17.6 19.2 17.6 11.6 0 11.2-9.6 11.2-17.2v-58.4c2.8-31.6 27.6-66 39.2-66 7.6 0 8.4 10.4 8.4 22.8v81.2c0 20.4 12.4 37.6 33.6 37.6 34.4 0 51.4-36 58.2-62.4.4-1.6.4-2.4.4-2.8 0-7.6-7.6-16.4-14-16.4-4 0-8 3.6-9.6 12-3.6 17.6-11.8 43.2-27.8 43.2-10.4 0-10.4-14.8-10.4-18.4v-82.8c0-18.4-6.4-40.4-33.2-40.4-19.6 0-34 17.2-44.8 39.6v-18c0-9.6-8.4-17.6-21.6-17.6-5.6 0-10.4 2.8-10.4 6.8 0 2.8 1.6 5.2 1.6 12.8zm-725.6-309.8c143 0 259 116 259 259s-116 259-259 259-259-116-259-259 116-259 259-259z" + ></path></g + ><path + d="m435.2 337.5c-1.8-14.2-13.8-24-29.2-23.8-23 .3-42.3 12.2-55.1 20.1-5 3.1-9.3 5.4-13 7.1.8-11.6.1-26.8-5.9-43.5-7.3-20-17.1-32.3-24.1-39.4 8.1-11.8 19.2-29 24.4-55.6 4.5-22.7 3.1-58-7.2-77.8-2.1-4-5.6-6.9-10-8.1-1.8-.5-5.2-1.5-11.9.4-10.1-20.9-13.6-23.1-16.3-24.9-5.6-3.6-12.2-4.4-18.4-2.1-8.3 3-15.4 11-22.1 25.2-1 2.1-1.9 4.1-2.7 6.1-12.7.9-32.7 5.5-49.6 23.8-2.1 2.3-6.2 4-10.5 5.6h.1c-8.8 3.1-12.8 10.3-17.7 23.3-6.8 18.2.2 36.1 7.1 47.7-9.4 8.4-21.9 21.8-28.5 37.5-8.2 19.4-9.1 38.4-8.8 48.7-7 7.4-17.8 21.3-19 36.9-1.6 21.8 6.3 36.6 9.8 42 1 1.6 2.1 2.9 3.3 4.2-.4 2.7-.5 5.6.1 8.6 1.3 7 5.7 12.7 12.4 16.3 13.2 7 31.6 10 45.8 2.9 5.1 5.4 14.4 10.6 31.3 10.6h1c4.3 0 58.9-2.9 74.8-6.8 7.1-1.7 12-4.7 15.2-7.4 10.2-3.2 38.4-12.8 65-30 18.8-12.2 25.3-14.8 39.3-18.2 13.6-3.3 22.1-15.7 20.4-29.4zm-23.8 14.7c-16 3.8-24.1 7.3-43.9 20.2-30.9 20-64.7 29.3-64.7 29.3s-2.8 4.2-10.9 6.1c-14 3.4-66.7 6.3-71.5 6.4-12.9.1-20.8-3.3-23-8.6-6.7-16 9.6-23 9.6-23s-3.6-2.2-5.7-4.2c-1.9-1.9-3.9-5.7-4.5-4.3-2.5 6.1-3.8 21-10.5 27.7-9.2 9.3-26.6 6.2-36.9.8-11.3-6 .8-20.1.8-20.1s-6.1 3.6-11-3.8c-4.4-6.8-8.5-18.4-7.4-32.7 1.2-16.3 19.4-32.1 19.4-32.1s-3.2-24.1 7.3-48.8c9.5-22.5 35.1-40.6 35.1-40.6s-21.5-23.8-13.5-45.2c5.2-14 7.3-13.9 9-14.5 6-2.3 11.8-4.8 16.1-9.5 21.5-23.2 48.9-18.8 48.9-18.8s13-39.5 25-31.8c3.7 2.4 17 32 17 32s14.2-8.3 15.8-5.2c8.6 16.7 9.6 48.6 5.8 68-6.4 32-22.4 49.2-28.8 60-1.5 2.5 17.2 10.4 29 43.1 10.9 29.9 1.2 55 2.9 57.8.3.5.4.7.4.7s12.5 1 37.6-14.5c13.4-8.3 29.3-17.6 47.4-17.8 17.5-.3 18.4 20.2 5.2 23.4z" + fill="#fff"></path></svg + > + </a> + <a + class="sponsorship-title" + target="_blank" + rel="noopener noreferrer" + href="https://yarnpkg.com/en/" + > + <h3>Yarn</h3> + </a> + <div class="sponsorship-description"> + <p> + A package manager for Node that creates a lockfile for dependencies + and cache’s packages for future projects. + </p> + </div> + <div class="sponsorship-links"> + <a + class="sponsorship-link-site more" + target="_blank" + rel="noopener noreferrer" + href="https://yarnpkg.com/en/">Visit site</a + > + <a + class="sponsorship-link-github more" + target="_blank" + rel="noopener noreferrer" + href="https://github.com/yarnpkg/yarn">Code on GitHub</a + > + </div> + </div> + + <div class="sponsorship"> + <a + class="sponsorship-icon" + target="_blank" + rel="noopener noreferrer" + href="https://momentjs.com/" + > + <svg + height="2500" + preserveAspectRatio="xMidYMid" + viewBox="0 0 256 256" + width="2500" + xmlns="http://www.w3.org/2000/svg" + ><g fill-rule="evenodd" + ><path + d="m128 256c70.692 0 128-57.308 128-128s-57.308-128-128-128-128 57.308-128 128 57.308 128 128 128zm0-19.2c-60.089 0-108.8-48.711-108.8-108.8s48.711-108.8 108.8-108.8 108.8 48.711 108.8 108.8-48.711 108.8-108.8 108.8z" + fill="#376660"></path><path + d="m128 230.4c56.554 0 102.4-45.846 102.4-102.4s-45.846-102.4-102.4-102.4-102.4 45.846-102.4 102.4 45.846 102.4 102.4 102.4zm0-198.4c-3.535 0-6.4 2.88-6.4 6.444v83.156h-44.819a6.375 6.375 0 0 0 -6.381 6.4c0 3.535 2.916 6.4 6.37 6.4h57.63v-95.994a6.387 6.387 0 0 0 -6.4-6.406z" + fill="#529990"></path></g + ></svg + > + </a> + <a + class="sponsorship-title" + target="_blank" + rel="noopener noreferrer" + href="https://momentjs.com/" + > + <h3>Moment.js</h3> + </a> + <div class="sponsorship-description"> + <p> + A library for Parsing, validating, manipulating, and displaying dates + and times in JavaScript. + </p> + </div> + <div class="sponsorship-links"> + <a + class="sponsorship-link-site more" + target="_blank" + rel="noopener noreferrer" + href="https://momentjs.com/">Visit site</a + > + <a + class="sponsorship-link-github more" + target="_blank" + rel="noopener noreferrer" + href="https://github.com/moment/moment/">Code on GitHub</a + > + </div> + </div> + + <div class="sponsorship"> + <a + class="sponsorship-icon" + target="_blank" + rel="noopener noreferrer" + href="https://d3js.org/" + > + <svg + height="243" + preserveAspectRatio="xMidYMid" + viewBox="0 0 256 243" + width="256" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + ><linearGradient id="a" + ><stop offset="0" stop-color="#f9a03c"></stop><stop + offset="1" + stop-color="#f7974e"></stop></linearGradient + ><linearGradient + id="b" + x1="-82.637%" + x2="103.767%" + xlink:href="#a" + y1="-92.82%" + y2="106.042%"></linearGradient><linearGradient + id="c" + x1="-258.924%" + x2="97.62%" + xlink:href="#a" + y1="-248.97%" + y2="98.768%"></linearGradient><linearGradient + id="d" + x1="-223.163%" + x2="94.028%" + xlink:href="#a" + y1="-261.968%" + y2="101.691%"></linearGradient><linearGradient + id="e" + x1="11.339%" + x2="82.496%" + y1="-1.822%" + y2="92.107%" + ><stop offset="0" stop-color="#f26d58"></stop><stop + offset="1" + stop-color="#f9a03c"></stop></linearGradient + ><linearGradient + id="f" + x1="15.844%" + x2="120.126%" + y1="3.858%" + y2="72.38%" + ><stop offset="0" stop-color="#b84e51"></stop><stop + offset="1" + stop-color="#f68e48"></stop></linearGradient + ><linearGradient + id="g" + x1="46.984%" + x2="51.881%" + xlink:href="#a" + y1="23.466%" + y2="147.391%"></linearGradient><path + d="m255.52 175.619c.115-1.115.197-2.24.261-3.371.078-1.339-80.562-77.85-80.562-77.85h-1.928s81.736 86.215 82.229 81.22z" + fill="url(#b)"></path><path + d="m83.472 149.077c-.107.235-.213.47-.323.704-.114.246-.232.491-.349.734-2.57 5.36 35.987 43.053 39.088 38.474.141-.202.283-.416.424-.618.157-.24.312-.47.467-.71 2.48-3.765-38.206-41.032-39.307-38.584z" + fill="url(#c)"></path><path + d="m137.957 202.083c-.109.24-.885 1.552-1.594 2.245-.12.24 37.64 37.688 37.64 37.688h3.4c.002 0-35.446-38.35-39.446-39.933z" + fill="url(#d)"></path><path + d="m255.835 171.568c-1.766 39.147-34.152 70.448-73.72 70.448h-5.35l-39.514-38.928c3.25-4.584 6.272-9.333 8.962-14.285h35.902c11.354 0 20.594-9.235 20.594-20.595 0-11.355-9.24-20.595-20.594-20.595h-21.246c1.619-8.557 2.504-17.381 2.504-26.408 0-9.165-.901-18.114-2.578-26.808h13.197l81.61 80.414c.097-1.078.174-2.155.233-3.243zm-234.368-171.568h-21.467v53.213h21.467c37.493 0 68 30.499 68 67.992 0 10.2-2.275 19.883-6.318 28.576l39.163 38.59c12.859-19.24 20.376-42.339 20.376-67.166 0-66.832-54.381-121.205-121.221-121.205z" + fill="url(#e)"></path><path + d="m182.115 0h-86.928c21.232 12.963 38.813 31.344 50.792 53.213h36.136c11.354 0 20.594 9.235 20.594 20.595 0 11.357-9.24 20.592-20.594 20.592h-8.12l81.61 80.413c.192-2.181.312-4.376.312-6.605 0-17.939-6.437-34.395-17.125-47.203 10.688-12.802 17.125-29.261 17.125-47.197 0-40.696-33.104-73.808-73.802-73.808z" + fill="url(#f)"></path><path + d="m176.765 242.016h-80.957c16.296-10.064 30.384-23.35 41.443-38.928zm-54.453-53.645-39.16-38.59c-10.819 23.251-34.395 39.422-61.685 39.422h-21.467v53.208h21.467c42.01 0 79.09-21.488 100.845-54.04z" + fill="url(#g)"></path></svg + > + </a> + <a + class="sponsorship-title" + target="_blank" + rel="noopener noreferrer" + href="https://d3js.org/" + > + <h3>D3</h3> + </a> + <div class="sponsorship-description"> + <p> + A way to bind data to the DOM, and then apply data-driven + transformations to the document. + </p> + </div> + <div class="sponsorship-links"> + <a + class="sponsorship-link-site more" + target="_blank" + rel="noopener noreferrer" + href="https://d3js.org/">Visit site</a + > + <a + class="sponsorship-link-github more" + target="_blank" + rel="noopener noreferrer" + href="https://github.com/d3/d3">Code on GitHub</a + > + </div> + </div> + + <div class="sponsorship"> + <a + class="sponsorship-icon" + target="_blank" + rel="noopener noreferrer" + href="https://unpkg.com/" + > + <svg viewBox="0 0 460 460" xmlns="http://www.w3.org/2000/svg" + ><g fill="none" fill-rule="evenodd" + ><path d="m0 0h460v460h-460z" fill="#242424"></path><path + d="m184.743653 116.940353v136.521106c-.930514 40.687423 13.629462 61.031134 43.679928 61.031134s44.482439-20.343711 43.295919-61.031134v-136.521106h64.233003v146.323403c0 71.312276-35.842974 106.968415-107.528922 106.968415s-107.528922-35.656139-107.528922-106.968415v-147.375481z" + fill="#fff"></path></g + ></svg + > + </a> + <a + class="sponsorship-title" + target="_blank" + rel="noopener noreferrer" + href="https://unpkg.com/" + > + <h3>UNPKG</h3> + </a> + <div class="sponsorship-description"> + <p>A fast, global Content Delivery Network for everything on npm.</p> + </div> + <div class="sponsorship-links"> + <a + class="sponsorship-link-site more" + target="_blank" + rel="noopener noreferrer" + href="https://unpkg.com/">Visit site</a + > + <a + class="sponsorship-link-github more" + target="_blank" + rel="noopener noreferrer" + href="https://github.com/unpkg/unpkg">Code on GitHub</a + > + </div> + </div> + + <div class="sponsorship"> + <a + class="sponsorship-icon" + target="_blank" + rel="noopener noreferrer" + href="https://html5boilerplate.com/" + > + <svg + height="2500" + preserveAspectRatio="xMinYMin meet" + viewBox="0 0 256 256" + width="2500" + xmlns="http://www.w3.org/2000/svg" + ><path d="m0 0h256v256h-256z" fill="#222"></path><path + d="m114.406 35.277-22.579 64.543-68.494.189 55.51 41.021-19.948 66.233 56.075-39.7 57.584 42.147-21.079-68.869 54.948-41.021-68.306.189-23.71-64.732" + fill="#e08524"></path><path + d="m117.228 34.336 22.768 10.726 19.005 51.937h-18.817l-22.956-62.659" + fill="#466770"></path><path + d="m156.34 142.239 22.486 11.044 54.42-39.518-22.486-11.975z" + fill="#304a51"></path><path + d="m156.308 142.198 22.518 11.035 21.289 68.839-23.285-11.842-20.522-68.03" + fill="#466770"></path><path + d="m62.095 209.618 52.875-37.822 18.535 13.736-49.96 35z" + fill="#304a51"></path></svg + > + </a> + <a + class="sponsorship-title" + target="_blank" + rel="noopener noreferrer" + href="https://html5boilerplate.com/" + > + <h3>HTML5 Boilerplate</h3> + </a> + <div class="sponsorship-description"> + <p>A fast, robust, and adaptable way to create web apps or sites.</p> + </div> + <div class="sponsorship-links"> + <a + class="sponsorship-link-site more" + target="_blank" + rel="noopener noreferrer" + href="https://html5boilerplate.com/">Visit site</a + > + <a + class="sponsorship-link-github more" + target="_blank" + rel="noopener noreferrer" + href="https://github.com/h5bp/html5-boilerplate">Code on GitHub</a + > + </div> + </div> + + <div class="sponsorship"> + <a + class="sponsorship-icon" + target="_blank" + rel="noopener noreferrer" + href="https://cdnjs.com/" + > + <svg + enable-background="new 0 0 540 210" + viewBox="0 0 540 210" + xmlns="http://www.w3.org/2000/svg" + ><g fill="#dd4814" + ><path + d="m103.308 59.526-11.337 19.814c-6.217-5.862-14.553-8.794-25.006-8.794-10.031 0-17.96 3.338-23.787 10.013-5.828 6.676-8.742 15.912-8.742 27.708 0 23.806 11.337 35.708 34.012 35.708 9.817 0 18.471-3.249 25.959-9.748l9.748 20.873c-7.7 4.805-14.428 7.841-20.185 9.112-5.758 1.271-12.556 1.907-20.396 1.907-17.52 0-31.347-5.103-41.482-15.311-10.137-10.207-15.205-24.387-15.205-42.542 0-17.87 5.544-32.317 16.635-43.336 11.089-11.02 26.206-16.529 45.35-16.529 13.208 0 24.688 3.708 34.436 11.125z" + ></path><path + d="m198.352 164v-6.887c-2.19 2.401-5.899 4.503-11.125 6.304-5.229 1.802-10.632 2.702-16.211 2.702-15.824 0-28.274-5.015-37.35-15.046-9.078-10.029-13.616-24.016-13.616-41.959 0-17.941 5.208-32.545 15.628-43.813 10.419-11.266 23.47-16.9 39.151-16.9 8.617 0 16.458 1.767 23.522 5.298v-45.456l26.489-6.357v162.114zm0-86.355c-5.652-4.52-11.549-6.781-17.695-6.781-10.596 0-18.754 3.232-24.476 9.695-5.722 6.464-8.583 15.735-8.583 27.814 0 23.593 11.372 35.389 34.118 35.389 2.543 0 5.668-.758 9.377-2.277 3.708-1.519 6.127-3.055 7.258-4.609v-59.231z" + ></path><path + d="m324.334 164v-65.587c0-9.677-1.854-16.741-5.562-21.191-3.709-4.45-9.767-6.675-18.172-6.675-3.886 0-8.036 1.096-12.45 3.285-4.415 2.19-7.858 4.911-10.331 8.159v82.009h-26.489v-113.48h19.072l4.873 10.596c7.205-8.477 17.836-12.715 31.894-12.715 13.491 0 24.14 4.044 31.946 12.132 7.804 8.089 11.708 19.374 11.708 33.853v69.614z" + ></path><path + d="m363.856 208.502v-23.629c14.127 0 23.682-1.679 28.661-5.032 4.98-3.356 7.47-9.378 7.47-18.065v-89.429h-20.449v-21.827h46.939v110.832c0 16.246-5.104 28.166-15.312 35.76-10.207 7.592-25.977 11.39-47.309 11.39zm48.529-201.954c4.238 0 7.857 1.501 10.86 4.503 3.002 3.003 4.504 6.623 4.504 10.861s-1.502 7.859-4.504 10.861c-3.003 3.003-6.622 4.503-10.86 4.503s-7.859-1.5-10.86-4.503c-3.004-3.002-4.504-6.623-4.504-10.861s1.5-7.857 4.504-10.861c3-3.002 6.621-4.503 10.86-4.503z" + ></path><path + d="m452.012 156.689 9.431-21.086c7.91 6.288 16.847 9.43 26.808 9.43 10.312 0 15.469-3.672 15.469-11.02 0-4.307-1.554-7.84-4.662-10.596-3.108-2.754-9.148-6.002-18.118-9.748-19.567-8.121-29.351-19.496-29.351-34.118 0-9.817 3.744-17.465 11.232-22.94 7.486-5.473 17.059-8.211 28.714-8.211 11.796 0 22.887 2.649 33.271 7.947l-7.629 20.556c-5.793-4.943-13.846-7.417-24.158-7.417-9.255 0-13.881 3.674-13.881 11.02 0 2.897 1.519 5.51 4.557 7.841 3.036 2.331 9.536 5.457 19.496 9.377 9.96 3.921 17.129 8.673 21.51 14.251 4.379 5.582 6.568 12.328 6.568 20.238 0 10.526-3.903 18.808-11.708 24.847-7.806 6.04-18.42 9.06-31.84 9.06-7.56 0-13.615-.619-18.172-1.854-4.557-1.236-10.403-3.762-17.537-7.577z" + ></path></g + ></svg + > + </a> + <a + class="sponsorship-title" + target="_blank" + rel="noopener noreferrer" + href="https://cdnjs.com/" + > + <h3>cdnjs</h3> + </a> + <div class="sponsorship-description"> + <p>A free, public Content Delivery Network for popular libraries.</p> + </div> + <div class="sponsorship-links"> + <a + class="sponsorship-link-site more" + target="_blank" + rel="noopener noreferrer" + href="https://cdnjs.com/">Visit site</a + > + <a + class="sponsorship-link-github more" + target="_blank" + rel="noopener noreferrer" + href="https://github.com/cdnjs/cdnjs">Code on GitHub</a + > + </div> + </div> + + <div class="sponsorship"> + <a + class="sponsorship-icon" + target="_blank" + rel="noopener noreferrer" + href="https://webpack.js.org/" + > + <svg viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg" + ><path d="m600 0 530.3 300v600l-530.3 300-530.3-300v-600z" fill="#fff" + ></path><path + d="m1035.6 879.3-418.1 236.5v-184.2l260.5-143.3zm28.6-25.9v-494.6l-153 88.3v317.9zm-901.5 25.9 418.1 236.5v-184.2l-260.5-143.3-157.6 91zm-28.6-25.9v-494.6l153 88.3v317.9zm17.9-526.6 428.8-242.6v178.1l-274.7 151.1-2.1 1.2zm894.3 0-428.8-242.6v178.1l274.7 151.1 2.1 1.2z" + fill="#8ed6fb"></path><path + d="m580.8 889.7-257-141.3v-280l257 148.4zm36.7 0 257-141.3v-280l-257 148.4zm-276.3-453.7 258-141.9 258 141.9-258 149z" + fill="#1c78c0"></path></svg + > + </a> + <a + class="sponsorship-title" + target="_blank" + rel="noopener noreferrer" + href="https://webpack.js.org/" + > + <h3>Webpack</h3> + </a> + <div class="sponsorship-description"> + <p>A module bundler for combining JavaScript files.</p> + </div> + <div class="sponsorship-links"> + <a + class="sponsorship-link-site more" + target="_blank" + rel="noopener noreferrer" + href="https://webpack.js.org/">Visit site</a + > + <a + class="sponsorship-link-github more" + target="_blank" + rel="noopener noreferrer" + href="https://github.com/webpack/webpack">Code on GitHub</a + > + </div> + </div> + + <div class="sponsorship"> + <a + class="sponsorship-icon" + target="_blank" + rel="noopener noreferrer" + href="https://nodejs.org/" + > + <svg + width="2270" + height="2500" + viewBox="0 0 256 282" + xmlns="http://www.w3.org/2000/svg" + preserveAspectRatio="xMinYMin meet" + ><g fill="#8CC84B" + ><path + d="M116.504 3.58c6.962-3.985 16.03-4.003 22.986 0 34.995 19.774 70.001 39.517 104.99 59.303 6.581 3.707 10.983 11.031 10.916 18.614v118.968c.049 7.897-4.788 15.396-11.731 19.019-34.88 19.665-69.742 39.354-104.616 59.019-7.106 4.063-16.356 3.75-23.24-.646-10.457-6.062-20.932-12.094-31.39-18.15-2.137-1.274-4.546-2.288-6.055-4.36 1.334-1.798 3.719-2.022 5.657-2.807 4.365-1.388 8.374-3.616 12.384-5.778 1.014-.694 2.252-.428 3.224.193 8.942 5.127 17.805 10.403 26.777 15.481 1.914 1.105 3.852-.362 5.488-1.274 34.228-19.345 68.498-38.617 102.72-57.968 1.268-.61 1.969-1.956 1.866-3.345.024-39.245.006-78.497.012-117.742.145-1.576-.767-3.025-2.192-3.67-34.759-19.575-69.5-39.18-104.253-58.76a3.621 3.621 0 0 0-4.094-.006C91.2 39.257 56.465 58.88 21.712 78.454c-1.42.646-2.373 2.071-2.204 3.653.006 39.245 0 78.497 0 117.748a3.329 3.329 0 0 0 1.89 3.303c9.274 5.259 18.56 10.481 27.84 15.722 5.228 2.814 11.647 4.486 17.407 2.33 5.083-1.823 8.646-7.01 8.549-12.407.048-39.016-.024-78.038.036-117.048-.127-1.732 1.516-3.163 3.2-3 4.456-.03 8.918-.06 13.374.012 1.86-.042 3.14 1.823 2.91 3.568-.018 39.263.048 78.527-.03 117.79.012 10.464-4.287 21.85-13.966 26.97-11.924 6.177-26.662 4.867-38.442-1.056-10.198-5.09-19.93-11.097-29.947-16.55C5.368 215.886.555 208.357.604 200.466V81.497c-.073-7.74 4.504-15.197 11.29-18.85C46.768 42.966 81.636 23.27 116.504 3.58z" + ></path><path + d="M146.928 85.99c15.21-.979 31.493-.58 45.18 6.913 10.597 5.742 16.472 17.793 16.659 29.566-.296 1.588-1.956 2.464-3.472 2.355-4.413-.006-8.827.06-13.24-.03-1.872.072-2.96-1.654-3.195-3.309-1.268-5.633-4.34-11.212-9.642-13.929-8.139-4.075-17.576-3.87-26.451-3.785-6.479.344-13.446.905-18.935 4.715-4.214 2.886-5.494 8.712-3.99 13.404 1.418 3.369 5.307 4.456 8.489 5.458 18.33 4.794 37.754 4.317 55.734 10.626 7.444 2.572 14.726 7.572 17.274 15.366 3.333 10.446 1.872 22.932-5.56 31.318-6.027 6.901-14.805 10.657-23.56 12.697-11.647 2.597-23.734 2.663-35.562 1.51-11.122-1.268-22.696-4.19-31.282-11.768-7.342-6.375-10.928-16.308-10.572-25.895.085-1.619 1.697-2.748 3.248-2.615 4.444-.036 8.888-.048 13.332.006 1.775-.127 3.091 1.407 3.182 3.08.82 5.367 2.837 11 7.517 14.182 9.032 5.827 20.365 5.428 30.707 5.591 8.568-.38 18.186-.495 25.178-6.158 3.689-3.23 4.782-8.634 3.785-13.283-1.08-3.925-5.186-5.754-8.712-6.95-18.095-5.724-37.736-3.647-55.656-10.12-7.275-2.571-14.31-7.432-17.105-14.906-3.9-10.578-2.113-23.662 6.098-31.765 8.006-8.06 19.563-11.164 30.551-12.275z" + ></path></g + ></svg + > + </a> + <a + class="sponsorship-title" + target="_blank" + rel="noopener noreferrer" + href="https://nodejs.org/" + > + <h3>Node.js</h3> + </a> + <div class="sponsorship-description"> + <p>A JavaScript runtime built on Chrome’s V8 JavaScript engine.</p> + </div> + <div class="sponsorship-links"> + <a + class="sponsorship-link-site more" + target="_blank" + rel="noopener noreferrer" + href="https://nodejs.org/">Visit site</a + > + <a + class="sponsorship-link-github more" + target="_blank" + rel="noopener noreferrer" + href="https://github.com/nodejs/node">Code on GitHub</a + > + </div> + </div> + + <div class="sponsorship"> + <a + class="sponsorship-icon" + target="_blank" + rel="noopener noreferrer" + href="https://reactjs.org/" + > + <svg + enable-background="new 0 0 600 600" + viewBox="0 0 600 600" + xmlns="http://www.w3.org/2000/svg" + ><g fill="#00d8ff" + ><circle cx="299.5" cy="299.6" r="50.2"></circle><path + d="m299.5 414.6c-70.5 0-132.1-8.3-178.2-24.1-29.9-10.2-55.3-23.8-73.4-39.3-19.2-16.4-29.4-34.3-29.4-51.6 0-33.2 36.4-65.7 97.5-86.9 50-17.4 115.2-27.1 183.4-27.1 67 0 131.3 9.4 181 26.6 29.1 10 53.6 23 71 37.4 18.9 15.8 28.9 33.1 28.9 50 0 34.5-40.7 69.4-106.3 91.1-46.4 15.4-108.4 23.9-174.5 23.9zm0-205c-64.7 0-128.7 9.4-175.5 25.7-56.2 19.6-81.4 46.4-81.4 64.3 0 18.6 27.1 47.9 86.5 68.2 43.6 14.9 102.6 22.8 170.4 22.8 63.6 0 122.9-8 167-22.7 61.7-20.5 89.9-49.8 89.9-68.3 0-9.5-7.2-20.7-20.3-31.6-15.1-12.6-37.1-24.1-63.4-33.2-47.3-16.2-108.8-25.2-173.2-25.2z" + ></path><path + d="m185.6 549.8c-10.2 0-19.2-2.2-26.8-6.6-28.7-16.6-38.7-64.4-26.6-127.9 9.9-52.1 34.1-113.3 68.2-172.4 33.5-58 73.7-109 113.4-143.5 23.2-20.2 46.7-35 67.9-42.8 23.1-8.5 43.1-8.5 57.7-.1 29.9 17.2 39.8 70 25.8 137.6-9.9 48-33.5 105.9-66.5 163.2-35.2 61-73.2 110.2-109.9 142.3-23.8 20.8-48.3 36-70.7 43.9-11.7 4.2-22.7 6.3-32.5 6.3zm25.1-300.9 10.4 6c-32.3 56-56.2 116.1-65.4 164.9-11.1 58.5-.4 93.7 15 102.6 3.8 2.2 8.8 3.4 14.9 3.4 19.9 0 51.2-12.6 87.4-44.2 34.7-30.3 71-77.5 104.9-136.2 31.8-55.1 54.4-110.5 63.8-156 13.1-63.7 1.8-102.7-14.3-112-8.2-4.7-21.5-4.1-37.5 1.8-18.5 6.8-39.4 20.1-60.4 38.4-37.7 32.8-76.2 81.6-108.4 137.4z" + ></path><path + d="m413.4 550.1c-27.2 0-61.7-16.4-97.7-47.4-40.2-34.6-81.1-86.1-115.3-145.2-33.6-58-57.6-118.3-67.7-170-5.9-30.2-7-57.9-3.2-80.2 4.2-24.3 14.1-41.6 28.8-50.1 29.8-17.3 80.5.5 132.1 46.4 36.6 32.5 75 81.9 108.1 139.1 35.3 61 59 118.5 68.4 166.3 6.1 31 7.1 59.8 2.8 83.2-4.6 24.9-15 42.6-30 51.3-7.5 4.4-16.4 6.6-26.3 6.6zm-192.2-204.6c32.4 56 72.6 106.7 110.2 139 45.1 38.8 80.9 47.2 96.4 38.2 16.1-9.3 27.9-47.4 15.7-109-9-45.2-31.7-100.2-65.7-158.9-31.9-55.1-68.6-102.4-103.3-133.2-48.6-43.2-88-52.9-104.1-43.6-8.2 4.7-14.3 16.6-17.2 33.4-3.3 19.4-2.3 44.2 3.1 71.5 9.6 49.1 32.6 106.8 64.9 162.6z" + ></path></g + ></svg + > + </a> + <a + class="sponsorship-title" + target="_blank" + rel="noopener noreferrer" + href="https://reactjs.org/" + > + <h3>React</h3> + </a> + <div class="sponsorship-description"> + <p> + A JavaScript library for building user interfaces created by facebook. + </p> + </div> + <div class="sponsorship-links"> + <a + class="sponsorship-link-site more" + target="_blank" + rel="noopener noreferrer" + href="https://reactjs.org/">Visit site</a + > + <a + class="sponsorship-link-github more" + target="_blank" + rel="noopener noreferrer" + href="https://github.com/facebook/react/">Code on GitHub</a + > + </div> + </div> + + <div class="sponsorship"> + <a + class="sponsorship-icon" + target="_blank" + rel="noopener noreferrer" + href="https://git-scm.com/" + > + <svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" + ><path + d="m502.34111 278.80364-223.54302 223.53852c-12.86794 12.87712-33.74784 12.87712-46.63305 0l-46.4152-46.42448 58.88028-58.88364c13.68647 4.62092 29.3794 1.51948 40.28378-9.38732 10.97012-10.9748 14.04307-26.80288 9.30465-40.537l56.75401-56.74844c13.73383 4.73404 29.56829 1.67384 40.53842-9.31156 15.32297-15.3188 15.32297-40.15196 0-55.48356-15.3341-15.3322-40.16175-15.3322-55.50254 0-11.52454 11.53592-14.37572 28.47172-8.53182 42.6722l-52.93386 52.93048v-139.28512c3.73267-1.84996 7.25863-4.31392 10.37114-7.41756 15.32295-15.3216 15.32295-40.15196 0-55.49696-15.32296-15.3166-40.16844-15.3166-55.48025 0-15.32296 15.345-15.32296 40.17536 0 55.49696 3.78727 3.78288 8.17299 6.64472 12.85234 8.5604v140.57336c-4.67935 1.91568-9.05448 4.75356-12.85234 8.56264-11.60533 11.60168-14.39801 28.6378-8.4449 42.89232l-58.04894 58.05848-153.2840694-153.27388c-12.8743209-12.88768-12.8743209-33.768 0-46.64456l223.5540394-223.539c12.87017-12.87456 33.74338-12.87456 46.63305 0l222.49828 222.50316c12.87852 12.87876 12.87852 33.76968 0 46.64456" + fill="#f03c2e"></path></svg + > + </a> + <a + class="sponsorship-title" + target="_blank" + rel="noopener noreferrer" + href="https://git-scm.com/" + > + <h3>git</h3> + </a> + <div class="sponsorship-description"> + <p> + A version control system that allows millions people across the world + to collaborate on coding projects. + </p> + </div> + <div class="sponsorship-links"> + <a + class="sponsorship-link-site more" + target="_blank" + rel="noopener noreferrer" + href="https://git-scm.com/">Visit site</a + > + <a + class="sponsorship-link-github more" + target="_blank" + rel="noopener noreferrer" + href="https://github.com/git/git">Code on GitHub</a + > + </div> + </div> + + <div class="sponsorship"> + <a + class="sponsorship-icon" + target="_blank" + rel="noopener noreferrer" + href="https://www.kali.org/" + > + <svg + height="76.949" + viewBox="0 0 130.60352 76.948776" + width="130.6" + xmlns="http://www.w3.org/2000/svg" + ><g fill="#527d97" + ><path + d="m3.5254 0c-1.7704.0019531-3.5254 2.002-3.5254 3.5215v58.826c0 1.7559 1.755 3.5137 3.5273 3.5117h15.652v-4h-14c-.5855-.003-1.1793-.592-1.1793-1.174v-55.506c0-.5843.5938-1.182 1.1816-1.18h120.24c.59766-.00195 1.1797.5957 1.1816 1.1797v55.506c-.002.58203-.58398 1.1719-1.1816 1.1738h-100.6v4h102.25c1.5312.002 3.5274-1.7559 3.5293-3.5117v-58.828c0-1.518-1.99-3.518-3.52-3.52z" + fill-rule="evenodd"></path><path + d="m19.33 49.9v-32.87h5.09v16.298l13.519-16.298h6.237l-12.228 14.111 12.452 18.759h-5.7969l-10.549-15.402-3.6342 4.0101v11.392z" + ></path><path + d="m63.384 21.52-4.6201 14.482 9.563-.000252zm-3.0881-4.49h6.199l12.416 32.87h-6.1674l-3.0872-8.748h-12.883l-3.0872 8.748h-5.8291z" + ></path><path d="m82.704 17.03h4.8062v28.52h14.24v4.03h-19.046z" + ></path><path d="m106.09 17.03h4.8203v32.55h-4.8203z"></path><path + d="m2.4186 73.779v2.337h1.4894q.74932 0 1.1079-.28626.36317-.29053.36317-.8844 0-.59814-.36317-.88013-.3585-.286-1.1078-.286h-1.4894zm0-2.6233v1.9226h1.3745q.68036 0 1.0114-.23499.33559-.23926.33559-.72632 0-.48279-.33559-.72205-.33099-.23926-1.0114-.23926h-1.3745zm-.9286-.71h2.3721q1.0619 0 1.6366.41016.57463.41016.57463 1.1664 0 .58533-.29421.9314t-.86425.43152q.68496.13672 1.0619.57251.38156.43152.38156 1.0809 0 .85449-.6252 1.3202-.6252.4657-1.7791.4657h-2.464v-6.3788z" + ></path><path + d="m7.6299 70.446h.92712l1.7688 2.6233 1.756-2.6233h.92712l-2.256 3.3411v3.0377h-.86714v-3.0377z" + ></path><path + d="m21.333 71.031q-.93994 0-1.4954.70068-.55115.70068-.55115 1.9098 0 1.2048.55115 1.9055.55542.70068 1.4954.70068.93994 0 1.4868-.70068.55115-.70068.55115-1.9055 0-1.2091-.55115-1.9098-.54688-.70068-1.4868-.70068zm0-.70068q1.3416 0 2.1448.90149.80322.89722.80322 2.4097 0 1.5082-.80322 2.4097-.80322.89722-2.1448.89722-1.3458 0-2.1533-.89722-.80322-.89722-.80322-2.4097t.80322-2.4097q.8075-.90149 2.1533-.90149z" + ></path><path + d="m27.032 70.446h3.6658v.72632h-2.8027v1.8799h2.5293v.72632h-2.5293v3.0463h-.86304v-6.3788z" + ></path><path + d="m33.274 70.446h3.6658v.72632h-2.8027v1.8799h2.5293v.72632h-2.5293v3.0463h-.86304v-6.3788z" + ></path><path + d="m38.915 70.446h4.0332v.72632h-3.1702v1.8884h3.0377v.72632h-3.0377v2.3114h3.2471v.72632h-4.1101v-6.3788z" + ></path><path + d="m45.552 70.446h1.1621l2.8284 5.3363v-5.3363h.8374v6.3788h-1.1621l-2.8284-5.3363v5.3363h-.8374z" + ></path><path + d="m57.521 70.655v.84168q-.49133-.23498-.92712-.35034t-.84168-.11536q-.70496 0-1.0895.27344-.38025.27344-.38025.77759 0 .42297.25208.64087.25635.21362.96558.34607l.52124.10681q.96558.18372 1.4227.64941.46142.46143.46142 1.239 0 .92712-.62378 1.4056-.61951.47852-1.8201.47852-.45288 0-.96558-.10254-.50842-.10254-1.0553-.30334v-.88867q.52551.2948 1.0297.44434.50415.14954.99121.14954.73914 0 1.1407-.29053.40161-.29053.40161-.82886 0-.46997-.29053-.73486-.286-.265-.944-.397l-.52551-.10254q-.96558-.19226-1.3971-.60242t-.43152-1.1407q0-.84595.59387-1.333.59814-.48706 1.6449-.48706.44861 0 .91431.08118t.95276.24353z" + ></path><path d="m59.951 70.446h.86304v6.3788h-.86304z"></path><path + d="m65.476 76.825-2.4353-6.3788h.90149l2.0209 5.3705 2.0251-5.3705h.89722l-2.431 6.3788h-.97839z" + ></path><path + d="m70.313 70.446h4.0332v.72632h-3.1702v1.8884h3.0377v.72632h-3.0377v2.3114h3.2471v.72632h-4.1101v-6.3788z" + ></path><path + d="m84.061 70.655v.84168q-.49133-.23498-.92712-.35034t-.84168-.11536q-.70496 0-1.0895.27344-.38025.27344-.38025.77759 0 .42297.25208.64087.25635.21362.96558.34607l.52124.10681q.96558.18372 1.4227.64941.46142.46143.46142 1.239 0 .92712-.62378 1.4056-.61951.47852-1.8201.47852-.45288 0-.96558-.10254-.50842-.10254-1.0553-.30334v-.88867q.52551.2948 1.0297.44434.50415.14954.99121.14954.73914 0 1.1407-.29053.40161-.29053.40161-.82886 0-.46997-.29053-.73486-.286-.265-.944-.397l-.52551-.10254q-.96558-.19226-1.3971-.60242t-.43152-1.1407q0-.84595.59387-1.333.59814-.48706 1.6449-.48706.44861 0 .91431.08118t.95276.24353z" + ></path><path + d="m86.391 70.446h4.0332v.72632h-3.1702v1.8884h3.0377v.72632h-3.0377v2.3114h3.2471v.72632h-4.1101v-6.3788z" + ></path><path + d="m97.504 70.937v.91003q-.43579-.40588-.9314-.60669-.49133-.2008-1.0468-.2008-1.0938 0-1.6748.67078-.58105.6665-.58105 1.9312 0 1.2604.58105 1.9312.58106.6665 1.6748.6665.55542 0 1.0468-.20081.49561-.20081.9314-.60669v.90149q-.45288.30762-.9613.46143-.50415.15381-1.0681.15381-1.4484 0-2.2815-.8844-.83313-.88867-.83313-2.4225 0-1.5381.83313-2.4225.83313-.88867 2.2815-.88867.57251 0 1.0767.15381.50842.14954.95276.45288z" + ></path><path + d="m99.848 70.446h.86732v3.8751q0 1.0254.3717 1.4783.3717.44861 1.2048.44861.82886 0 1.2006-.44861.3717-.45288.3717-1.4783v-3.8751h.86731v3.9819q0 1.2476-.61951 1.8842-.61523.6366-1.8201.6366-1.2091 0-1.8286-.6366-.61524-.6366-.61524-1.8842v-3.9819z" + ></path><path + d="m110.78 73.834q.27771.09399.53833.40161.26489.30762.52978.84595l.87586 1.7432h-.92713l-.81604-1.6364q-.31616-.64087-.61523-.85022-.2948-.20935-.8075-.20935h-.93994v2.6959h-.86304v-6.3788h1.9482q1.0938 0 1.6321.45715.53833.45715.53833 1.38 0 .60242-.28199.99976-.27771.39734-.81176.55115zm-2.1619-2.6788v2.2644h1.0852q.62378 0 .93994-.28625.32043-.29053.32043-.85022t-.32043-.84168q-.31616-.28626-.93994-.28626h-1.0852z" + ></path><path d="m114.84 70.446h.86304v6.3788h-.86304z"></path><path + d="m117.53 70.446h5.3961v.72632h-2.2644v5.6525h-.86731v-5.6525h-2.2644v-.72632z" + ></path><path + d="m124.09 70.446h.92712l1.7688 2.6233 1.756-2.6233h.92712l-2.2559 3.3411v3.0377h-.86731v-3.0377l-2.2559-3.3411z" + ></path></g + ></svg + > + </a> + <a + class="sponsorship-title" + target="_blank" + rel="noopener noreferrer" + href="https://www.kali.org/" + > + <h3>Kali Linux</h3> + </a> + <div class="sponsorship-description"> + <p>Kali Linux is an advanced penetration testing Linux distribution.</p> + </div> + <div class="sponsorship-links"> + <a + class="sponsorship-link-site more" + target="_blank" + rel="noopener noreferrer" + href="https://www.kali.org/">Visit site</a + > + <a + class="sponsorship-link-github more" + target="_blank" + rel="noopener noreferrer" + href="https://github.com/offensive-security">Code on GitHub</a + > + </div> + </div> + + <div class="sponsorship"> + <a + class="sponsorship-icon" + target="_blank" + rel="noopener noreferrer" + href="https://clickhouse.tech/" + > + <svg + height="48" + viewBox="0 0 9 8" + width="54" + xmlns="http://www.w3.org/2000/svg" + ><path d="m0 7h1v1h-1z" fill="#f00"></path><g fill="#fc0" + ><path d="m0 0h1v7h-1z"></path><path d="m2 0h1v8h-1z"></path><path + d="m4 0h1v8h-1z"></path><path d="m6 0h1v8h-1z"></path><path + d="m8 3.25h1v1.5h-1z"></path></g + ></svg + > + </a> + <a + class="sponsorship-title" + target="_blank" + rel="noopener noreferrer" + href="https://clickhouse.tech/" + > + <h3>ClickHouse</h3> + </a> + <div class="sponsorship-description"> + <p>ClickHouse is a free analytics DBMS for big data</p> + </div> + <div class="sponsorship-links"> + <a + class="sponsorship-link-site more" + target="_blank" + rel="noopener noreferrer" + href="https://clickhouse.tech/">Visit site</a + > + <a + class="sponsorship-link-github more" + target="_blank" + rel="noopener noreferrer" + href="https://github.com/ClickHouse/ClickHouse">Code on GitHub</a + > + </div> + </div> + + <div class="sponsorship"> + <a + class="sponsorship-icon" + target="_blank" + rel="noopener noreferrer" + href="https://phalcon.io/" + > + <svg + height="300" + preserveAspectRatio="xMidYMid meet" + viewBox="0 0 640 640" + width="300" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + ><defs + ><path + id="a" + d="m249.75 391.53 60.83-4.81 11.59-9.15-3.38-5.78-39.59 5.78z" + ></path><path + id="b" + d="m411.01 554.72v8.67l-19.8-8.19-2.41 4.33 18.35 9.15 7.72 18.77-18.35-15.4h-3.86l3.86 47.18-23.17-26.96-1.93-27.44 10.62-23.59z" + ></path><path + id="c" + d="m376.74 569.64v22.15l-29.94 12.51-18.35-32.73v-95.8l7.73-14.92 2.9 2.41 4.34 74.13z" + ></path><path + id="d" + d="m447.22 473.37v9.14l-19.31-8.66-2.42 4.33 17.38 8.67 8.21 19.73-17.38-15.88h-3.86l3.38 46.69-25.11-28.88-.96-21.66 11.1-26.96z" + ></path><path + id="e" + d="m411.01 489.73v17.81l-48.28 18.3-16.9-35.63-3.86-23.1 11.1-7.22 32.83-1.93 7.73 20.22z" + ></path><path + id="f" + d="m346.8 409.34-47.8-12.51-90.77 90.01v58.73l22.69 77.51v-40.92l54.08-131.42z" + ></path><path + id="g" + d="m440.94 412.71c-3.79 3.15-22.78 18.87-56.97 47.18h-30.9l94.15-61.62h52.15c-31.16 7.7-50.64 12.52-58.43 14.44z" + ></path><path + id="h" + d="m599.31 395.38-27.52 14.45-102.36-41.88-2.41-28.41 56.97 3.85 52.15 14.93z" + ></path><path + id="i" + d="m566.48 393.94-.96-1.44 10.14-7.71 14.48 8.19-.97 1.92-13.51-7.7z" + ></path><path + id="j" + d="m460.26 325.58v63.06l-42.01-15.88-82.08-41.88 14.49-32.74z" + ></path><path id="k" d="m438.05 260.6 1.45-45.25 47.8 93.87z" + ></path><path + id="l" + d="m439.98 201.39-232.24-198.33 34.28 94.35 135.19 129.97 92.22 100.13z" + ></path><path + id="m" + d="m288.86 196.09 88.36 31.29-180.58-174.26 7.73 46.69z" + ></path><path + id="n" + d="m181.19 229.79 76.29 75.09-81.12-24.06-21.24-14.45-68.08-62.09z" + ></path><path + id="o" + d="m333.75 366.02-90.28-8.18-71.46-38.51 163.68 30.8z" + ></path><path + id="p" + d="m273.4 379.98-55.52-17.81 119.26 4.33 116.36 9.63 13.52 25.03-28.97-5.29-63.25-5.78-45.87-17.33-19.31 10.59z" + ></path><path + id="q" + d="m326.04 325.58 92.21 47.18-84.49-6.74-107.67-32.25-98.01-68.36z" + ></path><path + id="r" + d="m187.95 259.63 69.52 45.25 78.7 26 14.49-32.74-297.42-177.63 55.04 77.99z" + ></path><path + id="s" + d="m127.11 171.06 223.55 127.08 118.77 29.37-92.22-100.13-336.52-174.26z" + ></path><path + id="t" + d="m460.26 325.58 9.17 1.93v1.92l54.56 13.96-47.31 6.74 37.66 15.89-37.66.96 59.38 23.59 35.73 19.25 1.93 8.19-56.49-19.74h-50.21l-6.76-10.11z" + ></path><path + id="u" + d="m584.83 440.15h-14.97l6.76-11.55-4.83-18.77 27.52-14.45v32.26z" + ></path><path + id="v" + d="m289.82 557.61-2.41 3.37 7.73 77.02-49.25-93.39 42-99.16 86.91-55.36 72.43 7.7v.48l-118.78 77.5-10.14-14.92-39.11 83.28z" + ></path><path + id="w" + d="m208.22 494.07-36.21 42.84 12.55-46.69 37.66-51.51 106.71-65.95 45.86 17.33z" + ></path><path + id="x" + d="m328.45 592.75 14.97 37.07-64.22-85.69 39.11-83.28 10.14 14.92z" + ></path><path id="y" d="m164.77 480.59 30.9-41.4-30.9 25.03z" + ></path><path + id="z" + d="m201.95 602.86-11.59-7.22v-39.96l6.28 14.92z"></path></defs + ><use fill="#acd2b4" xlink:href="#a"></use><use + fill="none" + xlink:href="#a"></use><use fill="#3a3a3a" xlink:href="#b"></use><use + fill="none" + xlink:href="#b"></use><use fill="#8fc19a" xlink:href="#c"></use><use + fill="none" + xlink:href="#c"></use><use fill="#3a3a3a" xlink:href="#d"></use><use + fill="none" + xlink:href="#d"></use><use fill="#678a6f" xlink:href="#e"></use><use + fill="none" + xlink:href="#e"></use><use fill="#3a3a3a" xlink:href="#f"></use><use + fill="none" + xlink:href="#f"></use><use fill="#3a3a3a" xlink:href="#g"></use><use + fill="none" + xlink:href="#g"></use><use fill="#79a281" xlink:href="#h"></use><use + fill="none" + xlink:href="#h"></use><use fill="#3a3a3a" xlink:href="#i"></use><use + fill="none" + xlink:href="#i"></use><use fill="#8fc19a" xlink:href="#j"></use><use + fill="none" + xlink:href="#j"></use><use fill="#7aa383" xlink:href="#k"></use><use + fill="none" + xlink:href="#k"></use><use fill="#cce2d2" xlink:href="#l"></use><use + fill="none" + xlink:href="#l"></use><use fill="#acd2b4" xlink:href="#m"></use><use + fill="none" + xlink:href="#m"></use><use fill="#393839" xlink:href="#n"></use><use + fill="none" + xlink:href="#n"></use><use fill="#acd2b4" xlink:href="#o"></use><use + fill="none" + xlink:href="#o"></use><use fill="#678a6f" xlink:href="#p"></use><use + fill="none" + xlink:href="#p"></use><use fill="#cce1d1" xlink:href="#q"></use><use + fill="none" + xlink:href="#q"></use><use fill="#78a381" xlink:href="#r"></use><use + fill="none" + xlink:href="#r"></use><use fill="#393839" xlink:href="#s"></use><use + fill="none" + xlink:href="#s"></use><use fill="#cce1d1" xlink:href="#t"></use><use + fill="none" + xlink:href="#t"></use><use fill="#3a3a3a" xlink:href="#u"></use><use + fill="none" + xlink:href="#u"></use><use fill="#cde3d2" xlink:href="#v"></use><use + fill="none" + xlink:href="#v"></use><use fill="#8fc19a" xlink:href="#w"></use><use + fill="none" + xlink:href="#w"></use><use fill="#78a381" xlink:href="#x"></use><use + fill="none" + xlink:href="#x"></use><use fill="#3a3a3a" xlink:href="#y"></use><use + fill="none" + xlink:href="#y"></use><use fill="#678a6f" xlink:href="#z"></use><use + fill="none" + xlink:href="#z"></use></svg + > + </a> + <a + class="sponsorship-title" + target="_blank" + rel="noopener noreferrer" + href="https://phalcon.io/" + > + <h3>Phalcon</h3> + </a> + <div class="sponsorship-description"> + <p>Phalcon is a full-stack PHP framework delivered as a C-extension</p> + </div> + <div class="sponsorship-links"> + <a + class="sponsorship-link-site more" + target="_blank" + rel="noopener noreferrer" + href="https://phalcon.io/">Visit site</a + > + <a + class="sponsorship-link-github more" + target="_blank" + rel="noopener noreferrer" + href="https://github.com/phalcon/cphalcon/">Code on GitHub</a + > + </div> + </div> + + <div class="sponsorship"> + <a + class="sponsorship-icon" + target="_blank" + rel="noopener noreferrer" + href="https://www.jsdelivr.com/" + > + <svg + enable-background="new 0 0 120 100" + viewBox="0 0 120 100" + xmlns="http://www.w3.org/2000/svg" + ><path + d="m59.7 10-4.8 16.4v.2 16.4l4.8 16.6 4.9-16.6v-16.4-.2z" + fill="#bc473a"></path><g fill="#1d3a45" + ><path + d="m14.6 84c0 4-1.4 6-4.2 6-.4 0-.9-.1-1.3-.2v-1.1c.5.2.9.2 1.3.2 1.1 0 1.8-.4 2.3-1.2s.7-2.1.7-3.8v-10.8h1.1v10.9z" + ></path><path + d="m18.1 89.1v-1.3c1.2.8 2.4 1.1 3.6 1.1 1.3 0 2.3-.3 2.9-.8.7-.5 1-1.3 1-2.2 0-.8-.2-1.5-.7-2s-1.4-1.2-2.9-2.1c-1.7-1-2.7-1.8-3.2-2.5-.4-.7-.7-1.4-.7-2.3 0-1.2.5-2.2 1.4-3s2.1-1.2 3.6-1.2c1 0 2 .2 3 .5v1.2c-1-.4-2-.7-3.1-.7s-2 .3-2.7.9-1 1.3-1 2.2c0 .8.2 1.5.7 2s1.4 1.2 2.9 2c1.5.9 2.6 1.7 3.1 2.3.5.7.8 1.5.8 2.4 0 1.3-.4 2.3-1.3 3.1s-2.1 1.2-3.7 1.2c-.6 0-1.2-.1-2-.3-.8 0-1.4-.2-1.7-.5z" + ></path><path + d="m30.1 89.7v-16.6h5.9c5.9 0 8.8 2.7 8.8 8.1 0 2.6-.8 4.6-2.4 6.2-1.6 1.5-3.7 2.3-6.4 2.3zm3.7-13.5v10.5h1.8c1.6 0 2.9-.5 3.8-1.5s1.4-2.3 1.4-4c0-1.6-.5-2.8-1.4-3.7s-2.2-1.4-3.8-1.4h-1.8z" + ></path><path + d="m57.8 89.7h-9.9v-16.6h9.6v3h-5.8v3.7h5.3v3h-5.4v3.8h6.2z" + ></path><path d="m70.8 89.7h-9.9v-16.6h3.7v13.5h6.1v3.1z" + ></path><path d="m76.9 73.1v16.6h-3.7v-16.6z"></path><path + d="m94.7 73.1-5.7 16.6h-4.2l-5.6-16.6h4l3.5 11.5c.2.6.3 1.2.3 1.7.1-.5.2-1.1.4-1.7l3.4-11.5z" + ></path><path + d="m110.8 89.7h-4.3l-2.6-4.3c-.2-.3-.4-.6-.6-.9s-.4-.5-.5-.7c-.2-.2-.4-.3-.6-.4s-.4-.1-.6-.1h-1v6.3h-3.7v-16.5h5.9c4 0 6 1.5 6 4.5 0 .6-.1 1.1-.3 1.6s-.4.9-.8 1.3c-.3.4-.7.7-1.2 1s-1 .5-1.5.7c.2.1.5.2.7.4s.5.4.7.6.4.5.6.7c.2.3.4.5.5.8zm-10.2-13.8v4.6h1.6c.8 0 1.4-.2 1.9-.7s.7-1.1.7-1.7c0-1.4-.9-2.2-2.6-2.2z" + ></path></g + ><path d="m59.7 10-22.9 8.3 3.3 30.5 19.6 10.8" fill="#e84d3d" + ></path><path + d="m59.7 59.6 19.8-10.9 3.7-30.6-23.5-8.1" + fill="#bc473a"></path><path + d="m55.6 46.9c-2.3-.7-4.3-2-5.9-3.6-.2-.2-.4-.4-.6-.6-1.2-1.5-2.2-3.2-2.8-5.1.4.3.8.7 1.2 1 .6.4 1.2.8 1.9 1.1.2.1.4.2.6.3.3.1.5.2.8.3h.1.1c.1 1.1 1 2 2.1 2.2.5 1.7 1.3 3.2 2.5 4.4z" + fill="#fdc72e"></path><path + d="m56.4 27.1c0 .5.1.9.4 1.3-1.9 2.6-3.1 5.4-3.7 8.2-.1.3-.1.6-.2.9-.6.2-1.2.6-1.5 1.1 0 0 0 0-.1 0h-.1c-.2-.1-.4-.1-.6-.2-.3-.1-.6-.3-.9-.4-1-.5-1.9-1.1-2.7-1.9-.2-.2-.3-.3-.5-.5l-.1-.1c-.1-.1-.1-.1-.2-.2 0 0 0-.1-.1-.1 0 0 0 0-.1-.1s-.1-.1-.1-.1 0 0-.1-.1-.2-.2-.2-.3c0 0 0 0 0-.1 0-.3 0-.6 0-.9 0-2.8.8-5.4 2.3-7.6.2-.3.3-.5.5-.8.2-.2.4-.5.6-.7 1.7-2 3.9-3.4 6.4-4.2 0 .3.1.5.1.8.3 1.4.8 2.9 1.4 4.3-.2.4-.5 1-.5 1.7z" + fill="#fdc72e"></path><path + d="m54.9 40c0 .3-.1.6-.3.9 0 .1-.1.1-.1.1-.2.2-.5.3-.9.3-.3 0-.5-.1-.7-.2s-.4-.3-.5-.6c-.1-.2-.1-.4-.1-.6 0-.4.2-.7.4-1 0 0 .1-.1.1-.1.2-.2.5-.3.8-.3s.6.1.8.3l.3.3c.2.4.2.6.2.9z" + fill="#fdc72e"></path><path + d="m60.3 27.1c0 .2-.1.4-.1.6-.2.4-.7.7-1.2.7-.1 0-.1 0-.2 0-.6-.1-1-.5-1.1-1.1 0-.1 0-.1 0-.2 0-.2 0-.3.1-.4.2-.5.7-.9 1.3-.9h.1c.6 0 1.1.4 1.2 1-.1 0-.1.1-.1.3z" + fill="#fdc72e"></path><path + d="m73.7 33.5c0 1.3-.2 2.7-.5 3.9-1.2-.3-2.4-.8-3.6-1.4 0-.2 0-.3 0-.5 0-.3-.1-.7-.2-.9 0 0 .1-.1.1-.1 1.4-1.5 2.6-3.1 3.6-4.8.4 1.2.6 2.5.6 3.8z" + fill="#e09b00"></path><path + d="m72.4 27.7c-.1.2-.2.4-.3.6-.9 1.8-2.2 3.5-3.7 5.1-.4-.2-.9-.4-1.3-.4-.6 0-1.1.2-1.5.5-.8-.6-1.5-1.3-2.2-2-.9-.9-1.7-1.9-2.5-2.8.4-.4.6-1 .6-1.6 0-.5-.1-.9-.4-1.3 1.8-1.7 3.8-3.1 5.8-4.2 2.4 1.5 4.3 3.6 5.5 6.1z" + fill="#e09b00"></path><path + d="m68.4 35.6c0 .6-.3 1-.8 1.2-.2.1-.3.1-.5.1-.1 0-.2 0-.3 0-.6-.1-1-.6-1.1-1.2s0 0 0-.1c0-.6.4-1.1.9-1.3.1-.1.3-.1.5-.1.1 0 .3 0 .4.1.5.2.9.6 1 1.2-.1 0-.1 0-.1.1z" + fill="#e09b00"></path><path + d="m68.8 37.4c-.4.4-1 .7-1.7.7-.5 0-1-.2-1.4-.4-.1 0-.1.1-.2.1-1.8 1.2-3.8 2-5.8 2.5v7.2c5.8 0 10.8-3.5 12.9-8.6-1.3-.3-2.6-.8-3.8-1.5z" + fill="#e09b00"></path><path + d="m59.7 40.3c-1.2.3-2.4.5-3.6.5-.1 0-.1 0-.2 0-.2.6-.7 1.1-1.2 1.4.4 1.4 1 2.5 2 3.5.8.8 1.7 1.4 2.8 1.8h.2z" + fill="#fdc72e"></path><path + d="m64.5 35.6c0-.3 0-.5.1-.7-.8-.7-1.6-1.4-2.4-2.1-.9-.9-1.7-1.9-2.5-2.9v8.9c1.7-.5 3.3-1.2 4.9-2.2 0-.5-.1-.7-.1-1z" + fill="#e09b00"></path><path + d="m59.7 29.8c-.1-.1-.1-.2-.2-.3-.2 0-.4.1-.6.1-.3 0-.7-.1-.9-.2-1.7 2.4-2.8 5-3.3 7.5 0 .2-.1.5-.1.7.7.3 1.3.9 1.5 1.6h.1c1.2-.1 2.4-.3 3.6-.6v-8.8z" + fill="#fdc72e"></path><path + d="m59.7 19.6v5.1c.1 0 .1 0 .2.1 1.6-1.6 3.4-2.9 5.3-4-1.6-.8-3.5-1.2-5.5-1.2z" + fill="#e09b00"></path><path + d="m59.7 19.6c-.9 0-1.8.1-2.6.2 0 .3.1.6.2.9.3 1.3.7 2.6 1.2 3.8h.5c.3 0 .5 0 .8.1v-5z" + fill="#fdc72e"></path></svg + > + </a> + <a + class="sponsorship-title" + target="_blank" + rel="noopener noreferrer" + href="https://www.jsdelivr.com/" + > + <h3>JsDelivr</h3> + </a> + <div class="sponsorship-description"> + <p> + JsDelivr is a public, open-source CDN (Content Delivery Network) + developed by ProspectOne, focused on performance, reliability, and + security. + </p> + </div> + <div class="sponsorship-links"> + <a + class="sponsorship-link-site more" + target="_blank" + rel="noopener noreferrer" + href="https://www.jsdelivr.com/">Visit site</a + > + <a + class="sponsorship-link-github more" + target="_blank" + rel="noopener noreferrer" + href="https://github.com/jsdelivr/jsdelivr">Code on GitHub</a + > + </div> + </div> + + <div class="sponsorship"> + <a + class="sponsorship-icon" + target="_blank" + rel="noopener noreferrer" + href="https://freecodecamp.org/" + > + <svg viewBox="0 0 465 306" xmlns="http://www.w3.org/2000/svg" + ><g fill="#09630c" fill-rule="evenodd" + ><path + d="m.734375 147.007553c0-47.0698735 16.6933031-89.1850237 50.3328381-126.5931865 12.140584-13.37775353 22.2577374-20.3143665 29.086816-20.3143665 2.2763595 0 4.552719.74320853 6.8290785 2.2296256 1.517573 1.48641706 3.035146 3.71604266 3.035146 5.94566825 0 3.71604265-4.552719 8.91850235-12.8993705 17.34153245-35.1571081 32.9489115-52.8621265 73.3299085-52.8621265 122.1339357 0 54.006486 18.4638049 97.360317 54.3796995 131.052437 7.587865 6.688877 10.623011 11.891337 10.623011 16.350588 0 2.229626-1.517573 4.459251-3.035146 6.688877-1.517573 1.486417-4.552719 2.972834-6.8290785 2.972834-8.3466516 0-19.981378-9.661711-35.157108-28.48966-29.5926737-35.67401-43.5037596-78.284632-43.5037596-129.318285z" + ></path><path + d="m464.1 158.89889c0 47.069873-16.693303 89.185024-50.332838 126.593186-12.140584 13.377754-22.257738 20.314367-29.086816 20.314367-2.27636 0-4.552719-.743209-6.829079-2.229626-1.517573-1.486417-3.035146-4.459251-3.035146-6.688877 0-3.716042 4.552719-8.918502 12.899371-16.350587 35.157108-32.948912 52.862126-73.329909 52.862126-122.133936 0-54.006486-18.463805-97.3603174-54.379699-131.0524375-7.587865-6.6888768-10.623011-11.8913365-10.623011-16.3505877 0-2.22962559 1.517573-4.45925119 3.035146-6.68887679 1.517573-1.48641706 4.552719-2.97283412 6.829078-2.97283412 7.587865 0 19.981378 9.66171091 35.157108 28.48966041 28.075101 35.6740095 43.50376 78.2846317 43.50376 129.0705487z" + ></path><path + d="m231.018149 29.7567544h-212.030356c-7.5951172 0-13.29145509-5.5793914-13.29145509-13.01858 0-7.43918864 5.69633789-13.0185801 13.29145509-13.0185801h212.030356c7.595117 0 13.291455 5.57939146 13.291455 13.0185801-1.89878 7.4391886-5.696338 13.01858-13.291455 13.01858z" + transform="translate(107.580239 265.729742)"></path><path + d="m187.395855 25.6168263s17.705018 11.8913365 17.705018 33.6921201c0 37.4081628-52.10334 78.5323686-52.10334 117.4269476 0 41.867414 32.880749 68.870658 64.243924 77.045952 4.552719.743208 6.070292-3.716043.758787-5.20246-12.899371-2.972834-26.810457-19.571158-26.810457-44.840248 0-24.030409 23.775311-32.948912 32.880749-44.09704 9.864225-11.891336 7.587865-27.003243 1.517573-31.462494-6.070292-4.459252-1.517573-10.40492 8.346652-2.972834 9.864224 6.688876 18.463804 20.314366 15.17573 32.205703-3.793933 15.607379 1.517573 24.773617 9.864224 26.260034 8.346652 1.486417 17.705019-2.972834 16.693303-10.404919-1.011715-7.432085-5.311505-15.607379-3.035146-15.607379 6.070292 0 18.463805 14.120962 18.463805 37.408163 0 23.2872-19.222591 38.894579-25.292883 44.840248-3.793933 3.716042.758786 8.175294 6.070292 5.945668 2.276359-.743209 7.587865-3.716043 10.623011-5.945668 15.17573-10.40492 38.95104-32.205703 38.95104-71.843492 0-41.867414-15.934516-60.695363-32.880748-76.3027424-16.693303-15.6073792-20.740165-11.8913365-15.934517-3.7160426 13.658157 23.287201 5.311506 31.462494-4.552719 31.462494-12.140584 0-12.140584-26.2600343-17.705018-46.3266647-9.864225-44.8402481-52.10334-52.5200696-59.691205-53.2632781-5.564435-2.4773618-12.646442 1.9818894-3.288075 5.6979321z" + ></path></g + ></svg + > + </a> + <a + class="sponsorship-title" + target="_blank" + rel="noopener noreferrer" + href="https://freecodecamp.org/" + > + <h3>freeCodeCamp</h3> + </a> + <div class="sponsorship-description"> + <p>An online coding bootcamp that teaches you to code for free.</p> + </div> + <div class="sponsorship-links"> + <a + class="sponsorship-link-site more" + target="_blank" + rel="noopener noreferrer" + href="https://freecodecamp.org/">Visit site</a + > + <a + class="sponsorship-link-github more" + target="_blank" + rel="noopener noreferrer" + href="https://github.com/freeCodeCamp/freeCodeCamp">Code on GitHub</a + > + </div> + </div> + + <div class="sponsorship"> + <a + class="sponsorship-icon" + target="_blank" + rel="noopener noreferrer" + href="https://uppy.io/" + > + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 316.92 478.17" + ><title>uppy-dog-full-2 + + +

    Uppy

    +
    +
    +

    + A sleek, modular file uploader that integrates seamlessly with any + application. +

    +
    + +

    + +
    + + + + +

    Redux

    +
    +
    +

    + A predictable state container for JavaScript apps. It’s commonly used + with React to make state management simple. +

    +
    + +
    + +
    + + + + +

    jQuery

    +
    +
    +

    The Write Less, Do More, JavaScript Library.

    +
    + +
    + +
    + + + + +

    Select2

    +
    +
    +

    + A jQuery based replacement for select boxes. It supports searching, + remote data sets, and infinite scrolling of results. +

    +
    + +
    + +
    + + + + +

    Gulp

    +
    +
    +

    + A toolkit for automating painful or time-consuming tasks in your + development workflow, so you can stop messing around and build + something. +

    +
    + +
    + +
    + + + + +

    impress.js

    +
    +
    +

    + A presentation tool made to leverage the power of css3 transforms and + transitions. +

    +
    + +
    + +
    + + + + +

    Express

    +
    +
    +

    A fast, minimalist web framework for Node.js.

    +
    + +
    + +
    + + + + +

    REVEAL.JS

    +
    +
    +

    + A framework for making powerpoint-like presentations using HTML, CSS + and JS. +

    +
    + +
    + +
    + + + + +

    three.js

    +
    +
    +

    + A JavaScript library used to create and display animated 3D computer + graphics in a web browser. +

    +
    + +
    + +
    + + + + +

    Bootstrap

    +
    +
    +

    A responsive, mobile-first HTML, CSS and JS styling library.

    +
    + +
    +
    +
    + + + diff --git a/src/nimbus/pages/videos/[...slug].astro b/src/nimbus/pages/videos/[...slug].astro new file mode 100644 index 00000000000..38f7ffa51f1 --- /dev/null +++ b/src/nimbus/pages/videos/[...slug].astro @@ -0,0 +1,57 @@ +--- +/** + * /videos/ — Cloudflare Stream video page. + * + * CF source: cloudflare-docs/src/pages/videos/[...slug].astro (faithful port, + * minimal). The Starlight splash wrapper is swapped for Nimbus's BaseLayout + + * Header chrome. getStaticPaths keys on `data.url` (NOT the entry id) so the + * generated paths equal the `/videos/${data.url}/` hrefs emitted by + * ResourcesBySelector's video cards. + * + * Scope (W5): the requirement is that the URL resolves and the video renders. + * The cf-nimbus `stream` schema has no `transcript` field, so the upstream + * transcript block is omitted (full playback/transcript parity is secondary). + */ +import type { GetStaticPaths } from "astro"; +import { getCollection } from "astro:content"; +import BaseLayout from "~/layouts/BaseLayout.astro"; +import Header from "~/components/Header.astro"; +import Stream from "~/components/cf/Stream.astro"; +import Width from "~/components/cf/Width.astro"; + +export const prerender = true; + +export const getStaticPaths = (async () => { + const entries = await getCollection("stream"); + + return entries + .filter((entry) => Boolean(entry.data.url)) + .map((entry) => { + return { + params: { + slug: entry.data.url, + }, + props: { + entry: entry.data, + }, + }; + }); +}) satisfies GetStaticPaths; + +const { entry } = Astro.props; +--- + + +
    +
    +
    +
    +

    {entry.title}

    + {entry.description &&

    {entry.description}

    } + + + +
    +
    +
    +
    diff --git a/src/nimbus/pages/videos/index.astro b/src/nimbus/pages/videos/index.astro new file mode 100644 index 00000000000..de19fac16b0 --- /dev/null +++ b/src/nimbus/pages/videos/index.astro @@ -0,0 +1,18 @@ +--- +/** + * /videos/ — video catalog. + * + * CF source: src/pages/videos/index.astro (StarlightPage `template: "splash"`). + * Ported to SplashLayout; body unchanged. + */ +import SplashLayout from "~/layouts/SplashLayout.astro"; +import { ResourcesBySelector } from "~/components"; + +export const prerender = true; +--- + + +
    + +
    +
    diff --git a/src/nimbus/pages/waf/change-log/changelog/[...page].astro b/src/nimbus/pages/waf/change-log/changelog/[...page].astro new file mode 100644 index 00000000000..e6dc78e5197 --- /dev/null +++ b/src/nimbus/pages/waf/change-log/changelog/[...page].astro @@ -0,0 +1,74 @@ +--- +/** + * /waf/change-log/changelog/ — paginated WAF changelog. CF source: the same + * path's `[...page].astro` (StarlightPage → DocsLayout). Sibling imports + * rewired to `cf/`; `` is given the MDX registry so admonitions + * resolve (the Nimbus convention). Body otherwise unchanged. + */ +import type { GetStaticPaths } from "astro"; +import { render } from "astro:content"; +import { format } from "date-fns"; +import { getSidebar, getBreadcrumbs } from "nimbus-docs"; +import DocsLayout from "~/layouts/DocsLayout.astro"; +import { externalAppLinksTransform } from "~/util/sidebar"; +import Pagination from "~/components/changelog/Pagination.astro"; +import RSSButton from "~/components/cf/RSSButton.astro"; +import AnchorHeading from "~/components/cf/AnchorHeading.astro"; +import { components } from "@/mdx-components"; +import { getChangelogs } from "~/util/changelog"; + +export const prerender = true; + +export const getStaticPaths = (async ({ paginate }) => { + const entries = await getChangelogs({ + filter: (entry) => + !entry.data.hidden && + !entry.data.publish_future_dated_entry && + entry.data.products.some((p) => p.id === "waf"), + }); + + return paginate(entries, { pageSize: 25 }); +}) satisfies GetStaticPaths; + +const { page } = Astro.props; + +const sectionSlug = "/waf/change-log/changelog/"; +const sidebar = await getSidebar(sectionSlug, { collection: "docs", transform: externalAppLinksTransform }); +const breadcrumbs = await getBreadcrumbs(sectionSlug); +--- + + + + + + { + page.data.map(async (entry) => { + const { Content } = await render(entry); + return ( + <> + +
    + {entry.data.title} +
    + + + ); + }) + } + + +
    diff --git a/src/nimbus/pages/workers-ai/models/[...name].astro b/src/nimbus/pages/workers-ai/models/[...name].astro new file mode 100644 index 00000000000..ad81e329f8d --- /dev/null +++ b/src/nimbus/pages/workers-ai/models/[...name].astro @@ -0,0 +1,29 @@ +--- +/** + * /workers-ai/models// — a single Cloudflare-hosted model page. + * + * Rest param for route-shape consistency, though legacy short slugs are + * single-segment. Bound to `getLegacyModels` with the short slug + * `model.name.split('/').at(-1)`. + */ +import type { GetStaticPaths } from "astro"; +import ModelDetailPage from "~/components/models/ModelDetailPage.astro"; +import { getLegacyModels } from "~/util/models"; + +export const prerender = true; + +export const getStaticPaths = (async () => { + const models = await getLegacyModels(); + return models.map((model) => ({ + params: { name: model.name.split("/").at(-1)! }, + props: { model, slug: model.name.split("/").at(-1)! }, + })); +}) satisfies GetStaticPaths; + +const { model, slug } = Astro.props as { + model: Awaited>[number]; + slug: string; +}; +--- + + diff --git a/src/nimbus/pages/workers-ai/models/[...name].md.ts b/src/nimbus/pages/workers-ai/models/[...name].md.ts new file mode 100644 index 00000000000..5438921fee7 --- /dev/null +++ b/src/nimbus/pages/workers-ai/models/[...name].md.ts @@ -0,0 +1,30 @@ +/** + * TODO(migration): replace this build-time `.md` route with edge "Markdown for + * Agents" (HTML→Markdown at request time) at cutover. See the migration plan. + */ +/** + * /workers-ai/models/.md — markdown rendering of a model page. + * + * Backs the page-actions row (Copy page / View as Markdown). Same per-route + * binding as `[...name].astro`: `getLegacyModels` + the short slug. + */ +import type { APIRoute, GetStaticPaths } from "astro"; +import { getLegacyModels, type ModelView } from "~/util/models"; +import { renderModelMarkdown } from "~/util/model-markdown"; + +export const prerender = true; + +export const getStaticPaths = (async () => { + const models = await getLegacyModels(); + return models.map((model) => { + const slug = model.name.split("/").at(-1)!; + return { params: { name: slug }, props: { model, slug } }; + }); +}) satisfies GetStaticPaths; + +export const GET: APIRoute = ({ props }) => { + const { model, slug } = props as { model: ModelView; slug: string }; + return new Response(renderModelMarkdown(model, "/workers-ai/models", slug), { + headers: { "content-type": "text/markdown; charset=utf-8" }, + }); +}; diff --git a/src/nimbus/pages/workers-ai/models/[...schema].json.ts b/src/nimbus/pages/workers-ai/models/[...schema].json.ts new file mode 100644 index 00000000000..2c2371a9518 --- /dev/null +++ b/src/nimbus/pages/workers-ai/models/[...schema].json.ts @@ -0,0 +1,49 @@ +/** + * /workers-ai/models//.json — raw JSON-Schema endpoints. + * + * `detectApiModes` splits the schema into per-mode `{mode.id}-input/-output.json` + * when it has modes, else a single `schema-input/-output.json`. Bound to + * `getLegacyModels` with the short slug. + */ +import type { APIRoute, GetStaticPaths, InferGetStaticPropsType } from "astro"; +import { getLegacyModels, detectApiModes } from "~/util/models"; + +export const prerender = true; + +export const getStaticPaths = (async () => { + const models = await getLegacyModels(); + const paths: { params: { schema: string }; props: { schema: unknown } }[] = []; + + for (const model of models) { + const slug = model.name.split("/").at(-1)!; + const modes = detectApiModes(model.schema); + + if (modes) { + for (const mode of modes) { + paths.push({ + params: { schema: `${slug}/${mode.id}-input` }, + props: { schema: mode.input }, + }); + paths.push({ + params: { schema: `${slug}/${mode.id}-output` }, + props: { schema: mode.output }, + }); + } + } else { + paths.push({ + params: { schema: `${slug}/schema-input` }, + props: { schema: model.schema.input }, + }); + paths.push({ + params: { schema: `${slug}/schema-output` }, + props: { schema: model.schema.output }, + }); + } + } + + return paths; +}) satisfies GetStaticPaths; + +type Props = InferGetStaticPropsType; + +export const GET: APIRoute = ({ props }) => Response.json(props.schema); diff --git a/src/nimbus/pages/workers/platform/compatibility-flags.json.ts b/src/nimbus/pages/workers/platform/compatibility-flags.json.ts new file mode 100644 index 00000000000..d2f92e9d062 --- /dev/null +++ b/src/nimbus/pages/workers/platform/compatibility-flags.json.ts @@ -0,0 +1,31 @@ +import { getCollection } from "astro:content"; + +export async function GET() { + const entries = await getCollection("compatibility-flags"); + + entries.sort((a, b) => a.data.sort_date.localeCompare(b.data.sort_date)); + + const flags = entries.flatMap((x) => { + if (!x.data.enable_flag) { + x.data.enable_flag = null; + } + + if (!x.data.enable_date) { + x.data.enable_date = null; + } + + if (!x.data.disable_flag) { + x.data.disable_flag = null; + } + + // omit sort_date from output + const { sort_date: _, ...data } = x.data; + return { + ...data, + description: x.body?.trim(), + experimental: x.data.experimental ?? false, + }; + }); + + return Response.json(flags); +} diff --git a/src/nimbus/pages/workers/prompt.txt.ts b/src/nimbus/pages/workers/prompt.txt.ts new file mode 100644 index 00000000000..4cd5349e39d --- /dev/null +++ b/src/nimbus/pages/workers/prompt.txt.ts @@ -0,0 +1,8 @@ +import type { APIRoute } from "astro"; +import BasePrompt from "~/content/partials/prompts/base-prompt.txt?raw"; + +export const GET: APIRoute = async () => { + return new Response(BasePrompt, { + headers: { "content-type": "text/plain" }, + }); +}; diff --git a/src/nimbus/plugins/rehype/autolink-headings.ts b/src/nimbus/plugins/rehype/autolink-headings.ts new file mode 100644 index 00000000000..61807d5d3c3 --- /dev/null +++ b/src/nimbus/plugins/rehype/autolink-headings.ts @@ -0,0 +1,72 @@ +// Adapted from cloudflare-docs (src/plugins/rehype/autolink-headings.ts). +// +// Wraps each heading that has an id in a `.heading-wrapper` div and appends an +// `.anchor-link`. Runs after heading-slugs (ids exist) and before the built-in +// heading-ids, which still finds the wrapped heading and honours its id. + +import type { Element, HastPluginDefinition } from "./types"; + +function anchorIcon(): Element { + return { + type: "element", + tagName: "span", + properties: { ariaHidden: "true", className: ["anchor-icon"] }, + children: [ + { + type: "element", + tagName: "svg", + properties: { width: 16, height: 16, viewBox: "0 0 24 24" }, + children: [ + { + type: "element", + tagName: "path", + properties: { + fill: "currentcolor", + d: "m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z", + }, + children: [], + }, + ], + }, + ], + }; +} + +export default function rehypeAutolinkHeadings(): HastPluginDefinition { + return { + name: "cf-autolink-headings", + element: { + filter: ["h1", "h2", "h3", "h4", "h5", "h6"], + visit(node) { + const id = node.properties?.id; + if (typeof id !== "string" || id.length === 0) return; + + const anchor: Element = { + type: "element", + tagName: "a", + properties: { className: ["anchor-link"], href: `#${id}` }, + children: [anchorIcon()], + }; + + // Return a replacement wrapper rather than wrapNode + insertAfter: + // in this Sätteri version insertAfter places the anchor outside the + // wrapper, breaking the `.heading-wrapper > h, a` structure. + const heading: Element = { + type: "element", + tagName: node.tagName, + properties: { ...node.properties }, + children: [...(node.children ?? [])], + }; + return { + type: "element", + tagName: "div", + properties: { + tabIndex: -1, + className: ["heading-wrapper", `level-${node.tagName}`], + }, + children: [heading, anchor], + }; + }, + }, + }; +} diff --git a/src/nimbus/plugins/rehype/empty-table-headers.ts b/src/nimbus/plugins/rehype/empty-table-headers.ts new file mode 100644 index 00000000000..175db8b8fb6 --- /dev/null +++ b/src/nimbus/plugins/rehype/empty-table-headers.ts @@ -0,0 +1,21 @@ +// Drop a markdown table's header row when every header cell is empty. Some CF +// content uses "headerless" GFM tables (`| | |`), which render as a styled but +// empty band; remove it so no blank header shows. Markdown tables only +// — component (.astro) tables aren't in this pipeline. Emptiness is by text +// content, so a header whose cells hold only an image/
    counts as empty. + +import type { HastPluginDefinition } from "./types"; + +export default function rehypeEmptyTableHeaders(): HastPluginDefinition { + return { + name: "cf-empty-table-headers", + element: { + filter: ["thead"], + visit(node, ctx) { + if (ctx.textContent(node).trim() === "") { + ctx.removeNode(node); + } + }, + }, + }; +} diff --git a/src/nimbus/plugins/rehype/heading-slugs.ts b/src/nimbus/plugins/rehype/heading-slugs.ts new file mode 100644 index 00000000000..7f49439dd3a --- /dev/null +++ b/src/nimbus/plugins/rehype/heading-slugs.ts @@ -0,0 +1,88 @@ +// Adapted from cloudflare-docs (src/plugins/rehype/heading-slugs.ts). +// +// ## foo {/* bar */} ->

    foo

    +// +// Runs after external-links, so the arrow on any external link inside a +// heading is stripped before slugging. One GithubSlugger per document (factory +// form: Sätteri invokes the factory per compile) for deterministic -1/-2 +// dedupe. Sets `properties.id`; the built-in heading-ids honours it. +// +// A trailing `{/* id */}` arrives as an `mdxTextExpression` child in the .mdx +// pipeline; the literal-text form is handled as a fallback for .md. + +import GithubSlugger from "github-slugger"; +import type { + Element, + HastPluginDefinition, + HastVisitorContext, + Text, +} from "./types"; +import { EXTERNAL_LINK_ARROW } from "nimbus-docs/markdown"; + +const LITERAL_COMMENT_ID = /\{\/\*\s*([\s\S]*?)\s*\*\/\}\s*$/; + +function setSlug( + ctx: HastVisitorContext, + node: Element, + slug: string, +): void { + ctx.setProperty(node, "id", slug); +} + +export default function rehypeHeadingSlugs(): HastPluginDefinition { + const slugger = new GithubSlugger(); + + return { + name: "cf-heading-slugs", + element: { + filter: ["h1", "h2", "h3", "h4", "h5", "h6"], + visit(node, ctx) { + const children = node.children ?? []; + const last = children.at(-1); + if (!last) return; + + if ( + (last as { type: string }).type === "mdxTextExpression" && + typeof (last as { value?: unknown }).value === "string" + ) { + const value = (last as { value: string }).value; + if (value.startsWith("/*") && value.endsWith("*/")) { + const id = value.slice(2, -2).trim(); + setSlug(ctx, node, slugger.slug(id)); + + const prev = children.at(-2); + if (prev && (prev as { type: string }).type === "text") { + const text = prev as Text; + ctx.setProperty(text, "value", text.value.trimEnd()); + } + } + return; + } + + if ((last as { type: string }).type === "text") { + const text = last as Text; + const m = text.value.match(LITERAL_COMMENT_ID); + if (m) { + const id = m[1].trim(); + setSlug(ctx, node, slugger.slug(id)); + ctx.setProperty( + text, + "value", + text.value.slice(0, m.index).trimEnd(), + ); + return; + } + } + + if (!node.properties?.id) { + const string = ctx + .textContent(node) + .split(EXTERNAL_LINK_ARROW) + .join("") + .trimEnd(); + setSlug(ctx, node, slugger.slug(string)); + } + }, + }, + }; +} diff --git a/src/nimbus/plugins/rehype/index.ts b/src/nimbus/plugins/rehype/index.ts new file mode 100644 index 00000000000..ce687b97d43 --- /dev/null +++ b/src/nimbus/plugins/rehype/index.ts @@ -0,0 +1,43 @@ +// The cf-nimbus rehype pipeline, mirroring cloudflare-docs' +// `markdown.rehypePlugins` order. Sätteri runs these as user hast plugins in +// array order (shiki → these → image-marker → built-in heading-ids). +// +// `externalLinks` and `titleFigure` are the framework's general passes, from +// `nimbus-docs/markdown`. The rest are CF-specific and stay local. +// +// Order is load-bearing: +// - external-links before heading-slugs: the arrow must exist so it can be +// stripped before slugging. +// - heading-slugs before autolink-headings: ids must exist before anchors +// link to them. +// - shift-headings last: it operates on the `.heading-wrapper` autolink +// produces. + +import { externalLinks, titleFigure } from "nimbus-docs/markdown"; +import rehypeMermaid from "./mermaid"; +import rehypeHeadingSlugs from "./heading-slugs"; +import rehypeAutolinkHeadings from "./autolink-headings"; +import rehypeShiftHeadings from "./shift-headings"; +import rehypeEmptyTableHeaders from "./empty-table-headers"; +import type { HastPluginDefinition } from "./types"; + +export { + rehypeMermaid, + rehypeHeadingSlugs, + rehypeAutolinkHeadings, + rehypeShiftHeadings, + rehypeEmptyTableHeaders, +}; + +// Local entries are factory functions: Sätteri invokes each per document, so +// per-document state (e.g. heading-slugs' GithubSlugger) resets between pages. +// The framework passes are stateless definitions, safe to instantiate once. +export const rehypePlugins: HastPluginDefinition[] = [ + rehypeMermaid, + externalLinks(), + rehypeHeadingSlugs, + rehypeAutolinkHeadings, + titleFigure(), + rehypeShiftHeadings, + rehypeEmptyTableHeaders, +] as unknown as HastPluginDefinition[]; diff --git a/src/nimbus/plugins/rehype/mermaid.ts b/src/nimbus/plugins/rehype/mermaid.ts new file mode 100644 index 00000000000..6c47129e509 --- /dev/null +++ b/src/nimbus/plugins/rehype/mermaid.ts @@ -0,0 +1,47 @@ +// Adapted from cloudflare-docs (src/plugins/rehype/mermaid.ts) — the +// "pre-mermaid" strategy. A ```mermaid fence reaches hast as +// `
    ` (mermaid must be in +// `markdown.syntaxHighlight.excludeLangs`, or shiki tokenises it first) and is +// replaced with `
    ` for the client to render. + +import type { Element, HastPluginDefinition } from "./types"; +import { classNames, isElement } from "./types"; + +const NON_WHITESPACE = /\S/; + +function mermaidCodeChild(pre: Element): Element | null { + let found: Element | null = null; + for (const child of pre.children ?? []) { + if (child.type === "text") { + if (NON_WHITESPACE.test(child.value)) return null; + continue; + } + if (isElement(child, "code") && classNames(child).includes("language-mermaid")) { + if (found) return null; + found = child; + continue; + } + return null; + } + return found; +} + +export default function rehypeMermaid(): HastPluginDefinition { + return { + name: "cf-mermaid", + element: { + filter: ["pre"], + visit(node, ctx) { + const code = mermaidCodeChild(node); + if (!code) return; + + return { + type: "element", + tagName: "pre", + properties: { className: ["mermaid"] }, + children: [{ type: "text", value: ctx.textContent(code) }], + }; + }, + }, + }; +} diff --git a/src/nimbus/plugins/rehype/shift-headings.ts b/src/nimbus/plugins/rehype/shift-headings.ts new file mode 100644 index 00000000000..8b5df4c5d3f --- /dev/null +++ b/src/nimbus/plugins/rehype/shift-headings.ts @@ -0,0 +1,43 @@ +// Adapted from cloudflare-docs (src/plugins/rehype/shift-headings.ts). +// +// In changelog entries only, demotes h1–h3 to h4 (inside the `.heading-wrapper` +// autolink-headings produces) and rewrites the wrapper's `level-h${n}` class. +// Gated on the source path via `ctx.filename`. Runs last (after +// autolink-headings). + +import type { Element, HastPluginDefinition } from "./types"; +import { classNames, isElement } from "./types"; + +const CHANGELOG_PATH = "/content/changelog/"; +const HEADING = /^h([1-6])$/; + +export default function rehypeShiftHeadings(): HastPluginDefinition { + return { + name: "cf-shift-headings", + element: { + filter: ["div"], + visit(node, ctx) { + if (!ctx.filename || !ctx.filename.includes(CHANGELOG_PATH)) return; + + const classes = classNames(node); + if (!classes.includes("heading-wrapper")) return; + + const children = node.children ?? []; + const heading = children.find( + (c): c is Element => isElement(c) && HEADING.test(c.tagName), + ); + if (!heading) return; + + const level = Number(heading.tagName.slice(1)); + if (!(level >= 1 && level < 4)) return; + + ctx.replaceNode(heading, { ...heading, tagName: "h4" }); + ctx.setProperty( + node, + "className", + classes.map((c) => (c === `level-h${level}` ? "level-h4" : c)), + ); + }, + }, + }; +} diff --git a/src/nimbus/plugins/rehype/types.ts b/src/nimbus/plugins/rehype/types.ts new file mode 100644 index 00000000000..d88720e3d55 --- /dev/null +++ b/src/nimbus/plugins/rehype/types.ts @@ -0,0 +1,38 @@ +// Types for the cf-nimbus rehype pipeline, reimplemented as Sätteri hast +// plugins (the framework's supported extension point — `mdx()`-attached +// remark/rehype plugins are dropped by Sätteri). +// +// Pipeline order inside Sätteri: shiki → these plugins (array order) → +// image-marker → heading-ids. The built-in heading-ids honours a pre-set +// string `node.properties.id`, so `heading-slugs` only needs to set it. + +import type { + HastPluginDefinition, + HastVisitorContext, +} from "satteri"; +import type { Element, ElementContent, Text } from "hast"; + +export type { HastPluginDefinition, HastVisitorContext }; +export type { Element, ElementContent, Text }; + +export const HEADING_TAGS = ["h1", "h2", "h3", "h4", "h5", "h6"] as const; + +/** Narrow a hast node to an Element, optionally of a given tagName. */ +export function isElement( + node: { type: string; tagName?: string } | null | undefined, + tagName?: string, +): node is Element { + return ( + !!node && + node.type === "element" && + (tagName === undefined || node.tagName === tagName) + ); +} + +/** Normalise a hast `className` property to a string[]. */ +export function classNames(node: Element): string[] { + const cn = node.properties?.className; + if (Array.isArray(cn)) return cn.map(String); + if (typeof cn === "string") return cn.split(/\s+/).filter(Boolean); + return []; +} diff --git a/src/nimbus/schemas/base.ts b/src/nimbus/schemas/base.ts new file mode 100644 index 00000000000..739bec691bc --- /dev/null +++ b/src/nimbus/schemas/base.ts @@ -0,0 +1,101 @@ +import { z } from "astro/zod"; +import { reference, type SchemaContext } from "astro:content"; + +import { sidebar } from "./types/sidebar"; + +export const baseSchema = (_context: SchemaContext) => + z.object({ + pcx_content_type: z + .string() + .optional() + .describe( + "The purpose of the page, and defined through specific pages in [Content strategy](/style-guide/documentation-content-strategy/content-types/).", + ), + tags: z + .string() + .array() + .optional() + .describe( + "A group of related keywords relating to the purpose of the page.", + ), + external_link: z + .string() + .optional() + .describe( + "Path to another page in our docs or elsewhere. Used to add a crosslink entry to the lefthand navigation sidebar.", + ), + difficulty: z + .string() + .optional() + .describe( + "Difficulty is displayed as a column in the [ListTutorials component](/style-guide/components/list-tutorials/).", + ), + reviewed: z + .date() + .optional() + .describe( + "A `YYYY-MM-DD` value that signals when the page was last explicitly reviewed from beginning to end.", + ), + release_notes_file_name: z + .string() + .array() + .optional() + .describe( + "Required for the [`ProductReleaseNotes`](/style-guide/components/usage/#productreleasenotes) component.", + ), + products: z + .array(reference("directory")) + .default([]) + .describe( + "The names of related directory entries (according to their file name in `src/content/directory`). Usually, these correspond to file paths, but not always, such as with `cloudflare-tunnel`", + ), + summary: z + .string() + .optional() + .describe("Renders a summary description directly below the page title."), + noindex: z + .boolean() + .optional() + .describe( + "If true, this property adds a `noindex` declaration to the page, which will tell internal / external search crawlers to ignore this page. Helpful for pages that are historically accurate, but no longer recommended, such as [Workers Sites](/workers/configuration/sites/).", + ), + sidebar, + hideChildren: z + .boolean() + .optional() + .describe( + "Renders this group as a single link on the sidebar, to the index page. Refer to [Sidebar](https://developers.cloudflare.com/style-guide/frontmatter/sidebar/).", + ), + styleGuide: z + .object({ + component: z.string(), + }) + .optional() + .describe( + "Used by overrides for style guide component documentation, which helps us display the [usage counts](/style-guide/components/usage/) for components directly on the component page itself.", + ), + banner: z + .object({ + content: z.string(), + type: z + .enum(["default", "note", "tip", "caution", "danger"]) + .optional() + .default("default"), + }) + .optional() + .describe( + "Displays a [Banner](https://developers.cloudflare.com/style-guide/frontmatter/banner/) on the current docs page.", + ), + feedback: z + .boolean() + .default(true) + .describe( + "Whether to show the FeedbackPrompt on the page, defaults to true", + ), + canonical: z + .string() + .optional() + .describe( + 'A canonical URL or path to set as the `` in the page ``, overriding the default derived from the page URL.', + ), + }); diff --git a/src/nimbus/schemas/compatibility-flags.ts b/src/nimbus/schemas/compatibility-flags.ts new file mode 100644 index 00000000000..ab4143ebb96 --- /dev/null +++ b/src/nimbus/schemas/compatibility-flags.ts @@ -0,0 +1,12 @@ +import { z } from "astro/zod"; + +export type CompatibilityFlagsSchema = z.infer; + +export const compatibilityFlagsSchema = z.object({ + name: z.string(), + enable_date: z.string().optional().nullable(), + enable_flag: z.string().nullable(), + disable_flag: z.string().optional().nullable(), + sort_date: z.string(), + experimental: z.boolean().optional(), +}); diff --git a/src/nimbus/schemas/fields.ts b/src/nimbus/schemas/fields.ts new file mode 100644 index 00000000000..2724798fe96 --- /dev/null +++ b/src/nimbus/schemas/fields.ts @@ -0,0 +1,17 @@ +import { z } from "astro/zod"; + +export const fieldsSchema = z.object({ + entries: z + .object({ + name: z.string(), + data_type: z.string(), + categories: z.array(z.string()).optional(), + keywords: z.array(z.string()).optional(), + summary: z.string(), + description: z.string().optional(), + plan_info_label: z.string().optional(), + example_value: z.string().optional(), + example_block: z.string().optional(), + }) + .array(), +}); diff --git a/src/nimbus/schemas/index.ts b/src/nimbus/schemas/index.ts new file mode 100644 index 00000000000..7caab9b7b99 --- /dev/null +++ b/src/nimbus/schemas/index.ts @@ -0,0 +1,4 @@ +export { baseSchema } from "./base"; +export { warpReleasesSchema } from "./warp-releases"; +export { compatibilityFlagsSchema } from "./compatibility-flags"; +export type { CompatibilityFlagsSchema } from "./compatibility-flags"; diff --git a/src/nimbus/schemas/types/sidebar.ts b/src/nimbus/schemas/types/sidebar.ts new file mode 100644 index 00000000000..e5b367e7587 --- /dev/null +++ b/src/nimbus/schemas/types/sidebar.ts @@ -0,0 +1,77 @@ +/** + * Existing configuration options in Starlight's `sidebar` object are duplicated + * here due to https://github.com/StefanTerdell/zod-to-json-schema/issues/68 + * + * Existing options can be found in + * https://github.com/withastro/starlight/blob/main/packages/starlight/schema.ts + */ + +import { z } from "astro/zod"; + +/** + * From https://github.com/withastro/starlight/blob/main/packages/starlight/schemas/badge.ts + */ + +const linkHTMLAttributesSchema = z.record( + z.string(), + z.union([z.string(), z.number(), z.boolean(), z.undefined()]), +); + +const SidebarLinkItemHTMLAttributesSchema = () => + linkHTMLAttributesSchema.prefault({}); + +/** + * https://github.com/withastro/starlight/blob/main/packages/starlight/schemas/sidebar.ts + */ + +const badgeBaseSchema = z.object({ + variant: z + .enum(["note", "danger", "success", "caution", "tip", "default"]) + .default("default"), + class: z.string().optional(), +}); + +const badgeSchema = badgeBaseSchema.extend({ + text: z.string(), +}); + +const BadgeConfigSchema = () => + z + .union([z.string(), badgeSchema]) + .transform((badge) => { + if (typeof badge === "string") { + return { variant: "default" as const, text: badge }; + } + return badge; + }) + .optional(); + +export const sidebar = z + .object({ + order: z.number().optional(), + label: z.string().optional(), + hidden: z.boolean().default(false), + badge: BadgeConfigSchema(), + attrs: SidebarLinkItemHTMLAttributesSchema(), + group: z + .object({ + label: z + .string() + .optional() + .describe( + "Overrides the default 'Overview' label for index pages in the sidebar. Refer to https://developers.cloudflare.com/style-guide/frontmatter/sidebar/.", + ), + hideIndex: z + .boolean() + .default(false) + .describe( + "Hides the index page from the sidebar. Refer to [Sidebar](/style-guide/frontmatter/sidebar/).", + ), + badge: BadgeConfigSchema(), + }) + .optional(), + }) + .prefault({}) + .describe( + "Used to configure various sidebar options. Refer to [Sidebar](/style-guide/frontmatter/sidebar/).", + ); diff --git a/src/nimbus/schemas/warp-releases.ts b/src/nimbus/schemas/warp-releases.ts new file mode 100644 index 00000000000..0d7e7bc83f1 --- /dev/null +++ b/src/nimbus/schemas/warp-releases.ts @@ -0,0 +1,24 @@ +import { z } from "astro/zod"; + +export const warpReleasesSchema = z + .object({ + version: z.string(), + releaseDate: z.coerce.date(), + releaseNotes: z.string(), + packageSize: z.number().optional(), + packageURL: z.string(), + platformName: z.enum(["Windows", "macOS", "Linux"]), + linuxPlatforms: z.record(z.string(), z.number()).optional(), + }) + .refine( + (val) => { + if (val.platformName !== "Linux" && !val.packageSize) return false; + if (val.platformName === "Linux" && !val.linuxPlatforms) return false; + + return true; + }, + { + message: + "Non-Linux platforms require the 'packageSize' property. Linux platforms require the 'linuxPlatforms' property.", + }, + ); diff --git a/src/nimbus/scripts/mermaid.client.ts b/src/nimbus/scripts/mermaid.client.ts new file mode 100644 index 00000000000..3c200726b93 --- /dev/null +++ b/src/nimbus/scripts/mermaid.client.ts @@ -0,0 +1,258 @@ +// Renders `pre.mermaid` blocks: lazy-loads mermaid only on pages that have a +// diagram, applies brand theme variables, and adds an expand-to-dialog modal +// and annotation footer. Adapted from cloudflare-docs (src/scripts/mermaid.ts); +// dark mode keys off `[data-mode="dark"]` and re-runs on `astro:page-load` for +// the view-transitions ClientRouter. + +let dialog: HTMLDialogElement | null = null; +let themeObserver: MutationObserver | null = null; +// Per-
     guard: capture source text once, before mermaid replaces innerHTML.
    +const captured = new WeakSet();
    +
    +function getDialog(): HTMLDialogElement {
    +  if (dialog) return dialog;
    +
    +  dialog = document.createElement("dialog");
    +  dialog.className = "mermaid-dialog";
    +  dialog.innerHTML = `
    +    
    + + `; + document.body.appendChild(dialog); + + function closeWithAnimation() { + if (!dialog || !dialog.open) return; + dialog.classList.add("closing"); + dialog.addEventListener( + "animationend", + () => { + dialog!.classList.remove("closing"); + dialog!.close(); + document.documentElement.style.overflow = ""; + }, + { once: true }, + ); + } + + dialog.addEventListener("click", (e) => { + if (e.target === dialog) closeWithAnimation(); + }); + dialog + .querySelector(".mermaid-dialog-close") + ?.addEventListener("click", () => { + closeWithAnimation(); + }); + dialog.addEventListener("cancel", (e) => { + e.preventDefault(); + closeWithAnimation(); + }); + + return dialog; +} + +function openDiagram(container: HTMLElement) { + const d = getDialog(); + const clone = container.cloneNode(true) as HTMLElement; + + clone.querySelector(".mermaid-expand")?.remove(); + + const svg = clone.querySelector("svg"); + if (svg) { + svg.removeAttribute("style"); + svg.setAttribute("width", "100%"); + svg.setAttribute("height", "auto"); + } + + const body = d.querySelector(".mermaid-dialog-body"); + if (!body) return; + body.replaceChildren(clone); + + clone.addEventListener("click", (e) => { + const target = e.target as Element; + const anchor = target.closest("a"); + const clickable = target.closest(".clickable"); + if (anchor || clickable) { + d.close(); + document.documentElement.style.overflow = ""; + } + }); + + document.documentElement.style.overflow = "hidden"; + d.showModal(); +} + +function getFontFamily(): string { + const computedStyle = getComputedStyle(document.documentElement); + const font = computedStyle.getPropertyValue("--nb-font-sans").trim(); + return font || "system-ui, -apple-system, sans-serif"; +} + +function isLightTheme(): boolean { + return document.documentElement.getAttribute("data-mode") !== "dark"; +} + +function getPageBackground(): string { + const style = getComputedStyle(document.documentElement); + const bg = style.getPropertyValue("--nb-background").trim(); + return bg || (isLightTheme() ? "#ffffff" : "#1d1d1d"); +} + +function wrapDiagram(diagram: HTMLPreElement, title: string | null) { + if (diagram.parentElement?.classList.contains("mermaid-container")) { + return; + } + + const container = document.createElement("div"); + container.className = "mermaid-container"; + + diagram.parentNode?.insertBefore(container, diagram); + container.appendChild(diagram); + + const expandBtn = document.createElement("button"); + expandBtn.className = "mermaid-expand"; + expandBtn.setAttribute("aria-label", "Expand diagram"); + expandBtn.innerHTML = ` + + + + + `; + expandBtn.addEventListener("click", () => openDiagram(container)); + container.appendChild(expandBtn); + + if (title) { + const footer = document.createElement("div"); + footer.className = "mermaid-annotation"; + + const titleSpan = document.createElement("span"); + titleSpan.className = "mermaid-annotation-title"; + titleSpan.textContent = title; + + const logo = document.createElement("img"); + logo.src = "/logo.svg"; + logo.alt = "Cloudflare"; + logo.className = "mermaid-annotation-logo"; + + footer.appendChild(titleSpan); + footer.appendChild(logo); + container.appendChild(footer); + } +} + +async function render() { + const diagrams = + document.querySelectorAll("pre.mermaid"); + if (diagrams.length === 0) return; + + const { default: mermaid } = await import("mermaid"); + + const isLight = isLightTheme(); + const fontFamily = getFontFamily(); + const pageBg = getPageBackground(); + + const lightThemeVars = { + fontFamily, + primaryColor: "#fef1e6", + primaryBorderColor: "#f6821f", + primaryTextColor: "#1d1d1d", + secondaryColor: "#f2f2f2", + secondaryBorderColor: "#999999", + secondaryTextColor: "#1d1d1d", + tertiaryColor: "#f2f2f2", + tertiaryBorderColor: "#999999", + tertiaryTextColor: "#1d1d1d", + lineColor: "#f6821f", + textColor: "#1d1d1d", + mainBkg: "#fef1e6", + errorBkgColor: "#ffefee", + errorTextColor: "#3c0501", + edgeLabelBackground: pageBg, + labelBackground: pageBg, + }; + + const darkThemeVars = { + fontFamily, + primaryColor: "#482303", + primaryBorderColor: "#f6821f", + primaryTextColor: "#f2f2f2", + secondaryColor: "#313131", + secondaryBorderColor: "#797979", + secondaryTextColor: "#f2f2f2", + tertiaryColor: "#313131", + tertiaryBorderColor: "#797979", + tertiaryTextColor: "#f2f2f2", + lineColor: "#f6821f", + textColor: "#f2f2f2", + mainBkg: "#482303", + background: "#1d1d1d", + errorBkgColor: "#3c0501", + errorTextColor: "#ffefee", + edgeLabelBackground: pageBg, + labelBackground: pageBg, + }; + + const themeVariables = isLight ? lightThemeVars : darkThemeVars; + + mermaid.initialize({ + startOnLoad: false, + theme: "base", + themeVariables, + flowchart: { + htmlLabels: true, + useMaxWidth: true, + curve: "linear", + }, + }); + + for (const diagram of diagrams) { + try { + if (!captured.has(diagram)) { + diagram.setAttribute("data-diagram", diagram.textContent as string); + captured.add(diagram); + } + + const def = diagram.getAttribute("data-diagram") as string; + + const { svg } = await mermaid.render( + `mermaid-${crypto.randomUUID()}`, + def, + ); + diagram.innerHTML = svg; + + const svgElement = diagram.querySelector("svg"); + const titleElement = svgElement?.querySelector("title"); + const title = titleElement?.textContent?.trim() || null; + + wrapDiagram(diagram, title); + } catch (e) { + console.error("Mermaid render failed:", e); + } + + diagram.setAttribute("data-processed", "true"); + } +} + +function setup() { + const diagrams = + document.querySelectorAll("pre.mermaid"); + if (diagrams.length === 0) return; + + if (!themeObserver) { + themeObserver = new MutationObserver(() => render()); + themeObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ["data-mode"], + }); + } + + render(); +} + +setup(); +// Re-run after SPA navigations (view-transitions ClientRouter). +document.addEventListener("astro:page-load", setup); diff --git a/src/nimbus/styles/agent-setup.css b/src/nimbus/styles/agent-setup.css new file mode 100644 index 00000000000..31d9fe6e55d --- /dev/null +++ b/src/nimbus/styles/agent-setup.css @@ -0,0 +1,2523 @@ +/* + * Agent Setup — Kumo-inspired styles + * + * Uses existing --color-cl1-* tokens from tailwind.css + * and Starlight --sl-color-* variables. Scoped to .agent-setup-* + * classes to avoid conflicts with the rest of the docs. + */ + +/* ─── Back button ─── */ +.agent-setup-back { + display: inline-flex; + align-items: center; + gap: 0.25rem; + margin-bottom: 1.5rem; + font-size: 0.875rem; + font-weight: 500; + color: var(--color-cl1-gray-5); + text-decoration: none; +} + +.agent-setup-back:hover { + color: var(--sl-color-text-accent); +} + +/* ─── Agent icon light/dark switching ─── */ +.agent-setup-icon-wrap { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 8px; + overflow: hidden; + line-height: 0; +} + +.agent-setup-icon-wrap img, +.agent-setup-card-header img, +.agent-setup-related img { + margin: 0; + padding: 0; + border-radius: 8px; +} + +.agent-icon-dark { + display: none; +} + +:root[data-theme="dark"] .agent-icon-light { + display: none; +} + +:root[data-theme="dark"] .agent-icon-dark { + display: inline; +} + +/* ─── Cloudflare tools banner ─── */ +.cf-banner { + margin: 0; +} + +.cf-banner-items { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.625rem; +} + +@media (max-width: 640px) { + .cf-banner-items { + grid-template-columns: 1fr; + } +} + +.cf-banner-item { + display: flex; + flex-direction: column; + gap: 0.375rem; + padding: 0.875rem 1rem; + border: 1px solid var(--color-cl1-gray-8); + border-radius: 10px; +} + +:root[data-theme="dark"] .cf-banner-item { + border-color: var(--color-cl1-gray-2); +} + +.cf-banner-item-title { + font-size: 0.9375rem; + font-weight: 600; + margin: 0; +} + +.cf-banner-item-desc { + font-size: 0.8125rem; + line-height: 1.55; + color: var(--color-cl1-gray-5); + margin: 0; + flex: 1; +} + +:root[data-theme="dark"] .cf-banner-item-desc { + color: var(--color-cl1-gray-6); +} + +.cf-banner-item-desc a, +.cf-banner-item-links a { + color: var(--sl-color-text-accent); + text-decoration: none; +} + +.cf-banner-item-desc a:hover, +.cf-banner-item-links a:hover { + text-decoration: underline; +} + +.cf-banner-item-links { + font-size: 0.8125rem; + margin-top: auto; + padding-top: 0.125rem; +} + +/* ─── Landing page hero ─── */ +.agent-setup-lede { + font-size: 1.0625rem; + line-height: 1.6; + color: var(--color-cl1-gray-5); + margin: -0.5rem 0 2rem; + max-width: 640px; +} + +:root[data-theme="dark"] .agent-setup-lede { + color: var(--color-cl1-gray-7); +} + +.agent-setup-lede a { + color: var(--sl-color-text-accent); + text-decoration: none; +} + +.agent-setup-lede a:hover { + text-decoration: underline; +} + +.agent-setup-lede-highlight { + color: var(--sl-color-text); + font-weight: 600; + background: color-mix(in srgb, var(--color-cl1-orange-5) 10%, transparent); + padding: 0.05em 0.3em; + border-radius: 4px; +} + +:root[data-theme="dark"] .agent-setup-lede-highlight { + background: color-mix(in srgb, var(--color-cl1-orange-5) 15%, transparent); +} + +/* ─── Agent cards (modernized) ─── */ +.agent-setup-card { + position: relative; + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1.25rem; + border: 1px solid var(--color-cl1-gray-8); + border-radius: 14px; + background: var(--sl-color-bg); + text-decoration: none; + color: inherit; + overflow: hidden; + transition: + transform 0.2s ease, + border-color 0.2s ease; +} + +.agent-setup-card:hover { + transform: translateY(-2px); + border-color: var(--color-cl1-orange-6); +} + +:root[data-theme="dark"] .agent-setup-card { + border-color: var(--color-cl1-gray-2); +} + +:root[data-theme="dark"] .agent-setup-card:hover { + border-color: var(--color-cl1-orange-5); +} + +.agent-setup-card-header { + display: flex; + align-items: center; + gap: 0.875rem; +} + +.agent-setup-card-icon { + width: 44px; + height: 44px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 1.125rem; + background: var(--color-cl1-orange-9); + color: var(--color-cl1-orange-5); + flex-shrink: 0; +} + +:root[data-theme="dark"] .agent-setup-card-icon { + background: var(--color-cl1-orange-1); + color: var(--color-cl1-orange-6); +} + +.agent-setup-card-title { + font-weight: 600; + font-size: 1.0625rem; + line-height: 1.2; + margin: 0; + padding: 0; + letter-spacing: -0.01em; +} + +.agent-setup-card-vendor { + font-size: 0.8125rem; + line-height: 1.3; + margin: 0.1875rem 0 0; + padding: 0; + color: var(--color-cl1-gray-5); +} + +:root[data-theme="dark"] .agent-setup-card-vendor { + color: var(--color-cl1-gray-6); +} + +.agent-setup-card-body { + display: flex; + flex-direction: column; + gap: 0.875rem; + flex: 1; +} + +.agent-setup-card-description { + font-size: 0.875rem; + line-height: 1.55; + color: var(--color-cl1-gray-4); + margin: 0; +} + +:root[data-theme="dark"] .agent-setup-card-description { + color: var(--color-cl1-gray-7); +} + +.agent-setup-card-features { + list-style: none; + padding: 0; + margin: 0; + font-size: 0.8125rem; + color: var(--color-cl1-gray-4); + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +:root[data-theme="dark"] .agent-setup-card-features { + color: var(--color-cl1-gray-6); +} + +.agent-setup-card-features li { + padding: 0; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.agent-setup-card-features li::before { + content: ""; + width: 4px; + height: 4px; + border-radius: 50%; + background: var(--color-cl1-orange-5); + flex-shrink: 0; + margin: 0; +} + +:root[data-theme="dark"] .agent-setup-card-features li::before { + background: var(--color-cl1-orange-6); +} + +.agent-setup-card-cta { + display: inline-flex; + align-items: center; + gap: 0.375rem; + font-size: 0.8125rem; + font-weight: 600; + color: var(--color-cl1-orange-5); + margin-top: auto; + transition: gap 0.2s ease; +} + +.agent-setup-card:hover .agent-setup-card-cta { + gap: 0.625rem; +} + +:root[data-theme="dark"] .agent-setup-card-cta { + color: var(--color-cl1-orange-6); +} + +/* ─── Capability badges ─── */ +.agent-setup-badge { + display: inline-flex; + align-items: center; + padding: 0.125rem 0.5rem; + border-radius: 9999px; + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0.01em; + line-height: 1.6; + white-space: nowrap; +} + +.agent-setup-badge--ide { + background: var(--color-cl1-blue-9); + color: var(--color-cl1-blue-3); +} +:root[data-theme="dark"] .agent-setup-badge--ide { + background: var(--color-cl1-blue-1); + color: var(--color-cl1-blue-7); +} + +.agent-setup-badge--terminal { + background: var(--color-cl1-pink-9); + color: var(--color-cl1-pink-3); +} +:root[data-theme="dark"] .agent-setup-badge--terminal { + background: var(--color-cl1-pink-1); + color: var(--color-cl1-pink-7); +} + +.agent-setup-badge--standalone { + background: var(--color-cl1-cyan-9); + color: var(--color-cl1-cyan-3); +} +:root[data-theme="dark"] .agent-setup-badge--standalone { + background: var(--color-cl1-cyan-1); + color: var(--color-cl1-cyan-7); +} + +.agent-setup-badge--cloud { + background: var(--color-cl1-violet-9); + color: var(--color-cl1-violet-3); +} +:root[data-theme="dark"] .agent-setup-badge--cloud { + background: var(--color-cl1-violet-1); + color: var(--color-cl1-violet-7); +} + +.agent-setup-badge--extension { + background: var(--color-cl1-indigo-9); + color: var(--color-cl1-indigo-3); +} +:root[data-theme="dark"] .agent-setup-badge--extension { + background: var(--color-cl1-indigo-1); + color: var(--color-cl1-indigo-7); +} + +.agent-setup-badge--mcp { + background: var(--color-cl1-orange-9); + color: var(--color-cl1-orange-3); +} +:root[data-theme="dark"] .agent-setup-badge--mcp { + background: var(--color-cl1-orange-1); + color: var(--color-cl1-orange-7); +} + +.agent-setup-badge--skills { + background: var(--color-cl1-green-9); + color: var(--color-cl1-green-3); +} +:root[data-theme="dark"] .agent-setup-badge--skills { + background: var(--color-cl1-green-1); + color: var(--color-cl1-green-7); +} + +.agent-setup-badge--open-source { + background: var(--color-cl1-green-9); + color: var(--color-cl1-green-3); +} +:root[data-theme="dark"] .agent-setup-badge--open-source { + background: var(--color-cl1-green-1); + color: var(--color-cl1-green-7); +} + +/* ─── Comparison table ─── */ +.agent-table-scroll { + width: 100%; + overflow-x: auto; + overflow-y: visible; + border: 1px solid var(--color-cl1-gray-8); + border-radius: 12px; + background: var(--sl-color-bg); +} + +:root[data-theme="dark"] .agent-table-scroll { + border-color: var(--color-cl1-gray-2); +} + +.agent-setup-table { + display: table; + width: 100%; + min-width: 900px; + border-collapse: separate; + border-spacing: 0; + font-size: 0.875rem; + table-layout: auto; +} + +/* Sticky first column so the agent name stays visible while scrolling + horizontally on smaller viewports. */ +.agent-setup-table--sticky-first thead th:first-child, +.agent-setup-table--sticky-first tbody td:first-child { + position: sticky; + left: 0; + z-index: 2; + background: var(--sl-color-bg); + border-right: 1px solid var(--sl-color-gray-6); +} + +.agent-setup-table--sticky-first thead th:first-child { + background: var(--sl-color-gray-7, #f5f5f5); + z-index: 3; +} + +:root[data-theme="dark"] .agent-setup-table--sticky-first thead th:first-child { + background: var(--color-cl1-gray-1); +} + +.agent-setup-table th { + padding: 0.875rem 0.75rem; + font-weight: 600; + font-size: 0.75rem; + letter-spacing: 0.03em; + text-transform: uppercase; + color: var(--color-cl1-gray-5); + background: var(--sl-color-gray-7, #f5f5f5); + border-bottom: 1px solid var(--color-cl1-gray-8); + text-align: center; + vertical-align: middle; +} + +.agent-setup-table th[data-align="left"] { + text-align: left; +} + +.agent-setup-table th:first-child { + width: 140px; + border-top-left-radius: 12px; +} + +.agent-setup-table th:last-child { + border-top-right-radius: 12px; +} + +:root[data-theme="dark"] .agent-setup-table th { + color: var(--color-cl1-gray-6); + background: var(--color-cl1-gray-1); + border-bottom-color: var(--color-cl1-gray-2); +} + +.agent-setup-table td { + padding: 0.75rem 0.75rem; + border-bottom: 1px solid var(--color-cl1-gray-9); + text-align: center; + vertical-align: middle; +} + +.agent-setup-table td[data-align="left"] { + text-align: left; +} + +.agent-setup-table tbody tr:last-child td { + border-bottom: none; +} + +:root[data-theme="dark"] .agent-setup-table td { + border-bottom-color: var(--color-cl1-gray-2); +} + +.agent-setup-table td svg { + display: inline-block; + vertical-align: middle; +} + +.agent-setup-table tbody tr td { + transition: background-color 0.12s ease; +} + +.agent-setup-table tbody tr:hover td { + background-color: var(--sl-color-gray-7, #f5f5f5); +} + +:root[data-theme="dark"] .agent-setup-table tbody tr:hover td { + background-color: var(--color-cl1-gray-1); +} + +.agent-table-name { + font-weight: 600; + color: inherit; + text-decoration: none; + transition: color 0.15s ease; +} + +.agent-table-name:hover { + color: var(--color-cl1-orange-5); +} + +:root[data-theme="dark"] .agent-table-name:hover { + color: var(--color-cl1-orange-6); +} + +.agent-setup-table .check { + color: var(--color-cl1-green-5); +} + +.agent-setup-table .dash { + color: var(--color-cl1-gray-7); +} + +:root[data-theme="dark"] .agent-setup-table .dash { + color: var(--color-cl1-gray-3); +} + +/* ─── Quick start steps ─── + * Uses Starlight's native `.sl-steps` styling (24px counter bullets with + * hairline guide) — see node_modules/@astrojs/starlight/user-components/Steps.astro. + * These overrides tighten spacing and typography to match the rest of the + * agent-setup design system. + */ +.agent-setup-steps.sl-steps > li { + padding-bottom: 1rem; +} + +.agent-setup-steps.sl-steps > li:last-child { + padding-bottom: 1px; +} + +.agent-setup-step-title { + font-weight: 600; + font-size: 0.9375rem; + margin: 0 0 0.125rem 0; + line-height: 1.4; +} + +.agent-setup-step-description { + font-size: 0.875rem; + color: var(--color-cl1-gray-5); + margin: 0 0 0.5rem 0; + line-height: 1.5; +} + +:root[data-theme="dark"] .agent-setup-step-description { + color: var(--color-cl1-gray-6); +} + +/* Keep code blocks snug under their step description */ +.agent-setup-steps.sl-steps > li .expressive-code { + margin-top: 0.375rem; +} + +.agent-setup-inline-code { + background-color: var(--sl-color-bg-inline-code); + font-family: var(--sl-font-mono, monospace); + font-size: var(--sl-text-code-sm); + padding: 0.125rem 0.375rem; + border-radius: 4px; +} + +/* ─── Example prompts ─── */ +.agent-setup-prompts { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + column-gap: 0.5rem; + row-gap: 0.3rem; +} + +/* The whole chip is a button that copies the prompt text on click. */ +@media (max-width: 50rem) { + .agent-setup-prompt { + width: 100%; + } +} + +.agent-setup-prompt { + margin: 0 !important; + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem 0.5rem 0.875rem; + border-radius: 8px; + font-size: 0.8125rem; + line-height: 1.4; + font-family: inherit; + background: var(--color-cl1-gray-9); + border: 1px solid var(--color-cl1-gray-8); + color: inherit; + cursor: pointer; + text-align: left; + transition: + border-color 0.15s ease, + background-color 0.15s ease; +} + +.agent-setup-prompt:hover { + border-color: var(--color-cl1-gray-5); +} + +.agent-setup-prompt:focus-visible { + outline: 2px solid var(--color-cl1-brand-orange); + outline-offset: 2px; +} + +:root[data-theme="dark"] .agent-setup-prompt { + background: var(--color-cl1-gray-1); + border-color: var(--color-cl1-gray-2); +} + +:root[data-theme="dark"] .agent-setup-prompt:hover { + border-color: var(--color-cl1-gray-4); +} + +.agent-setup-prompt-text { + flex: 1; + min-width: 0; +} + +.agent-setup-prompt-icon { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 16px; + height: 16px; + vertical-align: middle; + color: var(--color-cl1-gray-5); + transition: color 0.15s ease; +} + +.agent-setup-prompt-icon svg { + width: 16px; + height: 16px; + vertical-align: middle; + margin: 0 !important; +} + +/* Swap copy icon for checkmark when the chip is in the "copied" state. */ +.agent-setup-prompt-icon-check { + display: none !important; +} + +.agent-setup-prompt[data-copied] .agent-setup-prompt-icon-copy { + display: none !important; +} + +.agent-setup-prompt[data-copied] .agent-setup-prompt-icon-check { + display: block !important; +} + +.agent-setup-prompt:hover .agent-setup-prompt-icon { + color: var(--color-cl1-gray-3); +} + +:root[data-theme="dark"] .agent-setup-prompt-icon { + color: var(--color-cl1-gray-6); +} + +:root[data-theme="dark"] .agent-setup-prompt:hover .agent-setup-prompt-icon { + color: var(--color-cl1-gray-8); +} + +.agent-setup-prompt[data-copied] .agent-setup-prompt-icon, +.agent-setup-prompt[data-copied]:hover .agent-setup-prompt-icon { + color: var(--color-cl1-green-4); +} + +:root[data-theme="dark"] + .agent-setup-prompt[data-copied] + .agent-setup-prompt-icon, +:root[data-theme="dark"] + .agent-setup-prompt[data-copied]:hover + .agent-setup-prompt-icon { + color: var(--color-cl1-green-7); +} + +/* ─── Workflows ─── */ +.agent-setup-workflow { + margin-bottom: 1.5rem; + border: 1px solid var(--color-cl1-gray-8); + border-radius: 8px; + overflow: hidden; +} + +:root[data-theme="dark"] .agent-setup-workflow { + border-color: var(--color-cl1-gray-2); +} + +.agent-setup-workflow-header { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + background: var(--sl-color-gray-7, #f5f5f5); + border-bottom: 1px solid var(--color-cl1-gray-8); + font-weight: 600; + font-size: 0.9375rem; +} + +:root[data-theme="dark"] .agent-setup-workflow-header { + background: var(--color-cl1-gray-1); + border-bottom-color: var(--color-cl1-gray-2); +} + +.agent-setup-workflow-steps { + padding: 0.75rem 1rem; + list-style: decimal; + list-style-position: inside; + margin: 0; + font-size: 0.875rem; +} + +.agent-setup-workflow-steps li { + padding: 0.375rem 0; + color: var(--color-cl1-gray-4); +} + +:root[data-theme="dark"] .agent-setup-workflow-steps li { + color: var(--color-cl1-gray-7); +} + +/* ─── FAQ ─── */ +.sl-markdown-content .agent-setup-faq-item, +.agent-setup-faq-item { + border-bottom: 1px solid var(--color-cl1-gray-8); + padding: 0; + margin: 0; + /* Override Starlight's orange accent — no left border on FAQ rows */ + border-inline-start: 0 none; + padding-inline-start: 0; +} + +:root[data-theme="dark"] .agent-setup-faq-item { + border-bottom-color: var(--color-cl1-gray-2); +} + +.sl-markdown-content .agent-setup-faq-item summary, +.agent-setup-faq-item summary { + padding: 0.875rem 0; + margin: 0; + font-weight: 600; + font-size: 0.9375rem; + cursor: pointer; + list-style: none; + display: flex; + justify-content: space-between; + align-items: center; + color: inherit; +} + +/* Hide Starlight's default disclosure marker and browser defaults */ +.sl-markdown-content .agent-setup-faq-item summary::-webkit-details-marker, +.agent-setup-faq-item summary::-webkit-details-marker { + display: none; +} + +.sl-markdown-content .agent-setup-faq-item summary::marker, +.agent-setup-faq-item summary::marker { + content: ""; +} + +/* Override Starlight's default summary::before chevron */ +.sl-markdown-content .agent-setup-faq-item summary::before, +.agent-setup-faq-item summary::before { + content: none; + display: none; +} + +.sl-markdown-content .agent-setup-faq-item summary::after, +.agent-setup-faq-item summary::after { + content: "+"; + font-size: 1.25rem; + color: var(--color-cl1-gray-5); + transition: transform 0.2s ease; +} + +.sl-markdown-content .agent-setup-faq-item[open] summary::after, +.agent-setup-faq-item[open] summary::after { + content: "−"; +} + +.sl-markdown-content .agent-setup-faq-answer, +.agent-setup-faq-answer { + padding: 0 0 1rem; + font-size: 0.875rem; + line-height: 1.6; + color: var(--color-cl1-gray-4); +} + +:root[data-theme="dark"] .agent-setup-faq-answer { + color: var(--color-cl1-gray-7); +} + +/* Tighten prose spacing inside FAQ answers */ +.sl-markdown-content .agent-setup-faq-answer > * + * { + margin-top: 0.75rem; +} + +.sl-markdown-content .agent-setup-faq-answer p { + margin: 0; + line-height: 1.6; +} + +/* ─── Troubleshooting ─── */ +.sl-markdown-content .agent-setup-troubleshooting, +.agent-setup-troubleshooting { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.sl-markdown-content .agent-setup-troubleshooting-item, +.agent-setup-troubleshooting-item { + border: 1px solid var(--color-cl1-gray-8); + border-radius: 8px; + background: transparent; + overflow: hidden; + transition: border-color 120ms ease; + /* Override Starlight's orange accent — restore uniform 1px border */ + border-inline-start: 1px solid var(--color-cl1-gray-8); + padding-inline-start: 0; + margin: 0; +} + +:root[data-theme="dark"] .agent-setup-troubleshooting-item { + border-color: var(--color-cl1-gray-2); +} + +.sl-markdown-content .agent-setup-troubleshooting-item:hover, +.agent-setup-troubleshooting-item:hover { + border-color: var(--color-cl1-gray-5); +} + +.sl-markdown-content .agent-setup-troubleshooting-item[open], +.agent-setup-troubleshooting-item[open] { + border-color: var(--color-cl1-gray-5); + background: var(--color-cl1-gray-9); +} + +:root[data-theme="dark"] .agent-setup-troubleshooting-item[open] { + background: var(--color-cl1-gray-1); +} + +.sl-markdown-content .agent-setup-troubleshooting-item > summary, +.agent-setup-troubleshooting-item > summary { + display: flex; + align-items: center; + gap: 0.625rem; + padding: 0.75rem 1rem; + margin: 0; + font-size: 0.9375rem; + font-weight: 500; + color: var(--color-cl1-gray-3); + cursor: pointer; + list-style: none; + user-select: none; +} + +:root[data-theme="dark"] .agent-setup-troubleshooting-item > summary { + color: var(--color-cl1-gray-8); +} + +.sl-markdown-content + .agent-setup-troubleshooting-item + > summary::-webkit-details-marker, +.agent-setup-troubleshooting-item > summary::-webkit-details-marker { + display: none; +} + +.sl-markdown-content .agent-setup-troubleshooting-item > summary:focus-visible, +.agent-setup-troubleshooting-item > summary:focus-visible { + outline: 2px solid var(--color-cl1-brand-orange); + outline-offset: -2px; +} + +.agent-setup-troubleshooting-dot { + flex-shrink: 0; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--color-cl1-brand-orange); + box-shadow: 0 0 0 3px + color-mix(in oklab, var(--color-cl1-brand-orange) 20%, transparent); +} + +.agent-setup-troubleshooting-issue-text { + flex: 1; + line-height: 1.45; +} + +.agent-setup-troubleshooting-chevron { + flex-shrink: 0; + color: var(--color-cl1-gray-5); + transition: transform 180ms ease; +} + +.sl-markdown-content + .agent-setup-troubleshooting-item[open] + .agent-setup-troubleshooting-chevron, +.agent-setup-troubleshooting-item[open] .agent-setup-troubleshooting-chevron { + transform: rotate(-180deg); +} + +.sl-markdown-content .agent-setup-troubleshooting-solution, +.agent-setup-troubleshooting-solution { + padding: 0 1rem 0.875rem calc(1rem + 8px + 0.625rem); + /* Align with issue text after the dot + gap on the summary row */ + font-size: 0.875rem; + line-height: 1.6; + color: var(--color-cl1-gray-4); +} + +:root[data-theme="dark"] .agent-setup-troubleshooting-solution { + color: var(--color-cl1-gray-7); +} + +.sl-markdown-content .agent-setup-troubleshooting-solution-label, +.agent-setup-troubleshooting-solution-label { + display: inline-flex; + align-items: center; + gap: 0.25rem; + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--color-cl1-green-4); + margin-bottom: 0.375rem; +} + +:root[data-theme="dark"] .agent-setup-troubleshooting-solution-label { + /* Light palette token reads better as "accent" on dark backgrounds. */ + color: var(--color-cl1-green-7); +} + +/* Tighten prose spacing inside troubleshooting solution body */ +.sl-markdown-content .agent-setup-troubleshooting-solution-body > * + * { + margin-top: 0.75rem; +} + +.sl-markdown-content .agent-setup-troubleshooting-solution-body p { + margin: 0; + line-height: 1.6; +} + +.agent-setup-troubleshooting-solution-body :global(code) { + background-color: var(--sl-color-bg-inline-code); + font-family: var(--sl-font-mono, monospace); + font-size: var(--sl-text-code-sm); + padding: 0.125rem 0.375rem; + border-radius: 4px; +} + +/* ─── Related agents ─── */ +.agent-setup-related { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 1rem; +} + +/* ─── Section spacing ─── */ +.agent-setup-section { + margin-bottom: 3rem; +} + +.agent-setup-section h2 { + font-size: 1.5rem; + font-weight: 700; + margin-bottom: 1rem; + letter-spacing: -0.01em; +} + +/* ─── Landing page section headers ─── */ +.agent-setup-section-header { + text-align: center; + margin-bottom: 2rem; +} + +.agent-setup-section-header h2 { + font-size: 1.75rem; + font-weight: 700; + letter-spacing: -0.02em; + margin-bottom: 0.5rem; + color: var(--sl-color-white); +} + +.agent-setup-section-header p { + font-size: 1rem; + color: var(--color-cl1-gray-5); +} + +:root[data-theme="dark"] .agent-setup-section-header p { + color: var(--color-cl1-gray-6); +} + +/* ─── Why Cloudflare grid ─── */ +.agent-setup-features { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 1.25rem; +} + +.agent-setup-feature { + padding: 1.25rem; + border: 1px solid var(--color-cl1-gray-8); + border-radius: 10px; + background: var(--sl-color-bg); +} + +:root[data-theme="dark"] .agent-setup-feature { + border-color: var(--color-cl1-gray-2); +} + +.agent-setup-feature h3 { + font-size: 0.9375rem; + font-weight: 600; + margin-bottom: 0.375rem; +} + +.agent-setup-feature p { + font-size: 0.8125rem; + line-height: 1.5; + color: var(--color-cl1-gray-5); + margin: 0; +} + +:root[data-theme="dark"] .agent-setup-feature p { + color: var(--color-cl1-gray-6); +} + +/* ─── Tips list ─── */ +.agent-setup-tips { + list-style: none; + padding: 0; + margin: 0; +} + +.agent-setup-tips li { + position: relative; + padding: 0.625rem 0 0.625rem 1.5rem; + font-size: 0.875rem; + line-height: 1.6; + border-bottom: 1px solid var(--color-cl1-gray-9); +} + +:root[data-theme="dark"] .agent-setup-tips li { + border-bottom-color: var(--color-cl1-gray-2); +} + +.agent-setup-tips li::before { + content: "→"; + position: absolute; + left: 0; + color: var(--color-cl1-orange-5); +} + +:root[data-theme="dark"] .agent-setup-tips li::before { + color: var(--color-cl1-orange-6); +} + +/* ─── External links row ─── */ +.agent-setup-links { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 2rem; +} + +.agent-setup-link { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.375rem 0.75rem; + border-radius: 6px; + font-size: 0.8125rem; + font-weight: 500; + border: 1px solid var(--color-cl1-gray-8); + color: inherit; + text-decoration: none; + transition: border-color 0.15s ease; +} + +.agent-setup-link:hover { + border-color: var(--color-cl1-gray-5); +} + +:root[data-theme="dark"] .agent-setup-link { + border-color: var(--color-cl1-gray-3); +} + +:root[data-theme="dark"] .agent-setup-link:hover { + border-color: var(--color-cl1-gray-5); +} + +[data-agent-tooltip] { + cursor: help; +} + +/* ─── Workflow filter strip ─── */ +.agent-filter-strip { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.75rem; + margin: 0 0 1.5rem; + padding: 0.75rem 0; +} + +.agent-filter-label { + font-size: 0.8125rem; + font-weight: 500; + color: var(--color-cl1-gray-5); +} + +:root[data-theme="dark"] .agent-filter-label { + color: var(--color-cl1-gray-6); +} + +.agent-filter-chips { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; +} + +.agent-filter-chip { + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + height: 34px; + min-width: 88px; + padding: 0 1rem; + border-radius: 9999px; + font-size: 0.8125rem; + font-weight: 500; + line-height: 1; + background: var(--sl-color-bg); + border: 1px solid var(--color-cl1-gray-8); + color: var(--color-cl1-gray-4); + cursor: pointer; + transition: + border-color 0.15s ease, + background 0.15s ease, + color 0.15s ease; + font-family: inherit; + white-space: nowrap; +} + +.agent-filter-chip:hover { + border-color: var(--color-cl1-orange-5); + color: var(--color-cl1-gray-2); +} + +.agent-filter-chip.is-active { + background: var(--color-cl1-brand-orange); + border-color: var(--color-cl1-brand-orange); + color: var(--color-cl1-black); +} + +.agent-filter-chip.is-active:hover { + opacity: 0.9; +} + +:root[data-theme="dark"] .agent-filter-chip { + border-color: var(--color-cl1-gray-3); + color: var(--color-cl1-gray-7); +} + +:root[data-theme="dark"] .agent-filter-chip:hover { + border-color: var(--color-cl1-orange-6); + color: var(--color-cl1-gray-9); +} + +:root[data-theme="dark"] .agent-filter-chip.is-active { + background: var(--color-cl1-brand-orange); + border-color: var(--color-cl1-brand-orange); + color: var(--color-cl1-black); +} + +.agent-filter-count { + margin-left: auto; + font-size: 0.8125rem; + color: var(--color-cl1-gray-5); +} + +:root[data-theme="dark"] .agent-filter-count { + color: var(--color-cl1-gray-6); +} + +.agent-filter-empty { + padding: 2rem; + border-radius: 10px; + background: var(--sl-color-gray-7, #f5f5f5); + text-align: center; +} + +:root[data-theme="dark"] .agent-filter-empty { + background: var(--color-cl1-gray-1); +} + +.agent-filter-empty p { + margin: 0 0 0.75rem; + color: var(--color-cl1-gray-4); +} + +:root[data-theme="dark"] .agent-filter-empty p { + color: var(--color-cl1-gray-7); +} + +/* ─── Catalog grid ─── */ +.agent-setup-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1rem; +} + +@media (min-width: 40rem) { + .agent-setup-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (min-width: 64rem) { + .agent-setup-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +/* ─── Card "best for" line ─── */ +.agent-setup-card-bestfor { + font-size: 0.8125rem; + line-height: 1.5; + margin: 0; + padding: 0.625rem 0.75rem; + border-radius: 6px; + background: var(--sl-color-gray-7, #f5f5f5); + color: var(--color-cl1-gray-4); +} + +.agent-setup-card-bestfor strong { + color: var(--color-cl1-gray-3); + font-weight: 600; +} + +:root[data-theme="dark"] .agent-setup-card-bestfor { + background: var(--color-cl1-gray-1); + color: var(--color-cl1-gray-7); +} + +:root[data-theme="dark"] .agent-setup-card-bestfor strong { + color: var(--color-cl1-gray-8); +} + +/* ─── Comparison table controls ─── */ +.agent-table-controls { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 1rem; + margin-bottom: 0.75rem; +} + +.agent-table-hint { + font-size: 0.75rem; + color: var(--color-cl1-gray-6); +} + +/* ─── Sortable column headers ─── */ +.agent-table-sort, +.agent-table-sort:hover, +.agent-table-sort:focus, +.agent-table-sort:active { + display: inline-flex; + align-items: center; + gap: 0.375rem; + background: none !important; + background-color: transparent !important; + border: none !important; + outline: none !important; + box-shadow: none !important; + padding: 0; + margin: 0; + font: inherit; + font-size: inherit; + font-weight: inherit; + letter-spacing: inherit; + text-transform: inherit; + cursor: pointer; + font-family: inherit; + text-decoration: none; +} + +.agent-table-sort { + color: inherit; + transition: color 0.15s ease; +} + +.agent-table-sort:hover { + color: var(--color-cl1-orange-5); +} + +:root[data-theme="dark"] .agent-table-sort:hover { + color: var(--color-cl1-orange-6); +} + +.agent-table-sort.is-active { + color: var(--color-cl1-orange-5); +} + +:root[data-theme="dark"] .agent-table-sort.is-active { + color: var(--color-cl1-orange-6); +} + +.agent-table-sort:focus-visible { + outline: 2px solid var(--color-cl1-orange-5) !important; + outline-offset: 2px; + border-radius: 3px; +} + +.agent-table-sort-indicator { + font-size: 0.75rem; + opacity: 0.9; + font-weight: 700; +} + +/* ─── Comparison table cells ─── */ +.agent-table-bestfor { + font-size: 0.8125rem; + line-height: 1.4; + max-width: 240px; + color: var(--color-cl1-gray-4); +} + +:root[data-theme="dark"] .agent-table-bestfor { + color: var(--color-cl1-gray-7); +} + +.agent-table-dim { + color: var(--color-cl1-gray-6); +} + +/* ─── Additional badge color variants ─── */ +.agent-setup-badge--free { + background: var(--color-cl1-green-9); + color: var(--color-cl1-green-3); +} + +:root[data-theme="dark"] .agent-setup-badge--free { + background: var(--color-cl1-green-1); + color: var(--color-cl1-green-7); +} + +.agent-setup-badge--subscription { + background: var(--color-cl1-blue-9); + color: var(--color-cl1-blue-3); +} + +:root[data-theme="dark"] .agent-setup-badge--subscription { + background: var(--color-cl1-blue-1); + color: var(--color-cl1-blue-7); +} + +.agent-setup-badge--usage-based { + background: var(--color-cl1-violet-9); + color: var(--color-cl1-violet-3); +} + +:root[data-theme="dark"] .agent-setup-badge--usage-based { + background: var(--color-cl1-violet-1); + color: var(--color-cl1-violet-7); +} + +.agent-setup-badge--byok { + background: var(--color-cl1-cyan-9); + color: var(--color-cl1-cyan-3); +} + +:root[data-theme="dark"] .agent-setup-badge--byok { + background: var(--color-cl1-cyan-1); + color: var(--color-cl1-cyan-7); +} + +.agent-setup-badge--hybrid { + background: var(--color-cl1-orange-9); + color: var(--color-cl1-orange-3); +} + +:root[data-theme="dark"] .agent-setup-badge--hybrid { + background: var(--color-cl1-orange-1); + color: var(--color-cl1-orange-7); +} + +.agent-setup-badge--locked { + background: var(--color-cl1-gray-9); + color: var(--color-cl1-gray-3); +} + +:root[data-theme="dark"] .agent-setup-badge--locked { + background: var(--color-cl1-gray-1); + color: var(--color-cl1-gray-7); +} + +.agent-setup-badge--byo-provider { + background: var(--color-cl1-cyan-9); + color: var(--color-cl1-cyan-3); +} + +:root[data-theme="dark"] .agent-setup-badge--byo-provider { + background: var(--color-cl1-cyan-1); + color: var(--color-cl1-cyan-7); +} + +.agent-setup-badge--multi-provider { + background: var(--color-cl1-green-9); + color: var(--color-cl1-green-3); +} + +:root[data-theme="dark"] .agent-setup-badge--multi-provider { + background: var(--color-cl1-green-1); + color: var(--color-cl1-green-7); +} + +.agent-setup-badge--suggest { + background: var(--color-cl1-gray-9); + color: var(--color-cl1-gray-3); +} + +:root[data-theme="dark"] .agent-setup-badge--suggest { + background: var(--color-cl1-gray-1); + color: var(--color-cl1-gray-7); +} + +.agent-setup-badge--approve { + background: var(--color-cl1-blue-9); + color: var(--color-cl1-blue-3); +} + +:root[data-theme="dark"] .agent-setup-badge--approve { + background: var(--color-cl1-blue-1); + color: var(--color-cl1-blue-7); +} + +.agent-setup-badge--autonomous { + background: var(--color-cl1-violet-9); + color: var(--color-cl1-violet-3); +} + +:root[data-theme="dark"] .agent-setup-badge--autonomous { + background: var(--color-cl1-violet-1); + color: var(--color-cl1-violet-7); +} + +.agent-setup-badge--session { + background: var(--color-cl1-gray-9); + color: var(--color-cl1-gray-3); +} + +:root[data-theme="dark"] .agent-setup-badge--session { + background: var(--color-cl1-gray-1); + color: var(--color-cl1-gray-7); +} + +.agent-setup-badge--project-memory { + background: var(--color-cl1-blue-9); + color: var(--color-cl1-blue-3); +} + +:root[data-theme="dark"] .agent-setup-badge--project-memory { + background: var(--color-cl1-blue-1); + color: var(--color-cl1-blue-7); +} + +.agent-setup-badge--indexed-codebase { + background: var(--color-cl1-green-9); + color: var(--color-cl1-green-3); +} + +:root[data-theme="dark"] .agent-setup-badge--indexed-codebase { + background: var(--color-cl1-green-1); + color: var(--color-cl1-green-7); +} + +/* ─── Agent primer ─── */ +.agent-primer { + display: flex; + flex-direction: column; + gap: 2.5rem; +} + +.agent-primer-intro { + font-size: 1rem; + line-height: 1.7; + color: var(--color-cl1-gray-4); + max-width: 780px; + margin: 0; +} + +:root[data-theme="dark"] .agent-primer-intro { + color: var(--color-cl1-gray-7); +} + +.agent-primer-block { + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.agent-primer-block-header h3 { + font-size: 1.125rem; + font-weight: 600; + margin: 0 0 0.25rem; + letter-spacing: -0.01em; +} + +.agent-primer-block-header p { + margin: 0; + font-size: 0.875rem; + color: var(--color-cl1-gray-5); +} + +:root[data-theme="dark"] .agent-primer-block-header p { + color: var(--color-cl1-gray-6); +} + +/* ─── Agent types grid ─── */ +.agent-primer-types { + display: grid; + grid-template-columns: 1fr; + gap: 0.875rem; +} + +@media (min-width: 40rem) { + .agent-primer-types { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (min-width: 64rem) { + .agent-primer-types { + grid-template-columns: repeat(4, 1fr); + } +} + +.agent-primer-type { + position: relative; + padding: 1.25rem; + border: 1px solid var(--color-cl1-gray-8); + border-radius: 12px; + background: var(--sl-color-bg); +} + +:root[data-theme="dark"] .agent-primer-type { + border-color: var(--color-cl1-gray-2); +} + +.agent-primer-type-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 10px; + background: var(--color-cl1-orange-9); + color: var(--color-cl1-orange-5); + margin-bottom: 0.875rem; +} + +:root[data-theme="dark"] .agent-primer-type-icon { + background: var(--color-cl1-orange-1); + color: var(--color-cl1-orange-6); +} + +.agent-primer-type-label { + font-size: 0.9375rem; + font-weight: 600; + letter-spacing: -0.01em; + margin-bottom: 0.375rem; + color: var(--color-cl1-gray-2); +} + +:root[data-theme="dark"] .agent-primer-type-label { + color: var(--color-cl1-gray-9); +} + +.agent-primer-type p { + font-size: 0.8125rem; + line-height: 1.5; + margin: 0; + color: var(--color-cl1-gray-5); +} + +:root[data-theme="dark"] .agent-primer-type p { + color: var(--color-cl1-gray-7); +} + +/* ─── Key concepts grid ─── */ +.agent-primer-concepts { + display: grid; + grid-template-columns: 1fr; + gap: 0; + border: 1px solid var(--color-cl1-gray-8); + border-radius: 12px; + overflow: hidden; + background: var(--sl-color-bg); +} + +:root[data-theme="dark"] .agent-primer-concepts { + border-color: var(--color-cl1-gray-2); +} + +@media (min-width: 40rem) { + .agent-primer-concepts { + grid-template-columns: repeat(2, 1fr); + } +} + +.agent-primer-concept { + padding: 1.25rem; + border-bottom: 1px solid var(--color-cl1-gray-8); +} + +.agent-primer-concept:last-child { + border-bottom: none; +} + +:root[data-theme="dark"] .agent-primer-concept { + border-bottom-color: var(--color-cl1-gray-2); +} + +@media (min-width: 40rem) { + .agent-primer-concept { + border-right: 1px solid var(--color-cl1-gray-8); + } + + .agent-primer-concept:nth-child(2n) { + border-right: none; + } + + .agent-primer-concept:nth-last-child(-n + 2) { + border-bottom: none; + } + + :root[data-theme="dark"] .agent-primer-concept { + border-right-color: var(--color-cl1-gray-2); + } +} + +.agent-primer-concept-term { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.9375rem; + font-weight: 600; + margin-bottom: 0.5rem; + color: var(--color-cl1-gray-2); +} + +:root[data-theme="dark"] .agent-primer-concept-term { + color: var(--color-cl1-gray-9); +} + +.agent-primer-concept-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + border-radius: 6px; + background: var(--color-cl1-orange-9); + color: var(--color-cl1-orange-5); + flex-shrink: 0; +} + +:root[data-theme="dark"] .agent-primer-concept-icon { + background: var(--color-cl1-orange-1); + color: var(--color-cl1-orange-6); +} + +.agent-primer-concept p { + font-size: 0.875rem; + line-height: 1.6; + margin: 0; + color: var(--color-cl1-gray-4); +} + +:root[data-theme="dark"] .agent-primer-concept p { + color: var(--color-cl1-gray-7); +} + +.agent-primer-concept strong { + font-weight: 600; + color: var(--color-cl1-gray-2); +} + +:root[data-theme="dark"] .agent-primer-concept strong { + color: var(--color-cl1-gray-8); +} + +/* ─── Tradeoffs grid ─── */ +.agent-primer-tradeoff-grid { + display: grid; + grid-template-columns: 1fr; + gap: 0.875rem; +} + +@media (min-width: 40rem) { + .agent-primer-tradeoff-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +.agent-primer-tradeoff { + padding: 1.125rem 1.25rem; + border: 1px solid var(--color-cl1-gray-8); + border-radius: 12px; + background: var(--sl-color-bg); +} + +:root[data-theme="dark"] .agent-primer-tradeoff { + border-color: var(--color-cl1-gray-2); +} + +.agent-primer-tradeoff-row { + margin-bottom: 0.625rem; + font-size: 0.9375rem; + font-weight: 600; + letter-spacing: -0.01em; + color: var(--color-cl1-gray-2); +} + +:root[data-theme="dark"] .agent-primer-tradeoff-row { + color: var(--color-cl1-gray-9); +} + +.agent-primer-tradeoff-opt { + color: inherit; +} + +.agent-primer-tradeoff-vs { + font-weight: 400; + color: var(--color-cl1-gray-5); + margin: 0 0.25rem; +} + +:root[data-theme="dark"] .agent-primer-tradeoff-vs { + color: var(--color-cl1-gray-6); +} + +.agent-primer-tradeoff p { + font-size: 0.875rem; + line-height: 1.6; + margin: 0; + color: var(--color-cl1-gray-4); +} + +:root[data-theme="dark"] .agent-primer-tradeoff p { + color: var(--color-cl1-gray-7); +} + +/* ─── Per-token badge colors for the comparison table ───────────── + Each value in Pricing / Model / Context gets its own unique color + so they can be distinguished at a glance — and no two values + across all three columns share a color. These overrides win the + cascade because .agent-token-badge is applied alongside + .agent-setup-badge--{token}. */ + +/* Pricing — subscription: blue */ +.agent-token-badge--subscription { + background: var(--color-cl1-blue-9); + color: var(--color-cl1-blue-3); +} +:root[data-theme="dark"] .agent-token-badge--subscription { + background: var(--color-cl1-blue-1); + color: var(--color-cl1-blue-7); +} + +/* Pricing — hybrid: violet */ +.agent-token-badge--hybrid { + background: var(--color-cl1-violet-9); + color: var(--color-cl1-violet-3); +} +:root[data-theme="dark"] .agent-token-badge--hybrid { + background: var(--color-cl1-violet-1); + color: var(--color-cl1-violet-7); +} + +/* Pricing — BYOK: cyan */ +.agent-token-badge--byok { + background: var(--color-cl1-cyan-9); + color: var(--color-cl1-cyan-3); +} +:root[data-theme="dark"] .agent-token-badge--byok { + background: var(--color-cl1-cyan-1); + color: var(--color-cl1-cyan-7); +} + +/* Model — locked: gold */ +.agent-token-badge--locked { + background: var(--color-cl1-gold-9); + color: var(--color-cl1-gold-3); +} +:root[data-theme="dark"] .agent-token-badge--locked { + background: var(--color-cl1-gold-1); + color: var(--color-cl1-gold-7); +} + +/* Model — multi-provider: orange (brand accent) */ +.agent-token-badge--multi-provider { + background: var(--color-cl1-orange-9); + color: var(--color-cl1-orange-3); +} +:root[data-theme="dark"] .agent-token-badge--multi-provider { + background: var(--color-cl1-orange-1); + color: var(--color-cl1-orange-6); +} + +/* Context — project memory: green */ +.agent-token-badge--project-memory { + background: var(--color-cl1-green-9); + color: var(--color-cl1-green-3); +} +:root[data-theme="dark"] .agent-token-badge--project-memory { + background: var(--color-cl1-green-1); + color: var(--color-cl1-green-7); +} + +/* Context — session: gray */ +.agent-token-badge--session { + background: var(--color-cl1-gray-9); + color: var(--color-cl1-gray-3); +} +:root[data-theme="dark"] .agent-token-badge--session { + background: var(--color-cl1-gray-2); + color: var(--color-cl1-gray-7); +} + +/* Context — indexed codebase: indigo */ +.agent-token-badge--indexed-codebase { + background: var(--color-cl1-indigo-9); + color: var(--color-cl1-indigo-3); +} +:root[data-theme="dark"] .agent-token-badge--indexed-codebase { + background: var(--color-cl1-indigo-1); + color: var(--color-cl1-indigo-7); +} + +/* Hidden attribute needs an explicit override when its element is a flex/grid + item — browsers don't always apply display:none over display:flex. */ +.agent-setup-card[hidden], +[data-filter-empty][hidden], +[data-filter-count][hidden] { + display: none !important; +} + +/* ─── Footnote below the comparison table ─── */ +.agent-table-footnote { + margin: 0.75rem 0 0; + font-size: 0.8125rem; + color: var(--color-cl1-gray-5); + text-align: center; +} + +:root[data-theme="dark"] .agent-table-footnote { + color: var(--color-cl1-gray-6); +} + +/* ──────────────────────────────────────────────────────────────────── + Agent detail page — Platform access deep-dive + + Each block is a bespoke (.agent-platform-card) + styled as a bordered card so it reads as a discrete "area" rather than + a run of indented headings. + + Uses high-specificity selectors (.sl-markdown-content .agent-platform-card) + so our card styling beats Starlight's default prose styling for
    + and WITHOUT needing a `.not-content` wrapper. That keeps inline + , hyperlinks, and other prose elements inside the body correctly + themed by Starlight's markdown styles. + ──────────────────────────────────────────────────────────────────── */ +.agent-setup-platform-access { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +/* Short intro sentence above the four Details cards. */ +.agent-setup-platform-access .agent-platform-lede { + margin: 0 !important; + padding: 0 !important; + font-size: 0.875rem; + line-height: 1.5; + color: var(--color-cl1-gray-4); +} + +/* + * Card reset + border. The `:where()` selectors in Starlight's + * `.sl-markdown-content details` rules have zero specificity, so a single + * class selector here is enough to beat them without !important. + */ +.sl-markdown-content .agent-platform-card, +.agent-platform-card { + margin: 0; + padding: 0; + border: 1px solid var(--color-cl1-gray-8); + border-radius: 10px; + background: var(--color-cl1-gray-9); + overflow: hidden; + transition: + border-color 0.15s ease, + background-color 0.15s ease; + /* Cancel Starlight's `border-inline-start: 2px solid` + `padding-inline-start: 1rem` */ + border-inline-start-width: 1px; + padding-inline-start: 0; +} + +:root[data-theme="dark"] .agent-platform-card { + border-color: var(--color-cl1-gray-2); + background: var(--color-cl1-gray-1); +} + +/* Closed state: subtle hover on the whole card. */ +.agent-platform-card:not([open]):hover { + border-color: var(--color-cl1-gray-5); +} + +:root[data-theme="dark"] .agent-platform-card:not([open]):hover { + border-color: var(--color-cl1-gray-4); +} + +/* Open state: stronger border + slightly different bg so it stands out. */ +.agent-platform-card[open] { + border-color: var(--color-cl1-gray-6); + background: var(--sl-color-bg); + /* Card owns the side padding — children get zero horizontal padding. */ + padding: 0 1rem 1rem; +} + +:root[data-theme="dark"] .agent-platform-card[open] { + border-color: var(--color-cl1-gray-3); + background: var(--color-cl1-gray-1); +} + +/* + * The . Uses its own class so we can beat Starlight's + * `.sl-markdown-content summary` rule without !important or `.not-content`. + */ +.sl-markdown-content .agent-platform-card-summary, +.agent-platform-card-summary { + padding: 0.875rem 1rem 0.875rem 1.75rem; + margin: 0; + list-style: none; + font-weight: 600; + font-size: 0.9375rem; + line-height: 1.5; + position: relative; + cursor: pointer; + color: inherit; +} + +.agent-platform-card-summary::-webkit-details-marker, +.agent-platform-card-summary::marker { + display: none; + content: ""; +} + +/* + * Custom chevron — overrides Starlight's default `summary::before` marker + * (which would show a larger arrow icon on the left). We replace its + * mask-image with our own chevron SVG and position it absolutely inside the + * summary's left padding. High specificity (`.sl-markdown-content ...`) + * ensures we beat Starlight's default `.sl-markdown-content summary::before`. + */ +.sl-markdown-content .agent-platform-card-summary::before, +.agent-platform-card-summary::before { + content: ""; + position: absolute; + left: 0.625rem; + top: 50%; + width: 16px; + height: 16px; + background: currentColor; + opacity: 0.6; + transform: translateY(-50%); + transition: transform 0.2s ease; + mask-image: url("data:image/svg+xml;utf8,"); + -webkit-mask-image: url("data:image/svg+xml;utf8,"); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-position: center; + /* Reset Starlight's default marker sizing + inline positioning — we use absolute */ + --sl-details-marker-size: 16px; + display: block; + margin-inline: 0; + vertical-align: baseline; +} + +.agent-platform-card[open] > .agent-platform-card-summary::before { + transform: translateY(-50%) rotate(90deg); + opacity: 1; +} + +/* Open body gets a top divider between summary and content. */ +.sl-markdown-content .agent-platform-card[open] > .agent-platform-card-summary, +.agent-platform-card[open] > .agent-platform-card-summary { + border-bottom: 1px solid var(--color-cl1-gray-8); + margin-bottom: 0; +} + +:root[data-theme="dark"] + .agent-platform-card[open] + > .agent-platform-card-summary { + border-bottom-color: var(--color-cl1-gray-2); +} + +/* The body wrapper — everything inside the card after . */ +.agent-platform-card-body { + font-size: 0.875rem; + line-height: 1.55; +} + +/* + * Tighten prose spacing inside the card. Starlight's default + * `.sl-markdown-content :not(...) + :not(...) { margin-top: var(--sl-content-gap-y) }` + * uses ~1.5rem which is too spacious for a compact card. + */ +.sl-markdown-content .agent-platform-card-body > * + * { + margin-top: 0.75rem; +} + +.sl-markdown-content .agent-platform-card-body p { + margin: 0; + line-height: 1.55; +} + +/* + * Hint line directly under the summary — small + muted. + * Force zero top-margin regardless of whether the hint is the first element + * (Astro-authored) or arrives via an MDX slot (where Starlight's > * + * + * rule would otherwise add spacing). + */ +.sl-markdown-content .agent-platform-card .agent-platform-hint, +.agent-platform-card .agent-platform-hint, +.sl-markdown-content .agent-platform-card .agent-platform-hint p, +.agent-platform-card .agent-platform-hint p { + margin: 0 !important; + margin-top: 0 !important; + padding: 0; + font-size: 0.8125rem; + color: var(--color-cl1-gray-5); +} + +.sl-markdown-content .agent-platform-card .agent-platform-hint + *, +.agent-platform-card .agent-platform-hint + * { + margin-top: 0.625rem !important; +} + +/* + * Code blocks inside platform-access cards. Tailwind preflight resets + * `border: 0px solid` on all elements, which wins against Expressive Code's + * layered `.frame { border: 1px solid }` when Starlight's user-components are + * imported in an Astro page (CSS layer flip). We restore the border here, + * unlayered, so the frame is visible inside these cards. + */ +.agent-platform-card .expressive-code figure.frame { + border: 1px solid var(--color-cl1-gray-8); + border-radius: 6px; + overflow: hidden; +} + +:root[data-theme="dark"] .agent-platform-card .expressive-code figure.frame { + border-color: var(--color-cl1-gray-2); +} + +/* Also restore the copy-button hover state — same issue as above. */ +.agent-platform-card .expressive-code .copy button { + border: 1px solid transparent; +} + +.agent-platform-card .expressive-code .copy button:hover { + border-color: var(--color-cl1-gray-6); +} + +/* + * "What's next" inline note inside a platform card. Rendered as an