{"id":27251,"plugin_id":"plugin_asdk_app_691f1f8f72408191afdbbdf8242bdf86","kind":"skill","collection_source":"plugin_package","comparison_source":null,"observed_at":"2026-10-07T00:02:43.539Z","digest":"1abb1f0eae1431f9dddd5332836494d2c5c6beddd0b35f99b4a8921b7b6e766f","against":24995,"payload":{"description":"Write and configure Netlify Edge Functions — TypeScript/JavaScript handlers running in a Deno runtime at the network edge. Use when adding auth middleware or auth redirects, geolocation or localization logic, A/B testing or personalization, request/response transforms (rewrites/redirects), or edge SSR to a Netlify site. Triggers on tasks like \"add an edge function\", \"auth check at the edge\", \"redirect visitors by country\", \"A/B test with cookies\", \"rewrite requests\", \"personalize by geo\", or \"cache an edge response\". Covers the config export, path routing, the Context object, response caching, environment variables, and edge-vs-serverless choices. Check the framework's adapter first — only hand-write an edge function when the framework doesn't already generate one.","included_files":[],"name":"netlify-edge-functions","skill_md_contents":"---\nname: netlify-edge-functions\ndescription: Write and configure Netlify Edge Functions — TypeScript/JavaScript handlers running in a Deno runtime at the network edge. Use when adding auth middleware or auth redirects, geolocation or localization logic, A/B testing or personalization, request/response transforms (rewrites/redirects), or edge SSR to a Netlify site. Triggers on tasks like \"add an edge function\", \"auth check at the edge\", \"redirect visitors by country\", \"A/B test with cookies\", \"rewrite requests\", \"personalize by geo\", or \"cache an edge response\". Covers the config export, path routing, the Context object, response caching, environment variables, and edge-vs-serverless choices. Check the framework's adapter first — only hand-write an edge function when the framework doesn't already generate one.\n---\n\n# Netlify Edge Functions\n\n## Modern syntax (reach for this)\n\nExport a default handler plus a `config` object. Import `Config`/`Context` types from `@netlify/edge-functions`; `Request`/`Response`/`URL` are global.\n\n```ts\nimport type { Config, Context } from \"@netlify/edge-functions\";\n\nexport default async (request: Request, context: Context) => {\n  return new Response(\"Hello world\");\n};\n\nexport const config: Config = {\n  path: \"/test\",\n};\n```\n\n**Do not hand-write an edge function when your framework's adapter already generates middleware for the job** — duplicating it causes conflicts. Check the framework adapter/reference first.\n\n**Edge vs serverless:** use edge functions for low-latency request/response manipulation, geolocation logic, auth checks/redirects, and A/B personalization. Use serverless functions for long-running work (up to 15 min), heavy Node.js dependencies, database-heavy operations, background/scheduled tasks, or memory above 512 MB.\n\n## File location\n\n- Default directory: `YOUR_BASE_DIRECTORY/netlify/edge-functions`. Custom: `edge_functions` under `[build]` in `netlify.toml` (path relative to base directory).\n- Keep the directory **outside your publish directory** so source files aren't deployed.\n- Extensions: `.js`, `.ts`, `.jsx`, `.tsx` (`.jsx`/`.tsx` useful for SSR).\n- Same-name conflict: if `my-function.ts` and `my-function.js` both exist, the **TypeScript file is ignored** and the JavaScript one is deployed.\n\n## Routing — required, or the function silently never runs\n\n⚠️ **An edge function without a route (no `config` export and no `netlify.toml` declaration) still deploys but never runs — no build error, no warning.** When \"my edge function does nothing\", check the route first.\n\n⚠️ **Scope `path` narrowly.** `path: \"/*\"` intercepts every request including static assets, adding latency and billing an edge invocation for each one.\n\nEdge functions are **not** auto-assigned a URL route. Configure via inline `config` or `netlify.toml`.\n\n`path` is a `URLPattern` expression, must start with `/`, single string or array:\n\n```ts\nexport const config: Config = {\n  path: [\"/\", \"/products/*\"],\n  excludedPath: [\"/*.css\", \"/*.js\"],\n};\n```\n\nConfig properties: `path`, `excludedPath`, `pattern` (regex alternative to `path`), `excludedPattern`, `method`, `header`, `onError`, `cache`.\n\n### netlify.toml declaration\n\nUse `[[edge_functions]]` to declare multiple functions on one path and control order:\n\n```toml\n[[edge_functions]]\n  path = \"/admin\"\n  function = \"auth\"\n\n[[edge_functions]]\n  path = \"/admin\"\n  function = \"injector\"\n  cache = \"manual\"\n\n[[edge_functions]]\n  pattern = \"/products/(.*)\"\n  excludedPattern = \"/products/things/(.*)\"\n  function = \"highlight\"\n```\n\nProperties: `function`, `path`, `excludedPath`, `pattern`, `excludedPattern`, `header`, `cache`.\n\n**Merge precedence:** if the same function is declared both inline and in `netlify.toml`, configs merge and are treated as inline; inline wins duplicate fields.\n\n### Match by headers\n\n`header` keys are HTTP header names (case-insensitive); values are `true` (present), `false` (absent), or a string regex on the value. Multiple same-name values match against the comma-joined list.\n\n```ts\nexport const config: Config = {\n  header: { \"x-required\": true, \"x-forbidden\": false, \"user-agent\": \"(iPhone|Android)\" },\n  path: \"/*\",\n};\n```\n\n### Declaration processing order\n\nNetlify runs the whole declaration order **TWICE**: the first pass runs only edge functions **not** configured for caching; the second pass runs the ones **with** caching configured. Within that:\n\n1. Framework-generated functions declared in a config file.\n2. Your `netlify.toml` declarations (top-to-bottom order).\n3. Framework/integration-generated functions with inline config.\n4. Your inline declarations (**alphabetical by function file name**).\n\nTo control order across multiple functions on a path, prefer `netlify.toml` declarations over inline.\n\nAfter all functions run, Netlify evaluates redirect rules — unless a function returned a response and ended the chain. To customize order, use `netlify.toml`.\n\n**Order caveats:**\n- A returned response ends the chain; redirects for that path don't occur.\n- An edge function on the **target** of a static rewrite does **not** execute for rewritten requests.\n- `fetch()` for internal requests or returning a `URL` starts a **new request chain** and re-runs matching edge functions. Use `context.next()` to avoid re-running them.\n\n## Function signature & return values\n\nHandler receives `(request: Request, context: Context)`. Return one of:\n- a `Response` — delivered to the client; **ends the request chain** (declared redirects for that path don't run).\n- a `URL` — rewrite to a **same-site** URL with 200 status; address bar unchanged. Same-site only — for other sites use `fetch`.\n- `undefined` / empty `return;` — bypass this function, continue the chain.\n\nModify a response as middleware by awaiting `context.next()`:\n\n```ts\nimport type { Context } from \"@netlify/edge-functions\";\n\nexport default async (request: Request, context: Context) => {\n  const url = new URL(request.url);\n  if (url.searchParams.get(\"method\") !== \"transform\") return;\n\n  const response = await context.next();\n  const text = await response.text();\n  return new Response(text.toUpperCase(), response);\n};\n```\n\nNetlify does **not** add headers to edge function requests — use `context` for client request info.\n\n## Common patterns\n\n**Redirect by geo + cookie:**\n```ts\nexport default async (req: Request, { cookies, geo }: Context) => {\n  if (geo.city === \"Paris\" && cookies.get(\"promo-code\") === \"15-for-followers\") {\n    return Response.redirect(new URL(\"/subscriber-sale\", req.url));\n  }\n};\n```\n\n**Rewrite (same-site, 200):**\n```ts\nexport default async (request: Request, { geo }: Context) => {\n  if (geo.city === \"Paris\") return new URL(\"/subscriber-sale\", request.url);\n};\n```\n\n**Read request body then continue** — a body can only be read once, so pass a new `Request` with an unread body:\n```ts\nexport default async (req: Request, context: Context) => {\n  const body = await req.json();\n  if (!isValid(body.access_token)) return new Response(\"forbidden\", { status: 403 });\n  return context.next(new Request(req, { body: JSON.stringify(body) }));\n};\n```\n\n**Conditional request:**\n```ts\nexport default async (req: Request, { next }: Context) => {\n  const res = await next({ sendConditionalRequest: true });\n  if (res.status === 304) return res;\n  const text = await res.text();\n  return new Response(text.toUpperCase(), res);\n};\n```\n\n**SSR with React (`.tsx`):**\n```tsx\nimport React from \"https://esm.sh/react\";\nimport { renderToReadableStream } from \"https://esm.sh/react-dom/server\";\nimport type { Config, Context } from \"@netlify/edge-functions\";\n\nexport default async function handler(req: Request, context: Context) {\n  const stream = await renderToReadableStream(\n    <html><body><h1>Hello {context.geo.country?.name}</h1></body></html>\n  );\n  return new Response(stream, { status: 200, headers: { \"Content-Type\": \"text/html\" } });\n}\n\nexport const config: Config = { path: \"/hello\" };\n```\n\n## Context object\n\n- **`geo`** — `city`, `country.{code,name}`, `subdivision.{code,name}`, `latitude`, `longitude`, `timezone`, `postalCode`.\n- **`cookies`** — `get(name)`, `set(options)` (CookieStore.set format), `delete(name|options)`. Cross-subdomain cookies need a custom domain — impossible on `netlify.app` (Public Suffix List).\n- **`next(options?)`** / **`next(request, options?)`** — invoke the next item in the chain; returns a `Promise<Response>` you can modify. `options.sendConditionalRequest: true` for conditional requests. Only call `next` if you need the response body. Pass an explicit `Request` when you've read the body.\n- **`params`** — path params, e.g. path `/pets/:name` + request `/pets/winter` → `{name:\"winter\"}`. Query string: use `request.url`.\n- **`ip`** — client IP string.\n- **`requestId`** — Netlify request ID.\n- **`account.id`**, **`site.{id,name,url}`**, **`server.region`**, **`deploy.{context,id,published,skewProtectionToken}`**.\n- **`waitUntil(promise)`** — extend execution past the response (analytics, logs) without blocking it. Still subject to the CPU limit.\n\n**`Netlify` global:** `Netlify.context` (null outside the handler), `Netlify.env.{get,has,set,delete,toObject}`. `Netlify.env.set`/`delete` are **invocation-scoped only** — they do not persist env vars; use the Netlify env API endpoints.\n\n## Response caching\n\n⚠️ **Caching requires BOTH opting in AND setting headers — it's both or neither.** Setting `Cache-Control` on the returned `Response` does nothing without `cache: \"manual\"` in config, and vice versa. Default (either missing): every request invokes the function.\n\n1. Opt in: `cache: \"manual\"` (inline or `netlify.toml`).\n2. Set headers **inline in the function code** (not in `netlify.toml`):\n\n```ts\nimport type { Context, Config } from \"@netlify/edge-functions\";\n\nexport default async (req: Request, context: Context) => {\n  return new Response(\"Hello world\", {\n    headers: { \"cache-control\": \"public, s-maxage=3600\" },\n  });\n};\n\nexport const config: Config = { cache: \"manual\", path: \"/hello\" };\n```\n\nSupported cache headers: `Cache-Control`, `CDN-Cache-Control`, `Netlify-CDN-Cache-Control`, `Expires` (overridden by `max-age`/`s-maxage`), `Vary`, `Netlify-Vary`. See https://docs.netlify.com/build/caching/caching-overview\n\n**Atomic deploys void the cache:** `s-maxage`/`max-age`/`Expires` are discarded by a new deploy in the same deploy context, even mid-lifetime.\n\n**When to cache:** endpoint responses reusable across clients (e.g. identical SSR HTML). **Do not cache** middleware, routing/transform logic, or per-client personalization.\n\n⚠️ **Caching functions always shadow static files.** A caching function on `/*` serves `/cat.png` instead of the static `cat.png`.\n\n## Error handling (`onError`, inline only)\n\n- **`fail`** (default) — serve a generic error page.\n- **`/YOUR_CUSTOM_PATH`** — rewrite to a same-site path (must start with `/`); served without invoking edge functions for that path.\n- **`bypass`** — skip the erroring function, continue the chain.\n\n```ts\nexport const config: Config = { path: \"/hello\", onError: \"/unavailable\" };\n```\n\nFail closed for critical logic (auth); fail open (`bypass`) for progressive enhancement (nice-to-have localization).\n\n## Environment variables\n\n- Set via UI/CLI/API; scope **must include Functions** to reach edge runtime.\n- **Env vars in `netlify.toml` are NOT available to edge functions.**\n- **Build-scope vars are NOT available at edge runtime** — only during the build step. Embed their values at build time if needed.\n- Changes require a **new build and deploy**; each deploy freezes values at deploy time.\n- Access at runtime with `Netlify.env.get(key)` / `Netlify.env.toObject()`.\n\n```ts\nexport default async (request: Request, context: Context) => {\n  const value = Netlify.env.get(\"MY_IMPORTANT_VARIABLE\");\n  return new Response(`Value: ${value}`);\n};\n```\n\nNext.js Middleware note: with Netlify Edge Functions for Middleware on Next.js, `process.env` also works.\n\n## Runtime & modules\n\nDeno-based. Import modules by:\n- **Node built-ins:** `import { randomBytes } from \"node:crypto\";`\n- **Deno/URL imports:** `import React from \"https://esm.sh/react\";`\n- **npm packages (beta):** `npm install` then import by name. ⚠️ Beta — packages using native binaries (Prisma) or runtime dynamic imports (cowsay) may fail.\n\n**Import maps** (module names instead of URLs) — use a separate import map file, declared in `netlify.toml`:\n\n```toml\n[functions]\n  deno_import_map = \"./path/to/your/import_map.json\"\n```\n\nSupported Web APIs include `fetch`/`Request`/`Response`/`URL`/`File`/`Blob`, `console`, `atob`/`btoa`, `TextEncoder`/`TextDecoder` (+ stream variants), Web Crypto (`randomUUID`, `getRandomValues`, `SubtleCrypto`), WebSocket, timers, Streams API, URLPattern, `Performance`.\n\n## Local dev & deploy\n\n```bash\nnpm install netlify-cli -g\nnetlify dev        # runs edge functions on local requests\n# visit http://localhost:8888/test\n```\n\n- Debug: `netlify dev` with `--edge-inspect` or `--edge-inspect-brk` (see https://cli.netlify.com/commands/dev/).\n- Geo mocking: `--geo=mock` (San Francisco) or `--geo=mock --country=XX`.\n- ⚠️ **No local caching** — cache headers are ignored in local testing.\n- Manual deploys require **Netlify CLI 12.2.8+** (older versions error).\n- Deploys are **atomic** — old deploys keep old behavior until you publish a new production deploy.\n\n**Monitor:** production logs at Netlify UI **Cloud compute > Edge functions**. Each `console.*` log includes the generating function name. Retention ≥ 24h (7 days on some plans). Log Drains on Enterprise.\n\n## Limits & feature gaps\n\n- **Code size:** 20 MB compressed (bundle max).\n- **Memory:** 512 MB per set of deployed edge functions.\n- **CPU time:** 50 ms per request (excludes wait time; `waitUntil` work still counts).\n- **Response header timeout:** 40 s.\n- Cached responses do **not** count toward invocations.\n- **Split Testing** enabled → edge functions do **not** run.\n- **Custom Headers** (incl. basic auth) do **not** apply to edge functions.\n- **Prerendering** does not apply to edge-served paths.\n- Rewrites are **same-site only** — use `fetch` for other/external sites.\n- Multiple framework plugins generating edge functions may collide.\n- **Not** supported under HIPAA-compliant hosting.\n\nSee the overview at https://docs.netlify.com/build/edge-functions/overview.md and the full example library at https://edge-functions-examples.netlify.app/\n\n<!-- system: agent-context/edge-functions/system.md — human-owned, merged by ctx-gen; edit system.md, not this section -->\n# Netlify house rules (edge-functions)\n\nThese are org conventions and field-learned guardrails, not docs facts — they\nare merged into the rendered skill by ctx-gen and are never generated.\nExtracted from the previous hand-written netlify-edge-functions skill; owned\nby the skills maintainer.\n\n1. Check the framework's adapter/reference first: a custom edge function that\n   duplicates adapter-generated middleware causes conflicts. Only hand-write\n   an edge function when the framework doesn't already generate one for the\n   job.\n2. Scope `path` narrowly. `path: \"/*\"` intercepts every request — including\n   static assets — adding latency to each one and billing an edge invocation\n   for it.\n3. An edge function without a route (no config export, no netlify.toml\n   declaration) still deploys, but silently never runs: no build error, no\n   warning. When \"my edge function does nothing\", check the route first.\n4. Choose edge vs serverless by workload shape: edge functions for low-latency\n   request/response manipulation, geolocation logic, auth checks/redirects,\n   and A/B personalization; serverless functions for long-running work (up to\n   15 min), heavy Node.js dependencies, database-heavy operations,\n   background/scheduled tasks, or memory needs above 512 MB.\n5. Cache headers on an edge response do nothing without `cache: \"manual\"` in\n   config — it's both or neither. Setting `Cache-Control` on the returned\n   `Response` has no effect unless the function also opts in.\n6. When explaining declaration processing order, state the two-pass loop,\n   not just the ordering: Netlify runs the whole declaration order TWICE —\n   first pass runs only edge functions not configured for caching, second\n   pass runs the ones with caching configured. \"Non-cached before cached\"\n   without the loop framing is an incomplete answer.\n"},"changes":[{"path":"/description","type":"changed","before":"Guide for writing Netlify Edge Functions. Use when building middleware, geolocation-based logic, request/response manipulation, authentication checks, A/B testing, or any low-latency edge compute. Covers Deno runtime, context.next() middleware pattern, geolocation, and when to choose edge vs serverless.","after":"Write and configure Netlify Edge Functions — TypeScript/JavaScript handlers running in a Deno runtime at the network edge. Use when adding auth middleware or auth redirects, geolocation or localization logic, A/B testing or personalization, request/response transforms (rewrites/redirects), or edge SSR to a Netlify site. Triggers on tasks like \"add an edge function\", \"auth check at the edge\", \"redirect visitors by country\", \"A/B test with cookies\", \"rewrite requests\", \"personalize by geo\", or \"cache an edge response\". Covers the config export, path routing, the Context object, response caching, environment variables, and edge-vs-serverless choices. Check the framework's adapter first — only hand-write an edge function when the framework doesn't already generate one."},{"path":"/included_files","type":"changed","before":[{"relative_path":"LICENSE.txt","size_in_bytes":10776},{"relative_path":"agents/openai.yaml","size_in_bytes":352},{"relative_path":"assets/netlify-small.svg","size_in_bytes":1291},{"relative_path":"assets/netlify.png","size_in_bytes":2686}],"after":[]},{"path":"/skill_md_contents","type":"changed","before":"---\nname: netlify-edge-functions\ndescription: Guide for writing Netlify Edge Functions. Use when building middleware, geolocation-based logic, request/response manipulation, authentication checks, A/B testing, or any low-latency edge compute. Covers Deno runtime, context.next() middleware pattern, geolocation, and when to choose edge vs serverless.\n---\n\n# Netlify Edge Functions\n\nEdge functions run on Netlify's globally distributed edge network (Deno runtime), providing low-latency responses close to users.\n\n## Syntax\n\n```typescript\nimport type { Config, Context } from \"@netlify/edge-functions\";\n\nexport default async (req: Request, context: Context) => {\n  return new Response(\"Hello from the edge!\");\n};\n\nexport const config: Config = {\n  path: \"/hello\",\n};\n```\n\nPlace files in `netlify/edge-functions/`. Uses `.ts`, `.js`, `.tsx`, or `.jsx` extensions.\n\n## Config Object\n\n```typescript\nexport const config: Config = {\n  path: \"/api/*\",                    // URLPattern path(s)\n  excludedPath: \"/api/public/*\",     // Exclusions\n  method: [\"GET\", \"POST\"],           // HTTP methods\n  onError: \"bypass\",                 // \"fail\" (default), \"bypass\", or \"/error-page\"\n  cache: \"manual\",                   // Enable response caching\n};\n```\n\n## Middleware Pattern\n\nUse `context.next()` to invoke the next handler in the chain and optionally modify the response:\n\n```typescript\nexport default async (req: Request, context: Context) => {\n  // Before: modify request or short-circuit\n  if (!isAuthenticated(req)) {\n    return new Response(\"Unauthorized\", { status: 401 });\n  }\n\n  // Continue to origin/next function\n  const response = await context.next();\n\n  // After: modify response\n  response.headers.set(\"x-custom-header\", \"value\");\n  return response;\n};\n```\n\nReturn `undefined` to pass through without modification:\n\n```typescript\nexport default async (req: Request, context: Context) => {\n  if (!shouldHandle(req)) return; // continues to next handler\n  return new Response(\"Handled\");\n};\n```\n\n## Geolocation and IP\n\n```typescript\nexport default async (req: Request, context: Context) => {\n  const { city, country, subdivision, timezone } = context.geo;\n  const ip = context.ip;\n\n  if (country?.code === \"DE\") {\n    return Response.redirect(new URL(\"/de\", req.url));\n  }\n};\n```\n\nLocal dev with mocked geo: `netlify dev --geo=mock --country=US`\n\n## Environment Variables\n\nUse `Netlify.env` (not `process.env` or `Deno.env`):\n\n```typescript\nconst secret = Netlify.env.get(\"API_SECRET\");\n```\n\n## Module Support\n\n- **Node.js builtins**: `import { randomBytes } from \"node:crypto\";`\n- **npm packages**: Install via npm and import by name\n- **Deno modules**: URL imports (e.g., `import X from \"https://esm.sh/package\"`)\n\nFor URL imports, use an import map:\n\n```json\n// import_map.json\n{ \"imports\": { \"html-rewriter\": \"https://ghuc.cc/worker-tools/html-rewriter/index.ts\" } }\n```\n\n```toml\n# netlify.toml\n[functions]\n  deno_import_map = \"./import_map.json\"\n```\n\n## When to Use Edge vs Serverless\n\n| Use Edge Functions for | Use Serverless Functions for |\n|---|---|\n| Low-latency responses | Long-running operations (up to 15 min) |\n| Request/response manipulation | Complex Node.js dependencies |\n| Geolocation-based logic | Database-heavy operations |\n| Auth checks and redirects | Background/scheduled tasks |\n| A/B testing, personalization | Tasks needing > 512 MB memory |\n\n## Limits\n\n| Resource | Limit |\n|---|---|\n| CPU time | 50 ms per request |\n| Memory | 512 MB per deployed set |\n| Response header timeout | 40 seconds |\n| Code size | 20 MB compressed |\n","after":"---\nname: netlify-edge-functions\ndescription: Write and configure Netlify Edge Functions — TypeScript/JavaScript handlers running in a Deno runtime at the network edge. Use when adding auth middleware or auth redirects, geolocation or localization logic, A/B testing or personalization, request/response transforms (rewrites/redirects), or edge SSR to a Netlify site. Triggers on tasks like \"add an edge function\", \"auth check at the edge\", \"redirect visitors by country\", \"A/B test with cookies\", \"rewrite requests\", \"personalize by geo\", or \"cache an edge response\". Covers the config export, path routing, the Context object, response caching, environment variables, and edge-vs-serverless choices. Check the framework's adapter first — only hand-write an edge function when the framework doesn't already generate one.\n---\n\n# Netlify Edge Functions\n\n## Modern syntax (reach for this)\n\nExport a default handler plus a `config` object. Import `Config`/`Context` types from `@netlify/edge-functions`; `Request`/`Response`/`URL` are global.\n\n```ts\nimport type { Config, Context } from \"@netlify/edge-functions\";\n\nexport default async (request: Request, context: Context) => {\n  return new Response(\"Hello world\");\n};\n\nexport const config: Config = {\n  path: \"/test\",\n};\n```\n\n**Do not hand-write an edge function when your framework's adapter already generates middleware for the job** — duplicating it causes conflicts. Check the framework adapter/reference first.\n\n**Edge vs serverless:** use edge functions for low-latency request/response manipulation, geolocation logic, auth checks/redirects, and A/B personalization. Use serverless functions for long-running work (up to 15 min), heavy Node.js dependencies, database-heavy operations, background/scheduled tasks, or memory above 512 MB.\n\n## File location\n\n- Default directory: `YOUR_BASE_DIRECTORY/netlify/edge-functions`. Custom: `edge_functions` under `[build]` in `netlify.toml` (path relative to base directory).\n- Keep the directory **outside your publish directory** so source files aren't deployed.\n- Extensions: `.js`, `.ts`, `.jsx`, `.tsx` (`.jsx`/`.tsx` useful for SSR).\n- Same-name conflict: if `my-function.ts` and `my-function.js` both exist, the **TypeScript file is ignored** and the JavaScript one is deployed.\n\n## Routing — required, or the function silently never runs\n\n⚠️ **An edge function without a route (no `config` export and no `netlify.toml` declaration) still deploys but never runs — no build error, no warning.** When \"my edge function does nothing\", check the route first.\n\n⚠️ **Scope `path` narrowly.** `path: \"/*\"` intercepts every request including static assets, adding latency and billing an edge invocation for each one.\n\nEdge functions are **not** auto-assigned a URL route. Configure via inline `config` or `netlify.toml`.\n\n`path` is a `URLPattern` expression, must start with `/`, single string or array:\n\n```ts\nexport const config: Config = {\n  path: [\"/\", \"/products/*\"],\n  excludedPath: [\"/*.css\", \"/*.js\"],\n};\n```\n\nConfig properties: `path`, `excludedPath`, `pattern` (regex alternative to `path`), `excludedPattern`, `method`, `header`, `onError`, `cache`.\n\n### netlify.toml declaration\n\nUse `[[edge_functions]]` to declare multiple functions on one path and control order:\n\n```toml\n[[edge_functions]]\n  path = \"/admin\"\n  function = \"auth\"\n\n[[edge_functions]]\n  path = \"/admin\"\n  function = \"injector\"\n  cache = \"manual\"\n\n[[edge_functions]]\n  pattern = \"/products/(.*)\"\n  excludedPattern = \"/products/things/(.*)\"\n  function = \"highlight\"\n```\n\nProperties: `function`, `path`, `excludedPath`, `pattern`, `excludedPattern`, `header`, `cache`.\n\n**Merge precedence:** if the same function is declared both inline and in `netlify.toml`, configs merge and are treated as inline; inline wins duplicate fields.\n\n### Match by headers\n\n`header` keys are HTTP header names (case-insensitive); values are `true` (present), `false` (absent), or a string regex on the value. Multiple same-name values match against the comma-joined list.\n\n```ts\nexport const config: Config = {\n  header: { \"x-required\": true, \"x-forbidden\": false, \"user-agent\": \"(iPhone|Android)\" },\n  path: \"/*\",\n};\n```\n\n### Declaration processing order\n\nNetlify runs the whole declaration order **TWICE**: the first pass runs only edge functions **not** configured for caching; the second pass runs the ones **with** caching configured. Within that:\n\n1. Framework-generated functions declared in a config file.\n2. Your `netlify.toml` declarations (top-to-bottom order).\n3. Framework/integration-generated functions with inline config.\n4. Your inline declarations (**alphabetical by function file name**).\n\nTo control order across multiple functions on a path, prefer `netlify.toml` declarations over inline.\n\nAfter all functions run, Netlify evaluates redirect rules — unless a function returned a response and ended the chain. To customize order, use `netlify.toml`.\n\n**Order caveats:**\n- A returned response ends the chain; redirects for that path don't occur.\n- An edge function on the **target** of a static rewrite does **not** execute for rewritten requests.\n- `fetch()` for internal requests or returning a `URL` starts a **new request chain** and re-runs matching edge functions. Use `context.next()` to avoid re-running them.\n\n## Function signature & return values\n\nHandler receives `(request: Request, context: Context)`. Return one of:\n- a `Response` — delivered to the client; **ends the request chain** (declared redirects for that path don't run).\n- a `URL` — rewrite to a **same-site** URL with 200 status; address bar unchanged. Same-site only — for other sites use `fetch`.\n- `undefined` / empty `return;` — bypass this function, continue the chain.\n\nModify a response as middleware by awaiting `context.next()`:\n\n```ts\nimport type { Context } from \"@netlify/edge-functions\";\n\nexport default async (request: Request, context: Context) => {\n  const url = new URL(request.url);\n  if (url.searchParams.get(\"method\") !== \"transform\") return;\n\n  const response = await context.next();\n  const text = await response.text();\n  return new Response(text.toUpperCase(), response);\n};\n```\n\nNetlify does **not** add headers to edge function requests — use `context` for client request info.\n\n## Common patterns\n\n**Redirect by geo + cookie:**\n```ts\nexport default async (req: Request, { cookies, geo }: Context) => {\n  if (geo.city === \"Paris\" && cookies.get(\"promo-code\") === \"15-for-followers\") {\n    return Response.redirect(new URL(\"/subscriber-sale\", req.url));\n  }\n};\n```\n\n**Rewrite (same-site, 200):**\n```ts\nexport default async (request: Request, { geo }: Context) => {\n  if (geo.city === \"Paris\") return new URL(\"/subscriber-sale\", request.url);\n};\n```\n\n**Read request body then continue** — a body can only be read once, so pass a new `Request` with an unread body:\n```ts\nexport default async (req: Request, context: Context) => {\n  const body = await req.json();\n  if (!isValid(body.access_token)) return new Response(\"forbidden\", { status: 403 });\n  return context.next(new Request(req, { body: JSON.stringify(body) }));\n};\n```\n\n**Conditional request:**\n```ts\nexport default async (req: Request, { next }: Context) => {\n  const res = await next({ sendConditionalRequest: true });\n  if (res.status === 304) return res;\n  const text = await res.text();\n  return new Response(text.toUpperCase(), res);\n};\n```\n\n**SSR with React (`.tsx`):**\n```tsx\nimport React from \"https://esm.sh/react\";\nimport { renderToReadableStream } from \"https://esm.sh/react-dom/server\";\nimport type { Config, Context } from \"@netlify/edge-functions\";\n\nexport default async function handler(req: Request, context: Context) {\n  const stream = await renderToReadableStream(\n    <html><body><h1>Hello {context.geo.country?.name}</h1></body></html>\n  );\n  return new Response(stream, { status: 200, headers: { \"Content-Type\": \"text/html\" } });\n}\n\nexport const config: Config = { path: \"/hello\" };\n```\n\n## Context object\n\n- **`geo`** — `city`, `country.{code,name}`, `subdivision.{code,name}`, `latitude`, `longitude`, `timezone`, `postalCode`.\n- **`cookies`** — `get(name)`, `set(options)` (CookieStore.set format), `delete(name|options)`. Cross-subdomain cookies need a custom domain — impossible on `netlify.app` (Public Suffix List).\n- **`next(options?)`** / **`next(request, options?)`** — invoke the next item in the chain; returns a `Promise<Response>` you can modify. `options.sendConditionalRequest: true` for conditional requests. Only call `next` if you need the response body. Pass an explicit `Request` when you've read the body.\n- **`params`** — path params, e.g. path `/pets/:name` + request `/pets/winter` → `{name:\"winter\"}`. Query string: use `request.url`.\n- **`ip`** — client IP string.\n- **`requestId`** — Netlify request ID.\n- **`account.id`**, **`site.{id,name,url}`**, **`server.region`**, **`deploy.{context,id,published,skewProtectionToken}`**.\n- **`waitUntil(promise)`** — extend execution past the response (analytics, logs) without blocking it. Still subject to the CPU limit.\n\n**`Netlify` global:** `Netlify.context` (null outside the handler), `Netlify.env.{get,has,set,delete,toObject}`. `Netlify.env.set`/`delete` are **invocation-scoped only** — they do not persist env vars; use the Netlify env API endpoints.\n\n## Response caching\n\n⚠️ **Caching requires BOTH opting in AND setting headers — it's both or neither.** Setting `Cache-Control` on the returned `Response` does nothing without `cache: \"manual\"` in config, and vice versa. Default (either missing): every request invokes the function.\n\n1. Opt in: `cache: \"manual\"` (inline or `netlify.toml`).\n2. Set headers **inline in the function code** (not in `netlify.toml`):\n\n```ts\nimport type { Context, Config } from \"@netlify/edge-functions\";\n\nexport default async (req: Request, context: Context) => {\n  return new Response(\"Hello world\", {\n    headers: { \"cache-control\": \"public, s-maxage=3600\" },\n  });\n};\n\nexport const config: Config = { cache: \"manual\", path: \"/hello\" };\n```\n\nSupported cache headers: `Cache-Control`, `CDN-Cache-Control`, `Netlify-CDN-Cache-Control`, `Expires` (overridden by `max-age`/`s-maxage`), `Vary`, `Netlify-Vary`. See https://docs.netlify.com/build/caching/caching-overview\n\n**Atomic deploys void the cache:** `s-maxage`/`max-age`/`Expires` are discarded by a new deploy in the same deploy context, even mid-lifetime.\n\n**When to cache:** endpoint responses reusable across clients (e.g. identical SSR HTML). **Do not cache** middleware, routing/transform logic, or per-client personalization.\n\n⚠️ **Caching functions always shadow static files.** A caching function on `/*` serves `/cat.png` instead of the static `cat.png`.\n\n## Error handling (`onError`, inline only)\n\n- **`fail`** (default) — serve a generic error page.\n- **`/YOUR_CUSTOM_PATH`** — rewrite to a same-site path (must start with `/`); served without invoking edge functions for that path.\n- **`bypass`** — skip the erroring function, continue the chain.\n\n```ts\nexport const config: Config = { path: \"/hello\", onError: \"/unavailable\" };\n```\n\nFail closed for critical logic (auth); fail open (`bypass`) for progressive enhancement (nice-to-have localization).\n\n## Environment variables\n\n- Set via UI/CLI/API; scope **must include Functions** to reach edge runtime.\n- **Env vars in `netlify.toml` are NOT available to edge functions.**\n- **Build-scope vars are NOT available at edge runtime** — only during the build step. Embed their values at build time if needed.\n- Changes require a **new build and deploy**; each deploy freezes values at deploy time.\n- Access at runtime with `Netlify.env.get(key)` / `Netlify.env.toObject()`.\n\n```ts\nexport default async (request: Request, context: Context) => {\n  const value = Netlify.env.get(\"MY_IMPORTANT_VARIABLE\");\n  return new Response(`Value: ${value}`);\n};\n```\n\nNext.js Middleware note: with Netlify Edge Functions for Middleware on Next.js, `process.env` also works.\n\n## Runtime & modules\n\nDeno-based. Import modules by:\n- **Node built-ins:** `import { randomBytes } from \"node:crypto\";`\n- **Deno/URL imports:** `import React from \"https://esm.sh/react\";`\n- **npm packages (beta):** `npm install` then import by name. ⚠️ Beta — packages using native binaries (Prisma) or runtime dynamic imports (cowsay) may fail.\n\n**Import maps** (module names instead of URLs) — use a separate import map file, declared in `netlify.toml`:\n\n```toml\n[functions]\n  deno_import_map = \"./path/to/your/import_map.json\"\n```\n\nSupported Web APIs include `fetch`/`Request`/`Response`/`URL`/`File`/`Blob`, `console`, `atob`/`btoa`, `TextEncoder`/`TextDecoder` (+ stream variants), Web Crypto (`randomUUID`, `getRandomValues`, `SubtleCrypto`), WebSocket, timers, Streams API, URLPattern, `Performance`.\n\n## Local dev & deploy\n\n```bash\nnpm install netlify-cli -g\nnetlify dev        # runs edge functions on local requests\n# visit http://localhost:8888/test\n```\n\n- Debug: `netlify dev` with `--edge-inspect` or `--edge-inspect-brk` (see https://cli.netlify.com/commands/dev/).\n- Geo mocking: `--geo=mock` (San Francisco) or `--geo=mock --country=XX`.\n- ⚠️ **No local caching** — cache headers are ignored in local testing.\n- Manual deploys require **Netlify CLI 12.2.8+** (older versions error).\n- Deploys are **atomic** — old deploys keep old behavior until you publish a new production deploy.\n\n**Monitor:** production logs at Netlify UI **Cloud compute > Edge functions**. Each `console.*` log includes the generating function name. Retention ≥ 24h (7 days on some plans). Log Drains on Enterprise.\n\n## Limits & feature gaps\n\n- **Code size:** 20 MB compressed (bundle max).\n- **Memory:** 512 MB per set of deployed edge functions.\n- **CPU time:** 50 ms per request (excludes wait time; `waitUntil` work still counts).\n- **Response header timeout:** 40 s.\n- Cached responses do **not** count toward invocations.\n- **Split Testing** enabled → edge functions do **not** run.\n- **Custom Headers** (incl. basic auth) do **not** apply to edge functions.\n- **Prerendering** does not apply to edge-served paths.\n- Rewrites are **same-site only** — use `fetch` for other/external sites.\n- Multiple framework plugins generating edge functions may collide.\n- **Not** supported under HIPAA-compliant hosting.\n\nSee the overview at https://docs.netlify.com/build/edge-functions/overview.md and the full example library at https://edge-functions-examples.netlify.app/\n\n<!-- system: agent-context/edge-functions/system.md — human-owned, merged by ctx-gen; edit system.md, not this section -->\n# Netlify house rules (edge-functions)\n\nThese are org conventions and field-learned guardrails, not docs facts — they\nare merged into the rendered skill by ctx-gen and are never generated.\nExtracted from the previous hand-written netlify-edge-functions skill; owned\nby the skills maintainer.\n\n1. Check the framework's adapter/reference first: a custom edge function that\n   duplicates adapter-generated middleware causes conflicts. Only hand-write\n   an edge function when the framework doesn't already generate one for the\n   job.\n2. Scope `path` narrowly. `path: \"/*\"` intercepts every request — including\n   static assets — adding latency to each one and billing an edge invocation\n   for it.\n3. An edge function without a route (no config export, no netlify.toml\n   declaration) still deploys, but silently never runs: no build error, no\n   warning. When \"my edge function does nothing\", check the route first.\n4. Choose edge vs serverless by workload shape: edge functions for low-latency\n   request/response manipulation, geolocation logic, auth checks/redirects,\n   and A/B personalization; serverless functions for long-running work (up to\n   15 min), heavy Node.js dependencies, database-heavy operations,\n   background/scheduled tasks, or memory needs above 512 MB.\n5. Cache headers on an edge response do nothing without `cache: \"manual\"` in\n   config — it's both or neither. Setting `Cache-Control` on the returned\n   `Response` has no effect unless the function also opts in.\n6. When explaining declaration processing order, state the two-pass loop,\n   not just the ordering: Netlify runs the whole declaration order TWICE —\n   first pass runs only edge functions not configured for caching, second\n   pass runs the ones with caching configured. \"Non-cached before cached\"\n   without the loop framing is an incomplete answer.\n"}],"summary":"Fields changed: 3. /description, /included_files, /skill_md_contents.","summary_kind":"deterministic","summary_metadata":{}}