diff --git a/.gitignore b/.gitignore index a290cce8a..b5c32bb93 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,6 @@ docs/*/images/ # Python bytecode cache (from scripts/) __pycache__/ -*.pyc \ No newline at end of file +*.pyc +# Wrangler local state +/.wrangler diff --git a/.python-version b/.python-version new file mode 100644 index 000000000..6324d401a --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/CLAUDE.md b/CLAUDE.md index f44844bcc..e7feccacb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,10 +29,35 @@ docker build -t wallarm-docs . && docker run -p 8080:80 wallarm-docs ### Deployment pipeline -* There are no tests or linters. Validation happens via **Netlify preview builds**. -* Creating a **PR to `master`** triggers a Netlify test build — the preview link appears in the PR checks (takes 3-5 minutes). -* **Merging to `master`** triggers production deployment via Netlify. -* Build configuration is in `netlify.toml`. +The site is **migrating from Netlify to Cloudflare** (DEVOPS-5014). Both run in +parallel until the cutover, so a PR gets a preview from each. + +* There are no tests or linters. Validation happens via **preview builds**. +* Creating a **PR to `master`** triggers a build — the preview link appears in + the PR checks (takes 3-5 minutes). +* **Merging to `master`** deploys production. +* Build configuration: `scripts/build.sh` (Cloudflare) and `netlify.toml` + (Netlify). **They must be kept in sync until Netlify is retired.** + +#### Cloudflare specifics + +* Hosting is **Workers Static Assets** — `wrangler.jsonc`, publish dir `site/`. + There is no Worker script; it is a plain static deployment. +* Redirects, headers, and `Accept: text/markdown` negotiation are split between + this repo (`_redirects`, `_headers`) and zone rules in + [infra/cloudflare-iac](https://gl.wallarm.com/infra/cloudflare-iac). +* **Preview URLs do not exercise zone rules.** Previews are served from + `*.workers.dev`, where redirects, markdown negotiation and WAF do not apply. + A preview shows *content* accurately but not *routing* — verify any redirect + or negotiation change on `docs-staging.wallarm.com` instead. +* `_redirects` has a hard limit: Cloudflare rejects the whole deployment past + **100 dynamic rules**, and counts every rule appearing *after the first + wildcard rule* toward that budget. Keep plain rules above the wildcard block + at the bottom of the file — the file says so too. +* Every page must have a `.md` companion, enforced by + `scripts/check_markdown_companions.py` in the build. A page without one is + not a fallback to HTML: it returns the 404 page labelled + `Content-Type: text/markdown`. ## Repository architecture @@ -98,14 +123,19 @@ The `_redirects` file lives in the root version's `docs_dir` (currently `docs/6. /old/path/page /new/path/page ``` -This prevents 404 errors for users with bookmarked URLs. Redirect syntax supports wildcards (`/*`). See [Netlify redirect docs](https://docs.netlify.com/routing/redirects/). +This prevents 404 errors for users with bookmarked URLs. Redirect syntax supports wildcards (`/*`). + +**Add plain path-to-path redirects ABOVE the wildcard block at the bottom of the file.** Cloudflare counts every rule after the first wildcard against a 100-rule budget and rejects the entire deployment past it. See the header comment in `_redirects`, plus the [Cloudflare](https://developers.cloudflare.com/workers/static-assets/redirects/) and [Netlify](https://docs.netlify.com/routing/redirects/) references. ### Key platform files | File | Purpose | |------|---------| | `mkdocs-base.yml` | Shared config: plugins, extensions, theme | -| `netlify.toml` | Build commands and deploy config (edit only for version management) | +| `netlify.toml` | Netlify build/deploy config (edit only for version management) | +| `scripts/build.sh` | Cloudflare build; mirrors `netlify.toml`, keep in sync | +| `wrangler.jsonc` | Cloudflare Workers Static Assets deploy config | +| `docs/6.x/_headers` | Response headers, Cloudflare | | `stylesheets/extra.js` | Custom JS: image zoom, version selector logic, `rootVersion` variable | | `stylesheets/partials/` | Custom HTML overrides: `nav.html` (version selector), `header.html`, `footer.html`, `feedback.html`, `actions.html` (edit actions), `toc.html`, `integrations/` | | `Dockerfile` | Full multi-version build for local testing | @@ -134,7 +164,7 @@ Follow these guides before writing or editing any content: ## What NOT to do * Do NOT edit `mkdocs-base.yml` unless explicitly required -* Do NOT edit `netlify.toml` or files in `stylesheets/` except for version management tasks (adding/deprecating a guide version) +* Do NOT edit `netlify.toml`, `scripts/build.sh`, `wrangler.jsonc` or files in `stylesheets/` except for version management tasks (adding/deprecating a guide version) * Do NOT edit files in `docs/6.x/` or `docs/7.x/` directly — they are include wrappers (exception: creating new wrappers for new pages) * Do NOT invent features not described in the source material * Do NOT add content in languages other than English diff --git a/DEPLOY-CLOUDFLARE.md b/DEPLOY-CLOUDFLARE.md new file mode 100644 index 000000000..e46df3db8 --- /dev/null +++ b/DEPLOY-CLOUDFLARE.md @@ -0,0 +1,110 @@ +# Cloudflare deployment + +Hosting for docs.wallarm.com on **Workers Static Assets**. Tracked in +[DEVOPS-5014](https://wallarm.atlassian.net/browse/DEVOPS-5014). + +Netlify still builds and serves production. Nothing here is live until the +cutover, and `netlify.toml` is deliberately untouched so rollback stays trivial. + +## What lives where + +| Concern | Here | infra/cloudflare-iac | +|---|---|---| +| Build | `scripts/build.sh` | — | +| Deploy config | `wrangler.jsonc` | — | +| Response headers | `docs/6.x/_headers` | `Vary`, `Link` for negotiable pages | +| Redirects | `docs/6.x/_redirects` | legacy `/2.x`–`/4.10` catch-all | +| `Accept: text/markdown` | `.md` companions from the build | the rewrite rules | +| DNS, WAF, bot management | — | all of it | + +There is **no Worker script**. Static Assets is served by a code-less Worker +entry, which is why asset requests are free and never invoke anything. + +## Workers Builds settings + +Workers Builds is configured in the Cloudflare dashboard, not in this repo, so +the settings are recorded here. + +| Setting | Value | +|---|---| +| Repository | `wallarm/product-documentation` | +| Production branch | `master` | +| Build command | `git submodule update --init --recursive && scripts/build.sh` | +| Deploy command | `npx wrangler deploy` | +| Non-production deploy command | `npx wrangler versions upload` | +| Build variable | `CONTEXT=production` | +| Build variable | `PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true` | + +Notes on each of those: + +* **Submodules are not cloned automatically.** The build fails without them — + terraform module snippets are included from `docs/latest/`. Both submodules + are public, so anonymous HTTPS is enough. +* **Python** is pinned by `.python-version` (3.14). The build image defaults to + 3.13. +* **`CONTEXT=production`** is what enables image optimisation, the `.md` + companions, OG images and feeds. Without it the companions are not generated + and markdown negotiation returns 404s. `scripts/build.sh` mirrors the variable + Netlify sets, so the two stay comparable. +* **`PUPPETEER_SKIP_CHROMIUM_DOWNLOAD`** — `package.json` carries puppeteer for + the PDF tooling, which the docs build does not use. Workers Builds installs + dependencies automatically, and without this it downloads ~170 MB of Chromium + on every build. + +Build takes roughly 3-6 minutes on a 4-vCPU runner; measured at 1m35s wall +clock locally with 518s of CPU across 6 cores. The platform timeout is 20 +minutes, so there is comfortable headroom. + +## Preview builds do not test routing + +Previews are served from `*.workers.dev`, and **zone rules do not apply there**. +Verified against staging: + +| | `*.workers.dev` | `docs-staging.wallarm.com` | +|---|---|---| +| `Accept: text/markdown` | `text/html` | `text/markdown` | +| `/4.8/admin-en/foo/` | `404` | `301` | +| `Vary` | absent | present | + +So a preview shows **content** accurately but not **routing**. Any change to +redirects, headers or markdown negotiation must be checked on +`docs-staging.wallarm.com`, which is a real hostname on the zone and behaves +exactly like production. + +For the same reason `workers_dev` is `false` on production: it would publish the +whole site a second time with no redirects, no negotiation, no WAF, and +indexable by search engines. + +## Two things that will break the build + +**Redirect ordering.** Cloudflare rejects the entire deployment past 100 +dynamic `_redirects` rules, and counts every rule appearing *after the first +wildcard rule* toward that budget regardless of its own shape. Plain rules go +above the wildcard block at the bottom of the file. + +**Missing markdown companions.** `scripts/check_markdown_companions.py` fails +the build if a page has no `.md`. This is deliberate: the negotiation is a blind +URL rewrite, so a missing companion does not fall back to HTML — it returns the +404 page labelled `Content-Type: text/markdown`. Its exemption list must stay in +sync with `local.md_no_companion` in the IaC repo. + +## Cutover + +1. Connect Workers Builds with the settings above; confirm a `master` build + deploys and a PR gets a preview. +2. Widen the zone rules in `infra/cloudflare-iac` from + `docs-staging.wallarm.com` to include `docs.wallarm.com`. +3. Point Cloudflare's record for the proxied hostname at the Worker. **This is a + Cloudflare-side DNS change, not a Route53 one** — `wallarm.com` is a partial + zone, and Route53 already delegates `docs` to Cloudflare. Today that record + reads `CNAME docs.wallarm.com -> pensive-dubinsky-5f7a00.netlify.app`. +4. Verify, then ask Anastasiia Popova to check. +5. Once stable, delete `netlify.toml`, `netlify/`, and the duplication between + `scripts/build.sh` and the Netlify build command. + +**Rollback** is restoring that one CNAME to the Netlify value. Netlify keeps +building from the same branch throughout, so its copy stays current. + +`cloudflare_workers_custom_domain` does **not** work here: Custom Domains refuse +a hostname that already carries a CNAME and are documented against full zones. +Production takes the same route-based binding staging uses. diff --git a/docs/6.x/_headers b/docs/6.x/_headers new file mode 100644 index 000000000..e2a8ad038 --- /dev/null +++ b/docs/6.x/_headers @@ -0,0 +1,74 @@ +# Static response headers for docs.wallarm.com. Port of the netlify.toml +# [[headers]] blocks (DEVOPS-5014). Lives in the root version's docs_dir so +# zensical copies it to the publish dir root, same as _redirects. +# +# Cloudflare's `*` is greedy and matches `/`, so one `/*.md` rule covers both +# `/foo.md` and `/7.x/a/b.md` — no `/**/*.md` twin needed as on Netlify. +# +# NOTE: these rules apply only to assets served directly by the asset server. +# The Worker (src/worker.ts) runs first for extension-less page URLs and sets +# the site-wide security headers itself, because `_headers` is not applied to +# responses returned from Worker code. + +/* + Content-Security-Policy: frame-ancestors * + X-Content-Type-Options: nosniff + Strict-Transport-Security: max-age=63072000; includeSubDomains + +# Raw .md / llms*.txt companions, served as plain UTF-8 text. CORS is open so +# AI tools and browser-side fetchers can read them cross-origin (Cloudflare / +# Anthropic / Stripe do the same on their .md docs); X-Robots-Tag tells search +# engines not to index these as duplicates of the HTML pages (the markdown has +# no
, so a robots meta cannot live in the file itself); the 1-hour +# Cache-Control prevents CDN/browser thrash. +/*.md + Content-Type: text/markdown; charset=utf-8 + Access-Control-Allow-Origin: * + Access-Control-Allow-Methods: GET, OPTIONS + Cache-Control: public, max-age=3600, must-revalidate + X-Robots-Tag: noindex, follow + +/llms*.txt + Content-Type: text/plain; charset=utf-8 + Access-Control-Allow-Origin: * + Access-Control-Allow-Methods: GET, OPTIONS + Cache-Control: public, max-age=3600, must-revalidate + X-Robots-Tag: noindex, follow + +# Aggregated changelog feeds (scripts/generate_feeds.py). Atom is served as +# application/atom+xml so RSS readers and Slack's /feed app recognise it. CORS +# is open so scripts, CI, and browser-side agents can fetch the JSON +# cross-origin (same policy as the .md companions). Cloudflare adds +# ETag/Last-Modified, so conditional polling still works. +/feeds/*.xml + Content-Type: application/atom+xml; charset=utf-8 + Access-Control-Allow-Origin: * + Access-Control-Allow-Methods: GET, OPTIONS + Cache-Control: public, max-age=3600, must-revalidate + +/feeds/*.rss + Content-Type: application/rss+xml; charset=utf-8 + Access-Control-Allow-Origin: * + Access-Control-Allow-Methods: GET, OPTIONS + Cache-Control: public, max-age=3600, must-revalidate + +/feeds/*.json + Content-Type: application/json; charset=utf-8 + Access-Control-Allow-Origin: * + Access-Control-Allow-Methods: GET, OPTIONS + Cache-Control: public, max-age=3600, must-revalidate + +# Screenshots and diagrams. Without an explicit header these are revalidated on +# every page view, and in analytics the top bandwidth consumers are all +# /images/*.png fetched tens of thousands of times each. A week-long browser +# cache stops the bleeding. NOT `immutable`: image filenames are not +# content-hashed, so a replaced screenshot reuses its name — max-age bounds how +# long a stale copy can linger, and the deploy purges the edge cache anyway. +/images/* + Cache-Control: public, max-age=604800, stale-while-revalidate=86400 + +# Theme bundles and fonts under /assets/ ARE content-hashed by the build +# (e.g. main.f62f0af6.min.css, bundle.d7f30b55.min.js), so a new build ships a +# new filename — the old URL can be cached forever safely. +/assets/* + Cache-Control: public, max-age=31536000, immutable diff --git a/docs/6.x/_redirects b/docs/6.x/_redirects index 6fea552db..58b2184f6 100644 --- a/docs/6.x/_redirects +++ b/docs/6.x/_redirects @@ -1,7 +1,19 @@ +# ORDERING IS LOAD-BEARING - static rules first, wildcard rules last. +# +# Cloudflare charges every rule that appears *after* the first wildcard rule +# against its 100-"dynamic"-rule budget, whatever that rule itself looks like. +# With wildcards interleaved, ~700 plain rules counted as dynamic and the +# deployment was rejected outright: +# Maximum number of dynamic _redirects rules limit of 100 exceeded [100324] +# Grouping the wildcards at the end takes the count to the number of genuinely +# dynamic rules. Netlify is unaffected: it is first-match-wins either way, and +# exact matches now win over wildcards, which is the intent in every case here. +# +# So: add plain path-to-path redirects ABOVE the wildcard block; add wildcard +# rules at the very bottom. + # Filtering node + Cloud architecture moved from about-wallarm/overview to about-wallarm/api-security-overview /about-wallarm/overview/#filtering-node /about-wallarm/api-security-overview/#filtering-node -/about-wallarm/overview/#cloud /about-wallarm/api-security-overview/#cloud -/about-wallarm/overview/#how-wallarm-works /about-wallarm/api-security-overview/#how-wallarm-api-security-works # AI Security section dissolved into API Security; overview page merged into Agentic AI Protection /agentic-ai/overview/ /agentic-ai/agentic-ai-protection/ @@ -19,7 +31,6 @@ /agentic-ai/rogue-mcp-inspection/ /vulnerability-detection/security-testing-overview/ /agentic-ai/rogue-mcp-inspection /vulnerability-detection/security-testing-overview/ -/vulnerability-detection/api-security-testing-via-postman/* /vulnerability-detection/security-testing-overview/ /en/admin-en/configure-tarantool-en.html /en/admin-en/configuration-guides/allocate-resources-for-waf-node.html /en/quickstart-en/qs-intro-en.html /en/quickstart-en/how-wallarm-works/qs-intro-en.html @@ -29,7 +40,6 @@ /installation/on-premise/deployment /installation/supported-deployment-options/ /installation/on-premise/maintenance /installation/supported-deployment-options/ /installation/on-premise /installation/supported-deployment-options/ -/installation/on-premise/* /installation/supported-deployment-options/ /ru/ https://docs.wallarm.ru/ /ru https://docs.wallarm.ru/ @@ -65,164 +75,64 @@ /admin-en/configuration-guides/wallarm-ingress-controller/best-practices/faq/ /faq/ingress-installation/ /admin-en/troubleshooting /faq/common-errors-after-installation/ /admin-en/configuration-guides/wallarm-ingress-controller/best-practices/faq /faq/ingress-installation/ - -/2.12/api/* /api/:splat 301! -/2.12/faq/* /faq/:splat 301! -/2.12/fast/* /fast/:splat 301! -/2.14/api/* /api/:splat 301! -/2.14/fast/* /fast/:splat 301! -/2.14/api-firewall/* /api-firewall/:splat 301! -/2.16/api/* /api/:splat 301! -/2.16/fast/* /fast/:splat 301! -/2.16/api-firewall/* /api-firewall/:splat 301! -/2.18/fast/* /fast/:splat 301! -/3.0/fast/* /fast/:splat 301! -/2.12/admin-en/* /2.12 -/2.12/quickstart-en/* /2.12 -/2.12/updating-migrating/* /2.12 -/2.12/user-guides/* /2.12 -/2.12/partner-waf-node/* /2.12 /2.12/attacks-vulns-list /2.12 /2.12/attacks-vulns-list/ /2.12 /2.12/glossary-en /2.12 /2.12/glossary-en/ /2.12 -/2.10/* /2.12 /2.10/ /2.12 /2.10 /2.12 -/2.12/* /2.14 /2.12/ /2.14 /2.12 /2.14 -/2.14/* /2.16 /2.14/ /2.16 /2.14 /2.16 -/2.14/admin-en/* /2.14 -/2.14/quickstart-en/* /2.14 -/2.14/updating-migrating/* /2.14 -/2.14/user-guides/* /2.14 -/2.14/partner-waf-node/* /2.14 /2.14/attacks-vulns-list /2.14 /2.14/attacks-vulns-list/ /2.14 /2.14/glossary-en /2.14 /2.14/glossary-en/ /2.14 -/2.16/admin-en/* /2.16 -/2.16/quickstart-en/* /2.16 -/2.16/updating-migrating/* /2.16 -/2.16/user-guides/* /2.16 -/2.16/partner-waf-node/* /2.16 /2.16/attacks-vulns-list /2.16 /2.16/attacks-vulns-list/ /2.16 /2.16/glossary-en /2.16 /2.16/glossary-en/ /2.16 -/2.16/* /2.18 /3.6/news/ /news/ -/3.0/admin-en/* /2.18 -/3.0/quickstart-en/* /2.18 -/3.0/updating-migrating/* /2.18 -/3.0/user-guides/* /2.18 -/3.0/waf-installation/* /2.18 -/3.0/installation/* /2.18 -/3.0/about-wallarm-waf/* /2.18 -/3.0/about-wallarm/* /2.18 -/3.0/partner-waf-node/* /2.18 /3.0/attacks-vulns-list /2.18 /3.0/attacks-vulns-list/ /2.18 /3.0/glossary-en /2.18 /3.0/glossary-en/ /2.18 -/3.0/* /3.2 -/3.2/admin-en/* /3.2 -/3.2/updating-migrating/* /3.2 -/3.2/user-guides/* /3.2 -/3.2/waf-installation/* /3.2 -/3.2/installation/* /3.2 -/3.2/about-wallarm-waf/* /3.2 -/3.2/about-wallarm/* /3.2 -/3.2/partner-waf-node/* /3.2 /3.2/attacks-vulns-list /3.2 /3.2/attacks-vulns-list/ /3.2 /3.2/glossary-en /3.2 /3.2/glossary-en/ /3.2 /3.2/quickstart/ /3.2 -/4.2/faq/* /faq/:splat 301! -/3.2/* /3.4 -/3.4/admin-en/* /3.4 /3.4/quickstart/ /3.4 /3.4/quickstart /3.4 -/3.4/updating-migrating/* /3.4 -/3.4/user-guides/* /3.4 -/3.4/waf-installation/* /3.4 -/3.4/installation/* /3.4 -/3.4/about-wallarm-waf/* /3.4 -/3.4/about-wallarm/* /3.4 -/3.4/partner-waf-node/* /3.4 /3.4/attacks-vulns-list /3.4 /3.4/attacks-vulns-list/ /3.4 /3.4/glossary-en /3.4 /3.4/glossary-en/ /3.4 -/4.0/admin-en/* /4.0 /4.0/quickstart/ /4.0 /4.0/quickstart /4.0 -/4.0/updating-migrating/* /4.0 -/4.0/user-guides/* /4.0 -/4.0/waf-installation/* /4.0 -/4.0/installation/* /4.0 -/4.0/about-wallarm-waf/* /4.0 -/4.0/about-wallarm/* /4.0 -/4.0/partner-waf-node/* /4.0 /4.0/attacks-vulns-list /4.0 /4.0/attacks-vulns-list/ /4.0 /4.0/glossary-en /4.0 /4.0/glossary-en/ /4.0 -/4.0/* /4.2 -/4.2/admin-en/* /4.2 -/4.2/installation/* /4.2 /4.2/quickstart/ /4.2 /4.2/quickstart /4.2 -/4.2/updating-migrating/* /4.2 -/4.2/user-guides/* /4.2 -/4.2/waf-installation/* /4.2 -/4.2/about-wallarm-waf/* /4.2 -/4.2/partner-waf-node/* /4.2 /4.2/attacks-vulns-list /4.2 /4.2/attacks-vulns-list/ /4.2 /4.2/glossary-en /4.2 /4.2/glossary-en/ /4.2 -/4.2/* /4.4 -/4.4/admin-en/* /4.4 -/4.4/installation/* /4.4 /4.4/quickstart/ /4.4 /4.4/quickstart /4.4 -/4.4/updating-migrating/* /4.4 -/4.4/user-guides/* /4.4 -/4.4/waf-installation/* /4.4 -/4.4/about-wallarm-waf/* /4.4 -/4.4/partner-waf-node/* /4.4 /4.4/attacks-vulns-list /4.4 /4.4/attacks-vulns-list/ /4.4 /4.4/glossary-en /4.4 /4.4/glossary-en/ /4.4 -/4.4/* /4.6 -/4.6/admin-en/* /4.6 -/4.6/installation/* /4.6 -/4.6/quickstart/* /4.6 -/4.6/updating-migrating/* /4.6 -/4.6/user-guides/* /4.6 -/4.6/about-wallarm/* /4.6 /4.6/attacks-vulns-list/ /4.6 /4.6/glossary-en/ /4.6 -/4.6/api/* /4.6 -/4.6/api-abuse-prevention/* /4.6 -/4.6/api-discovery/* /4.6 -/4.6/api-protection/* /4.6 -/4.6/api-specification-enforcement/* /4.6 -/4.6/demo-videos/* /4.6 -/4.6/faq/* /4.6 -/4.6/fast/* /4.6 -/4.6/integrations-devsecops/* /4.6 -/4.6/vulnerability-detection/* /4.6 /4.6/api-sessions/ /4.6 /4.6/news/ /4.6 @@ -310,10 +220,10 @@ /admin-en/scanner-ips-whitelisting/ /user-guides/ip-lists/allowlist/#examples-of-ip-allowlist-usage /admin-en/block-part-en/ /admin-en/configure-wallarm-mode/ /admin-en/configuration-guides/wallarm-ingress-controller/best-practices/whitelist-wallarm-ip-addresses/ /user-guides/ip-lists/allowlist/#examples-of-ip-allowlist-usage -/3.0/partner-waf-node/overview/ /2.18/partner-waf-node/overview/ 301! -/3.0/partner-waf-node/creating-partner-account/ /2.18/partner-waf-node/creating-partner-account/ 301! -/3.0/partner-waf-node/connecting-clients/ /2.18/partner-waf-node/connecting-clients/ 301! -/3.0/partner-waf-node/installing-partner-waf-node/ /2.18/partner-waf-node/installing-partner-waf-node/ 301! +/3.0/partner-waf-node/overview/ /2.18/partner-waf-node/overview/ 301 +/3.0/partner-waf-node/creating-partner-account/ /2.18/partner-waf-node/creating-partner-account/ 301 +/3.0/partner-waf-node/connecting-clients/ /2.18/partner-waf-node/connecting-clients/ 301 +/3.0/partner-waf-node/installing-partner-waf-node/ /2.18/partner-waf-node/installing-partner-waf-node/ 301 /en/admin-en/installation-ami-en.html /admin-en/installation-ami-en/ @@ -326,9 +236,6 @@ /3.0/quickstart-en/qs-check-operation-en/ /3.0/admin-en/installation-check-operation-en/ /user-guides/rules/add-rule/#equal /user-guides/rules/add-rule/#condition-type-equal -/user-guides/rules/add-rule/#iequal /user-guides/rules/add-rule/#condition-type-iequal-aa -/user-guides/rules/add-rule/#regex /user-guides/rules/add-rule/#condition-type-regex -/user-guides/rules/add-rule/#absent /user-guides/rules/add-rule/#condition-type-absent /glossary-en/#attack-vector /glossary-en/#malicious-payload /en/admin-en/configure-backup-en.html /admin-en/configure-backup-en/ @@ -410,11 +317,6 @@ /user-guides/denylist/ /user-guides/ip-lists/denylist/ /admin-en/installation-kong-en/ /installation/kubernetes/kong-ingress-controller/deployment/ - -/about-wallarm-waf/* /about-wallarm/:splat 301! -/4.0/about-wallarm-waf/* /4.0/about-wallarm/:splat 301! -/waf-installation/* /installation/:splat 301! -/4.0/waf-installation/* /4.0/installation/:splat 301! /admin-en/configuration-guides/allocate-resources-for-waf-node/ /admin-en/configuration-guides/allocate-resources-for-node/ /4.0/admin-en/configuration-guides/allocate-resources-for-waf-node/ /4.0/admin-en/configuration-guides/allocate-resources-for-node/ /admin-en/configuration-guides/waf-in-separated-environments/how-waf-in-separated-environments-works/ /admin-en/configuration-guides/wallarm-in-separated-environments/how-wallarm-in-separated-environments-works/ @@ -437,7 +339,6 @@ /4.2/user-guides/dashboard/ /4.2/user-guides/dashboards/threat-prevention/ /4.0/user-guides/dashboard/ /4.0/user-guides/dashboards/threat-prevention/ -/4.0/* /4.0 /4.0/user-guides/vulnerabilities/analyze-vuln/ /4.0/user-guides/vulnerabilities/ /4.0/user-guides/vulnerabilities/check-vuln/ /4.0/user-guides/vulnerabilities/ /4.0/user-guides/vulnerabilities/close-open-vuln/ /4.0/user-guides/vulnerabilities/ @@ -548,21 +449,16 @@ /user-guides/ip-lists/graylist/ /user-guides/ip-lists/overview/ /user-guides/ip-lists/denylist/ /user-guides/ip-lists/overview/ /user-guides/triggers/trigger-examples/ /user-guides/triggers/triggers/ -/user-guides/triggers/trigger-examples/#detect-weak-jwts /about-wallarm/detecting-vulnerabilities/#weak-jwts-detection -/user-guides/triggers/trigger-examples/#group-hits-originating-from-the-same-ip-into-one-attack /user-guides/events/analyze-attack/#grouping-of-hits /user-guides/bola-protection/ /admin-en/configuration-guides/protecting-against-bola/ /4.8/user-guides/ip-lists/allowlist/ /4.8/user-guides/ip-lists/overview/ /4.8/user-guides/ip-lists/graylist/ /4.8/user-guides/ip-lists/overview/ /4.8/user-guides/ip-lists/denylist/ /4.8/user-guides/ip-lists/overview/ /4.8/user-guides/triggers/trigger-examples/ /4.8/user-guides/triggers/triggers/ -/4.8/user-guides/triggers/trigger-examples/#detect-weak-jwts /4.8/about-wallarm/detecting-vulnerabilities/#weak-jwts-detection -/4.8/user-guides/triggers/trigger-examples/#group-hits-originating-from-the-same-ip-into-one-attack /4.8/user-guides/events/analyze-attack/#grouping-of-hits /4.8/user-guides/bola-protection/ /4.8/admin-en/configuration-guides/protecting-against-bola/ /user-guides/rules/intro/ /user-guides/rules/rules/ -/user-guides/rules/add-rule/ /user-guides/rules/rules/#configuring /user-guides/rules/view/ /user-guides/rules/rules/#inspecting /user-guides/rules/compiling/ /user-guides/rules/rules/#ruleset-lifecycle /user-guides/rules/backup/ /user-guides/rules/rules/#backup-and-restore @@ -587,8 +483,6 @@ /user-guides/rules/api-abuse-url/ /api-abuse-prevention/exceptions/#exceptions-for-target-urls-and-specific-requests /4.8/user-guides/rules/api-abuse-url/ /4.8/api-abuse-prevention/exceptions/#exceptions-for-target-urls-and-specific-requests -/api-policy-enforcement/* /api-specification-enforcement/:splat - /about-wallarm/api-abuse-prevention/ /api-abuse-prevention/overview/ /user-guides/api-abuse-prevention/ /api-abuse-prevention/setup/ /user-guides/api-abuse-prevention-exceptions/ /api-abuse-prevention/exceptions/ @@ -683,18 +577,12 @@ /admin-en/configuration-guides/sso/employ-user-auth/ /admin-en/configuration-guides/sso/intro/#available-options /admin-en/configuration-guides/sso/change-sso-provider/ /admin-en/configuration-guides/sso/setup/#disabling-and-deletion /user-guides/use-sso/ /admin-en/configuration-guides/sso/intro/ -/admin-en/configuration-guides/sso/gsuite/* /admin-en/configuration-guides/sso/sso-gsuite/ -/admin-en/configuration-guides/sso/okta/* /admin-en/configuration-guides/sso/sso-okta/ /4.10/admin-en/configuration-guides/sso/employ-user-auth/ /4.10/admin-en/configuration-guides/sso/intro/#available-options /4.10/admin-en/configuration-guides/sso/change-sso-provider/ /4.10/admin-en/configuration-guides/sso/setup/#disabling-and-deletion /4.10/user-guides/use-sso/ /4.10/admin-en/configuration-guides/sso/intro/ -/4.10/admin-en/configuration-guides/sso/gsuite/* /4.10/admin-en/configuration-guides/sso/sso-gsuite/ -/4.10/admin-en/configuration-guides/sso/okta/* /4.10/admin-en/configuration-guides/sso/sso-okta/ /4.8/admin-en/configuration-guides/sso/employ-user-auth/ /4.8/admin-en/configuration-guides/sso/intro/#available-options /4.8/admin-en/configuration-guides/sso/change-sso-provider/ /4.8/admin-en/configuration-guides/sso/setup/#disabling-and-deletion /4.8/user-guides/use-sso/ /4.8/admin-en/configuration-guides/sso/intro/ -/4.8/admin-en/configuration-guides/sso/gsuite/* /4.8/admin-en/configuration-guides/sso/sso-gsuite/ -/4.8/admin-en/configuration-guides/sso/okta/* /4.8/admin-en/configuration-guides/sso/sso-okta/ /installation/oob/terraform-module/mirroring-by-web-server/ /installation/oob/overview/#supported-deployment-options /installation/oob/web-server-mirroring/aws-ami/ /installation/oob/overview/#supported-deployment-options @@ -703,8 +591,6 @@ /installation/oob/web-server-mirroring/linux/all-in-one/ /installation/oob/overview/#supported-deployment-options /installation/oob/web-server-mirroring/overview/ /installation/oob/overview/#supported-deployment-options -/admin-en/monitoring/* /admin-en/configure-statistics-service/ - /admin-en/configuration-guides/wallarm-in-separated-environments/how-wallarm-in-separated-environments-works/ /installation/multi-tenant/overview/ /admin-en/configuration-guides/wallarm-in-separated-environments/configure-wallarm-in-separated-environments/ /installation/multi-tenant/overview/ /5.x/admin-en/configuration-guides/wallarm-in-separated-environments/how-wallarm-in-separated-environments-works/ /5.x/installation/multi-tenant/overview/ @@ -721,25 +607,8 @@ /admin-en/installation-guides/envoy/envoy-docker /installation/connectors/istio /installation/inline/compute-instances/docker/envoy-based /installation/connectors/istio -/4.6/* /4.8 -/4.8/admin-en/* /4.8 -/4.8/installation/* /4.8 -/4.8/quickstart/* /4.8 -/4.8/updating-migrating/* /4.8 -/4.8/user-guides/* /4.8 -/4.8/about-wallarm/* /4.8 /4.8/attacks-vulns-list/ /4.8 /4.8/glossary-en/ /4.8 -/4.8/api/* /4.8 -/4.8/api-abuse-prevention/* /4.8 -/4.8/api-discovery/* /4.8 -/4.8/api-protection/* /4.8 -/4.8/api-specification-enforcement/* /4.8 -/4.8/demo-videos/* /4.8 -/4.8/faq/* /4.8 -/4.8/fast/* /4.8 -/4.8/integrations-devsecops/* /4.8 -/4.8/vulnerability-detection/* /4.8 /4.8/api-sessions/ /4.8 /4.8/news/ /4.8 /4.8/api-attack-surface/ /4.8 @@ -749,8 +618,6 @@ /4.10/user-guides/scanner/ /4.10/api-attack-surface/api-surface/#replacement-of-old-scanner -/2.18/* /updating-migrating/older-versions/what-is-new/ -/3.6/* /updating-migrating/older-versions/what-is-new/ /faq/common-errors-after-installation/#why-does-not-the-filtering-node-block-attacks-when-operating-in-blocking-mode-wallarm_mode-block /troubleshooting/detection-and-blocking/#filtering-node-does-not-block-attacks-when-operating-in-blocking-mode-wallarm_mode-block /faq/wallarm-status-page/ /troubleshooting/overview/#wallarm-service-status-page @@ -773,7 +640,6 @@ /installation/on-premise/ /installation/supported-deployment-options/ /5.x/installation/on-premise/ /5.x/installation/supported-deployment-options/ -/5.x/installation/on-premise/* /5.x/installation/supported-deployment-options/ /4.10/installation/on-premise/ /4.10/installation/supported-deployment-options/ /installation/connectors/istio-inline/ /installation/connectors/istio/ @@ -787,37 +653,14 @@ /5.x/installation/security-edge/deployment/ /5.x/installation/security-edge/inline/deployment/ /5.x/installation/se-connector/ /5.x/installation/security-edge/se-connector/ -/vulnerability-detection/api-test-patrol/* /vulnerability-detection/schema-based-testing/:splat -/5.x/vulnerability-detection/api-test-patrol/* /5.x/vulnerability-detection/schema-based-testing/:splat -/4.10/vulnerability-detection/api-test-patrol/* /4.10/vulnerability-detection/schema-based-testing/:splat -/4.8/* /4.10 - -/4.10/admin-en/* /4.10 -/4.10/installation/* /4.10 -/4.10/quickstart/* /4.10 -/4.10/updating-migrating/* /4.10 -/4.10/user-guides/* /4.10 -/4.10/about-wallarm/* /4.10 /4.10/attacks-vulns-list/ /4.10 /4.10/glossary-en/ /4.10 -/4.10/api/* /4.10 -/4.10/api-abuse-prevention/* /4.10 -/4.10/api-discovery/* /4.10 -/4.10/api-protection/* /4.10 -/4.10/api-specification-enforcement/* /4.10 -/4.10/demo-videos/* /4.10 -/4.10/faq/* /4.10 -/4.10/fast/* /4.10 -/4.10/integrations-devsecops/* /4.10 -/4.10/vulnerability-detection/* /4.10 /4.10/api-sessions/ /4.10 /4.10/news/ /4.10 /4.10/api-attack-surface/ /4.10 /4.10/maintenance/ /4.10 /4.10/agentic-ai/ /4.10 -/4.10/api-attack-surface/ /4.10 -/4.10/api-sessions/ /4.10 /4.10/platform-management/ /4.10 /4.10/reference/ /4.10 @@ -861,13 +704,62 @@ /user-guides/rules/limit-data-export/ /admin-en/export-to-cloud/#attacks-incidents +/7.x/admin-en/configuration-guides/wallarm-ingress-controller/best-practices/ingress-controller-monitoring/ /7.x/admin-en/nginx-node-metrics/ +/7.x/admin-en/configuration-guides/wallarm-ingress-controller/best-practices/report-public-user-ip/ /7.x/admin-en/using-proxy-or-balancer-en/ +/7.x/updating-migrating/migrate-ip-lists-to-node-3/ /7.x/updating-migrating/versioning-policy/#version-list +/7.x/updating-migrating/nginx-modules/ /7.x/updating-migrating/versioning-policy/#version-list + +# --- Ported from the netlify.toml [[redirects]] blocks (DEVOPS-5014) --------- +# 200 = rewrite: serve the target in place, no client-visible redirect. + +# Serve favicon.png under the conventional /favicon.ico path. Browsers viewing +# raw .md / .txt files have no HTML to read from, so +# they fall back to auto-discovering /favicon.ico — without this rewrite the +# tab would show no favicon for any .md page. +/favicon.ico /favicon.png 200 + +# ---- wildcard rules: everything below is charged against Cloudflare's +# ---- 100-dynamic-rule budget. Keep this block small. + +/vulnerability-detection/api-security-testing-via-postman/* /vulnerability-detection/security-testing-overview/ +/installation/on-premise/* /installation/supported-deployment-options/ + +/2.12/api/* /api/:splat 301 +/2.12/faq/* /faq/:splat 301 +/2.12/fast/* /fast/:splat 301 +/2.14/api/* /api/:splat 301 +/2.14/fast/* /fast/:splat 301 +/2.14/api-firewall/* /api-firewall/:splat 301 +/2.16/api/* /api/:splat 301 +/2.16/fast/* /fast/:splat 301 +/2.16/api-firewall/* /api-firewall/:splat 301 +/2.18/fast/* /fast/:splat 301 +/3.0/fast/* /fast/:splat 301 +/4.2/faq/* /faq/:splat 301 + +/about-wallarm-waf/* /about-wallarm/:splat 301 +/4.0/about-wallarm-waf/* /4.0/about-wallarm/:splat 301 +/waf-installation/* /installation/:splat 301 +/4.0/waf-installation/* /4.0/installation/:splat 301 + +/api-policy-enforcement/* /api-specification-enforcement/:splat +/admin-en/configuration-guides/sso/gsuite/* /admin-en/configuration-guides/sso/sso-gsuite/ +/admin-en/configuration-guides/sso/okta/* /admin-en/configuration-guides/sso/sso-okta/ + +/admin-en/monitoring/* /admin-en/configure-statistics-service/ +/5.x/installation/on-premise/* /5.x/installation/supported-deployment-options/ + +/vulnerability-detection/api-test-patrol/* /vulnerability-detection/schema-based-testing/:splat +/5.x/vulnerability-detection/api-test-patrol/* /5.x/vulnerability-detection/schema-based-testing/:splat +/4.10/vulnerability-detection/api-test-patrol/* /4.10/vulnerability-detection/schema-based-testing/:splat + /ja/* /:splat 301 /tr/* /:splat 301 /ar/* /:splat 301 /pt-br/* /:splat 301 - -/7.x/admin-en/configuration-guides/wallarm-ingress-controller/best-practices/ingress-controller-monitoring/ /7.x/admin-en/nginx-node-metrics/ -/7.x/admin-en/configuration-guides/wallarm-ingress-controller/best-practices/report-public-user-ip/ /7.x/admin-en/using-proxy-or-balancer-en/ /7.x/updating-migrating/older-versions/* /7.x/updating-migrating/versioning-policy/#version-list -/7.x/updating-migrating/migrate-ip-lists-to-node-3/ /7.x/updating-migrating/versioning-policy/#version-list -/7.x/updating-migrating/nginx-modules/ /7.x/updating-migrating/versioning-policy/#version-list + +# Serve the version-root sitemap for a sitemap.xml requested at any depth. +# Cloudflare's `*` is greedy and matches `/`, so this single rule replaces the +# three depth-specific netlify.toml entries. +/*/sitemap.xml /sitemap.xml 200 diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 000000000..60c5437c8 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Full multi-version site build. Extracted verbatim from the `[build] command` +# in netlify.toml so Netlify and Cloudflare run the same steps (DEVOPS-5014). +# netlify.toml is left untouched until the DNS cutover is verified — until then +# the two copies must be kept in sync. +# +# CONTEXT mirrors Netlify's variable: "production" enables the expensive +# production-only steps (image optimisation, raw markdown, OG images, feeds). +# Anything else (PR preview, branch build) skips them to keep builds fast. +# +# SKIP_IMAGE_OPTIMISATION=1 drops just the image pass while keeping the rest of +# a production build. The pass downloads x86_64 Linux binaries (pngquant, +# oxipng), so without this the script cannot run in production mode on a +# developer machine — and the markdown companions the Worker serves are only +# generated in production mode, so they would otherwise be untestable locally. +set -euo pipefail + +CONTEXT="${CONTEXT:-deploy-preview}" + +pip3 install --no-cache-dir -r requirements.txt + +# The lazy-loading markdown extension (mdx_lazy_images) lives at the repo root; +# make it importable for every `zensical build` below. +export PYTHONPATH="$PWD" + +# Image optimisation, two passes, before any per-version build copies the +# directory. Both run on the ephemeral CI checkout — full-resolution originals +# stay in git. Together they take images/ from ~191 MB to ~63 MB (oxipng alone +# managed only ~121 MB); the top bandwidth consumers in analytics roughly halve. +# +# 1. Quantise (scripts/optimize_images.py -> pngquant): the heavy images are +# 24-bit true-colour retina screenshots, but UI screenshots use few real +# colours, so quantising to a <=256-colour palette (what TinyPNG does) roughly +# halves each file with no visible loss and keeps the .png extension, so no +# markdown references change. --quality=65-95 SKIPS any image it cannot +# quantise cleanly (gradients/photos), --skip-if-larger never writes a bigger +# file. +# 2. oxipng: losslessly recompress every PNG. --strip safe keeps colour profile +# and gamma metadata. -o 2 is the recommended speed/size tradeoff for batch +# use. Runs AFTER quantise so it squeezes the palette PNGs further. +# WARNING FOR LOCAL RUNS: the optimisation below rewrites images/ IN PLACE. +# On CI that is an ephemeral checkout, but locally it replaces the +# full-resolution sources in your working tree, and `git add -A` will happily +# commit the lossy output. Use SKIP_IMAGE_OPTIMISATION=1 locally, or +# `git checkout origin/master -- images/` afterwards. +if [ "$CONTEXT" = "production" ] && [ -n "${SKIP_IMAGE_OPTIMISATION:-}" ]; then + echo "Skipping image optimisation (SKIP_IMAGE_OPTIMISATION set)" +elif [ "$CONTEXT" = "production" ]; then + echo "Image size before optimisation: $(du -sh images/ | cut -f1) ($(find images/ -name '*.png' | wc -l) PNGs)" + curl -fsSL https://pngquant.org/pngquant-linux.tar.bz2 | tar xj pngquant + python3 scripts/optimize_images.py images/ --pngquant ./pngquant + curl -fsSL https://github.com/oxipng/oxipng/releases/download/v10.1.1/oxipng-10.1.1-x86_64-unknown-linux-gnu.tar.gz | tar xz --strip-components=1 oxipng-10.1.1-x86_64-unknown-linux-gnu/oxipng + ./oxipng -r -o 2 --strip safe -q images/ + echo "Image size after optimisation: $(du -sh images/ | cut -f1)" +else + echo "Skipping image optimisation (CONTEXT=$CONTEXT — only runs on production deploys)" +fi + +# Raw .md companion files (scripts/generate_raw_markdown.py) only ship on +# production. The companions power the "Copy as Markdown" button — readers only +# ever fetch them from docs.wallarm.com, so previews skip the step to save build +# minutes. On previews the button falls back to its "Failed to copy" snackbar, +# which is the expected dev UX. +build_version() { + local docs_dir="$1" config="$2" og_only="${3:-}" + cp -R images/ "docs/${docs_dir}/images/" + zensical build -f "$config" + if [ "$CONTEXT" = "production" ]; then + [ "$og_only" = "og-only" ] || python3 scripts/generate_raw_markdown.py "$config" + python3 scripts/generate_og_images.py "$config" + fi + rm -rf "docs/${docs_dir}/images/" +} + +build_version 6.x mkdocs-6.x.yml +build_version 7.x mkdocs-7.x.yml +build_version 5.0 mkdocs-5.0.yml +build_version deprecated mkdocs-deprecated.yml og-only + +# Aggregated changelog feeds (Atom + RSS + JSON), one per node artifact. Reads +# the changelog files from docs/, self-validates, and aborts the deploy on a +# broken changelog. Production only. See scripts/README-feeds.md. +if [ "$CONTEXT" = "production" ]; then + python3 scripts/generate_feeds.py --output site + + # Fail the build if any page lacks its .md companion. Cloudflare serves + # `Accept: text/markdown` with a blind URL rewrite, so a missing companion + # is not a fallback to HTML — it is a 404 mislabelled as Markdown. Only + # meaningful on production builds, where the companions are generated. + python3 scripts/check_markdown_companions.py site +fi diff --git a/scripts/check_markdown_companions.py b/scripts/check_markdown_companions.py new file mode 100755 index 000000000..190a83524 --- /dev/null +++ b/scripts/check_markdown_companions.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Assert every built page has a raw-markdown companion. + +Cloudflare serves `Accept: text/markdown` by rewriting the page URL to its .md +companion (infra/cloudflare-iac, markdown_negotiation.tf). A rewrite is blind: +Cloudflare cannot check whether the target exists, so a page without a +companion does not fall back to HTML — it returns the 404 page, labelled +`Content-Type: text/markdown` because the _headers rule matches the request +path. An agent gets 279 KB of HTML claiming to be Markdown. + +The Worker this replaced fetched the companion and fell back on a miss. Losing +that fallback is only safe if "every page has a companion" is enforced, so this +turns a silent runtime failure into a build failure. + +Usage: scripts/check_markdown_companions.py site +""" +import sys +from pathlib import Path + +# Version landing pages are assembled by the theme rather than authored, so +# generate_raw_markdown.py produces nothing for them. They are excluded from +# the rewrite in markdown_negotiation.tf (local.md_no_companion) and serve HTML +# to a markdown request, which is the correct fallback. KEEP THE TWO IN SYNC. +EXPECTED_WITHOUT_COMPANION = {"7.x", "5.x", "4.10"} + + +def main() -> int: + site = Path(sys.argv[1] if len(sys.argv) > 1 else "site") + if not site.is_dir(): + print(f"error: {site} is not a directory", file=sys.stderr) + return 2 + + missing, checked = [], 0 + for page in site.rglob("index.html"): + rel = page.parent.relative_to(site) + slug = "" if str(rel) == "." else str(rel) + checked += 1 + if slug in EXPECTED_WITHOUT_COMPANION: + continue + companion = site / "index.md" if not slug else site / f"{slug}.md" + if not companion.is_file(): + missing.append(f"/{slug}/" if slug else "/") + + print(f"markdown companions: checked {checked} pages, " + f"{len(EXPECTED_WITHOUT_COMPANION)} exempt, {len(missing)} missing") + if missing: + print("\nPages with no .md companion. Under the Cloudflare rewrite these " + "return a mislabelled 404 to any agent sending Accept: text/markdown.\n" + "Either fix generate_raw_markdown.py, or exempt the page here AND in " + "markdown_negotiation.tf (local.md_no_companion):\n", file=sys.stderr) + for m in missing[:40]: + print(f" {m}", file=sys.stderr) + if len(missing) > 40: + print(f" … and {len(missing) - 40} more", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/wrangler.jsonc b/wrangler.jsonc new file mode 100644 index 000000000..6662a006f --- /dev/null +++ b/wrangler.jsonc @@ -0,0 +1,69 @@ +// Cloudflare Workers Static Assets config for docs.wallarm.com (DEVOPS-5014). +// Replaces netlify.toml's [build] publish dir, edge function, and headers. +// +// There is deliberately no `main`: this is an assets-only Worker. The Worker +// script that used to live at src/worker.ts did `Accept: text/markdown` +// negotiation, and that is now three zone rules in infra/cloudflare-iac +// (markdown_negotiation.tf). Rules cost nothing to run, where a Worker script +// is billed per request — and the script ran on *every* HTML page view, since +// a plain page request has no Accept header to rewrite on. +// +// What the script did, and where it went: +// negotiation + fallback -> URL rewrite rules, plus a build-time guard +// (scripts/check_markdown_companions.py) that +// fails the build if a page has no companion, +// because a rewrite cannot fall back +// Vary: Accept, Link -> response-header rules +// CSP / HSTS / nosniff -> _headers, which now applies again: those had to +// be re-set in code only because _headers does not +// apply to responses returned from Worker code +// +// Recoverable from git history if the rules ever have to be reverted. +{ + "name": "wallarm-docs", + "compatibility_date": "2026-08-01", + "assets": { + // Same publish dir as Netlify. Built by scripts/build.sh. + "directory": "./site/", + // zensical emits directory-style pages (`/foo/bar/index.html`), so both + // `/foo/bar` and `/foo/bar/` must resolve to the same page. + "html_handling": "auto-trailing-slash", + "not_found_handling": "404-page" + }, + // A workers.dev URL would publish the whole site a second time, and zone + // rules do NOT apply there — verified against staging: + // + // Accept: text/markdown workers.dev -> text/html zone -> text/markdown + // /4.8/admin-en/foo/ workers.dev -> 404 zone -> 301 + // Vary workers.dev -> absent zone -> present + // + // So the workers.dev copy has no redirects, no markdown negotiation, no WAF + // and no bot protection, and search engines can index it as duplicate + // content. Production is reachable only through docs.wallarm.com. + // + // preview_urls is left on: Workers Builds gives each non-production branch a + // versioned preview URL, which is how PR review works. Those previews are on + // workers.dev too, so they show CONTENT accurately but NOT routing — a + // redirect or Accept-negotiation change has to be verified on + // docs-staging.wallarm.com instead. + "workers_dev": false, + "preview_urls": true, + + "observability": { + "enabled": true + }, + + // Staging twin, served at docs-staging.wallarm.com so the whole flow can be + // exercised before docs.wallarm.com moves. A separate Worker rather than a + // preview version of this one, so staging can be redeployed without touching + // whatever production is running. + // + // The hostname is wired up in infra/cloudflare-iac (proxied DNS record + + // Worker route), not here — wrangler must not own routes we manage in + // Terraform, or the two will fight over them. + "env": { + "staging": { + "name": "wallarm-docs-staging" + } + } +}