--- name: cloudflare-pages-gotchas description: This skill should be used when working on a Cloudflare Pages project with Pages Functions — debugging on-the-fly image resizing that won't work, a wrangler.toml with named [env.*] blocks where bindings mysteriously go missing in one environment, a crypto.subtle PBKDF2 call that throws in production but works locally, or a _redirects rule that silently never fires on a path a Function also handles. Trigger phrases include "pages images binding", "env.IMAGES", "wrangler.toml env production", "bindings missing in production", "PBKDF2 iterations error", "NotSupportedError iteration counts", "_redirects not working", "redirect skipped by functions", "pages function precedence". version: 0.1.0 --- # Cloudflare Pages + Wrangler Gotchas Four real, non-obvious platform behaviors hit while building and deploying a Cloudflare Pages site with Functions, KV, and R2. Each looked like a bug in the project's own code at first; each was actually a documented (but easy to miss) platform constraint. ## Pages Functions have NO Images binding — at all, ever `env.IMAGES` (the on-the-fly image transform API) is unsupported on Cloudflare Pages, full stop: - `wrangler.toml`: an `[images]` block fails config validation on a Pages project. - Dashboard: Images isn't in the Pages bindings list at all (it exists for Workers, not Pages). This is a hard platform limit, not a config mistake to debug around. **Solution: pre-generate the responsive/derivative images you need (WebP, AVIF, thumbnails, etc.) at build or ingest time and serve them as plain R2 objects** — don't design around an on-the-fly resize step for Pages. (Workers *do* support the Images binding; only Pages doesn't. `keep_vars` is similarly Workers-only and will fail on a Pages project.) ## Empty `[env.production]` / `[env.preview]` blocks silently drop top-level bindings In `wrangler.toml`, declaring *any* named environment block — even an empty one — stops Wrangler from inheriting top-level `kv_namespaces` / `r2_buckets` / other bindings into that environment. The result: `pages deploy` (or a branch deploy) ships with **no bindings at all**, and every Function that touches one 500s in production while working fine locally. ```toml # BAD — this empty block silently drops top-level bindings from prod: [env.production] # GOOD — no named env blocks at all; top-level bindings apply to every deploy [[kv_namespaces]] binding = "MY_KV" id = "..." ``` Wrangler does print a warning about this during deploy, but still deploys successfully — easy to miss in CI logs. Fix is simply deleting the empty `[env.*]` blocks so the top-level config applies to all deploys (production and preview alike). ## Workers' `crypto.subtle` caps PBKDF2 at 100,000 iterations `crypto.subtle.deriveBits`/`deriveKey` with PBKDF2 throws `NotSupportedError: iteration counts above 100000 are not supported` on real Workers/Pages Functions — but **not** in `wrangler dev` or Miniflare, which use Node's crypto implementation with no such cap. Code hashing at a higher iteration count (210,000, matching some published bcrypt/PBKDF2 guidance) will pass every local test and then 500 on every request in production. **Lesson: PBKDF2/crypto.subtle behavior must be verified against a real deploy, not just local dev** — Node-vs-workerd crypto differences hide this kind of bug completely locally. If you store the iteration count alongside each hash (e.g. `iterations` field in the stored credential), fixing a too-high count is just re-hashing existing values, not a schema migration. ## `_redirects` is silently skipped for any path a Pages Functions route matches Cloudflare's own docs: *"Redirects defined in the `_redirects` file are not applied to requests served by Pages Functions, even if the Function route matches the URL pattern."* This applies even when the matching function's only job for that path is to call `next()` and pass through — the mere existence of a matching Functions route is enough to disable `_redirects` for that path. **Practical impact: a root `functions/_middleware.ts` (which matches every request) means `_redirects` never fires anywhere on that project, for any path.** If you need a redirect (e.g. a renamed page/slug) on a Pages project that has a catch-all middleware, implement the redirect *inside* the middleware itself — checking `url.pathname` and returning `Response.redirect(...)` — rather than relying on `_redirects` at all. ## Related For Workers Cron Trigger gotchas (day-of-week numbering, testing `scheduled()` against real bindings, subrequest-limit sizing) and R2 delete/rename verification, see the `cloudflare-workers-cron-email` skill — distinct topic area, same "the platform did something non-obvious" theme.