netlify-edge-functions について
netlify-edge-functions は、Netlify Edge Functions の開発者向けスキルです。ルーティング、ミドルウェア、リクエストコンテキスト、キャッシュ、エラー処理、環境変数、実行上の制約を説明します。
主な機能
- 01パス、パターン、リクエスト条件の設定
- 02ミドルウェアによるオリジン応答の変更
- 03リライトとリダイレクトの利用
- 04Cookie、位置情報、ユーザーコンテキストの参照
- 05応答キャッシュとエラー処理の設定
活用シーン
- 01選択したパスやリクエストでエッジ処理を実行
- 02ブログページのオリジン応答を変更
- 03ユーザー情報や位置情報に基づくアクセス制限
- 04再利用する応答のキャッシュ
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ netlify/ context-and-tools --skill netlify-edge-functionsインストール後、Claude Code で/netlify-edge-functionsを入力して読み込まれたか確認します。
詳細
Netlify Edge Functions
このフォームを使用
// netlify/edge-functions/hello.ts
import type { Config, Context } from "@netlify/edge-functions";
export default async (request: Request, context: Context) => {
return new Response(`Hello from ${context.geo.country?.name ?? "somewhere"}`);
};
export const config: Config = { path: "/hello" };
- 1 ファイルにつき 1 つの関数です。デフォルトエクスポートのハンドラーと、ルートを指定する
configエクスポートを用意します。代わりに、netlify.tomlでルートを宣言することもできます。 RequestとResponseはグローバルです。@netlify/edge-functionsからContextとConfigの型をインポートします。- 環境変数は
Netlify.env.get()で読み取ります。process.envが使えるのは Next.js のミドルウェアのみです。
次のことは避けてください:
config.pathもpatternもなく、[[edge_functions]]エントリーもないファイル。デプロイはされますが、実行されません。deno.jsonのインポートマップ。サポートされていません。[functions] deno_import_mapを指定した別ファイルを使用してください。netlify.tomlで設定した変数。エッジ関数からは参照できません。- 永続化のための
Netlify.env.set()またはNetlify.env.delete()。これらの影響は現在の呼び出しに限られます。 - 現在のパスに対する同一サイトへの
fetch()。代わりにcontext.next()を使用してください。
⚠️ 注意点
- ルートがない場合、何も実行されません。 エッジ関数に URL は自動で割り当てられません。
path: "/*"はアセットを含むすべてのリクエストで実行されます。 対象を絞るか、excludedPathを追加してください。- キャッシュには 2 つの設定が必要です: 設定の
cache: "manual"と、返すResponseのキャッシュヘッダーです。どちらか一方だけではキャッシュされません。 - キャッシュされた関数は静的ファイルを隠します。
/*に設定されたキャッシュ関数は、ファイルの代わりに/cat.pngを配信します。 Responseを返すとチェーンが終了します。 そのパスに対するリダイレクトルールは実行されません。- 同一サイトへの
fetch()とURLの書き換えは、新しいリクエストチェーンを開始します。 一致するエッジ関数が再度実行されます。同じパスの静的アセットまたはサーバーレス関数にアクセスするには、context.next()を使用してください。 - リクエストボディは 1 回だけ読み取れます。
await req.json()の後、new Request(req, { body })をcontext.next()に渡してください。 foo.tsとfoo.jsの名前が重複する場合: TS ファイルは無視され、JS ファイルがデプロイされます。- リクエストあたりの CPU 使用時間は 50 ms です。
waitUntilの処理も含まれます。 - Split Testing が有効なサイトでは実行されません。
- カスタムヘッダーと basic-auth ヘッダーはエッジ関数に適用されません。
- HIPAA 準拠のホスティングではサポートされません。
netlify.appは Public Suffix List に含まれています。 サブドメインをまたぐ Cookie にはカスタムドメインが必要です。
ファイル
| 内容 | 場所 |
|---|---|
| 関数 | <base>/netlify/edge-functions/*.{js,ts,jsx,tsx} |
| カスタムディレクトリ | [build] edge_functions = "my-dir"(base dir からの相対パスです。publish dir の外に置いてください) |
| toml 内の名前 | 拡張子を除いたファイル名(auth.ts → function = "auth") |
| インポートマップ | 任意の JSON ファイル。[functions] の deno_import_map = "./import_map.json" で参照します |
戻り値
| 戻り値 | 効果 |
|---|---|
Response | クライアントに送信され、チェーンが終了します |
URL | 同一サイトの書き換えを行います。ステータスは 200 で、アドレスバーは変わりません |
undefined / return; | この関数をスキップし、チェーンを続行します |
エンドポイント
export default async () =>
new Response("Hello, World!", { headers: { "content-type": "text/html" } });
export const config = { path: "/api/hello" };
リダイレクト/書き換え
import type { Context } from "@netlify/edge-functions";
export default async (req: Request, { cookies, geo }: Context) => {
if (geo.city === "Paris" && cookies.get("promo-code") === "15-for-followers") {
return Response.redirect(new URL("/subscriber-sale", req.url)); // redirect
// or: return new URL("/subscriber-sale", req.url); // rewrite (same-site only)
}
};
export const config = { path: "/sale" };
書き換え先は同一サイトである必要があります。他のサイトにアクセスするには、fetch を使用してください。
ミドルウェア(オリジンのレスポンスを変更)
import type { Context } from "@netlify/edge-functions";
export default async (request: Request, context: Context) => {
if (new URL(request.url).searchParams.get("method") !== "transform") return;
const response = await context.next();
return new Response((await response.text()).toUpperCase(), response);
};
export const config = { path: "/blog/*" };
レスポンスボディが必要な場合にのみ context.next() を呼び出してください。undefined を返すだけであれば、チェーンは自動的に続行されます。
条件付きリクエスト
export default async (req: Request, { next }: Context) => {
const res = await next({ sendConditionalRequest: true });
if (res.status === 304) return res;
return new Response((await res.text()).toUpperCase(), res);
};
デフォルトでは、next() は条件付きヘッダーを削除するため、常に完全なレスポンスを返します。
ボディを読み取ってから続行
export default async (req: Request, context: Context) => {
const body = await req.json();
if (!isValid(body.access_token)) return new Response("forbidden", { status: 403 });
return context.next(new Request(req, { body: JSON.stringify(body) }));
};
Netlify のユーザーゲート(非公開/チームログインで保護されたプロジェクト)
import type { Config, Context } from "@netlify/edge-functions";
export default async (req: Request, context: Context) => {
const user = context.user; // undefined: public project, password protection, netlify dev
if (!user) return new Response("Forbidden", { status: 403 });
const allowed = (Netlify.env.get("ADMIN_USER_IDS") ?? "").split(",");
if (!allowed.includes(user.id)) return new Response("Forbidden", { status: 403 });
// user.email may be undefined, so use it for display only
};
export const config: Config = { path: "/admin/*" };
レスポンス送信後の処理
export default async (req: Request, context: Context) => {
context.waitUntil(fetch("https://analytics.example.com/event", { method: "POST" }));
return new Response("ok");
};
waitUntil はレスポンスをブロックしませんが、50 ms の CPU 使用時間制限には引き続き含まれます。
ストリーミング SSR(.tsx)
import React from "https://esm.sh/react";
import { renderToReadableStream } from "https://esm.sh/react-dom/server";
import type { Config, Context } from "@netlify/edge-functions";
export default async (req: Request, context: Context) => {
const stream = await renderToReadableStream(
<html><body><h1>Hello {context.geo.country?.name}</h1></body></html>
);
return new Response(stream, { headers: { "Content-Type": "text/html" } });
};
export const config: Config = { path: "/hello" };
Context
| プロパティ | 備考 |
|---|---|
cookies.get(name) / .set(options) / .delete(name | options) | set と delete は CookieStore のオプション形式を使用し、送信するレスポンスに対して動作します |
geo | city、country.{code,name}、subdivision.{code,name}、latitude、longitude、timezone、postalCode(形式は異なる場合があります) |
ip | クライアントの IP |
next(options?) / next(request, options?) | チェーン内の次の項目から返される Promise<Response> |
params | path の URLPattern 名前付きグループ(/pets/:name → { name })。クエリ文字列を取得するには、request.url を使用します |
user | { id, email?, expiresAt } または undefined。これは Netlify アカウントであり、Netlify Identity ではありません。Identity には、@netlify/identity の getUser() を使用してください |
waitUntil(promise) | レスポンスの送信後に実行されます |
requestId, account.id, server.region | — |
site | { id, name, url } |
deploy | { context, id, published, skewProtectionToken } |
Netlify は、クライアント情報を含むヘッダーをリクエストに追加しません。クライアント情報は代わりに context から読み取ってください。
Netlify.context は同じオブジェクトを返しますが、ハンドラー内でのみ使用できます。ハンドラーの外では null です。
Netlify.env:get(name)、has(name)、toObject()、および呼び出しスコープの set と delete。環境変数を永続的に変更するには、env API を使用してください。
Web API:
console、atob、btoafetch、Request、Response、URL、File、BlobTextEncoder/TextDecoder(およびそれらの Stream バリアント)、Performance- Web Crypto:
randomUUID、getRandomValues、SubtleCrypto - WebSocket
setTimeout、clearTimeout、setInterval- Streams:
ReadableStream、WritableStream、TransformStream - URLPattern
モジュール:
- Node の組み込みモジュール:
node:プレフィックスを使用します(import { randomBytes } from "node:crypto")。 - Deno モジュール:URL、またはインポートマップを介してインポートします。
- npm パッケージ(ベータ版):パッケージ名でインポートします。ネイティブバイナリを含むパッケージや、実行時に動的インポートを行うパッケージは動作しない場合があります。
宣言
インライン config
| キー | 値 |
|---|---|
path | URLPattern 文字列または配列。/ で始まる必要があります |
excludedPath | URLPattern 文字列または配列。/ で始まる必要があります |
pattern / excludedPattern | path / excludedPath の代わりに使う正規表現 |
method | HTTP メソッドの文字列または配列 |
header | { name: true | false | "regex" }。名前では大文字と小文字を区別せず、複数の値を持つヘッダーはコンマで結合したリストに照合されます |
onError | "fail"(デフォルト)、"/custom-path"、または "bypass" |
cache | "manual" |
export const config: Config = {
path: "/*",
excludedPath: ["/*.css", "/*.js", "/img/*"],
header: { "user-agent": "(iPhone|Android)" },
};
netlify-agent-category リクエストヘッダーは、任意のユーザーエージェントをクライアントカテゴリに対応付けます。
netlify.toml
toml 形式では、function、path、excludedPath、pattern、excludedPattern、[edge_functions.header]、および cache をサポートしています。onError はインラインで設定してください。同じパス上に複数の関数を明示的な順序で実行する必要がある場合は、toml を使用してください。
[[edge_functions]]
path = "/admin"
function = "auth"
[[edge_functions]]
path = "/admin"
function = "injector"
cache = "manual"
[[edge_functions]]
pattern = "/products/(.*)"
excludedPattern = "/products/things/(.*)"
function = "highlight"
[[edge_functions]]
path = "/*"
excludedPath = "/img/*"
function = "common"
[edge_functions.header]
x-required = true
x-forbidden = false
/admin へのリクエストでは、auth、common、injector の順に実行されます。
順序
Netlify は、リクエストのパスに対して次の順序を2回適用します。1回目はキャッシュが設定されていない関数のみを実行し、2回目はキャッシュされた関数のみを実行します。
- 設定ファイルで宣言された、フレームワークが生成した関数。
netlify.tomlの宣言(上から順)。- フレームワークおよびインテグレーションが生成したインライン宣言。
- インライン宣言(ファイル名のアルファベット順)。
同じ関数が toml とインラインの両方で宣言されている場合、設定はマージされて手順 4 として扱われます。同じフィールドが重複する場合は、インラインの設定が優先されます。
エッジ関数の実行後に、関数がすでに Response を返していない限り、リダイレクトルールが実行されます。新しいパスへのリダイレクトでは、そのパスに対して一連の処理が最初から繰り返されます。
静的リライトのターゲットで宣言されたエッジ関数は、リライトされたリクエストでは実行されません。
レスポンスのキャッシュ
import type { Config } from "@netlify/edge-functions";
export default async () =>
new Response("Hello world", { headers: { "cache-control": "public, s-maxage=3600" } });
export const config: Config = { path: "/hello", cache: "manual" };
- ヘッダーは関数コードで設定する必要があります。対応しているヘッダー:
Cache-Control、CDN-Cache-Control、Netlify-CDN-Cache-ControlExpires(max-age/s-maxageで上書きされます)Vary、Netlify-Vary
- キャッシュの概要をご覧ください。
- 同じデプロイコンテキストで新しいデプロイを行うと、キャッシュされたレスポンスは無効になります。
- キャッシュヒット時は関数が呼び出されず、呼び出し回数の上限にもカウントされません。
- キャッシュするもの:SSR など、再利用可能なエンドポイントの出力。
- キャッシュしないもの:Cookie に基づく商品表示順など、ルーティングやパーソナライズを行うミドルウェア。
- ローカル開発ではキャッシュはありません。キャッシュヘッダーはローカルでは無視されます。
エラー処理(onError)
| 値 | 動作 |
|---|---|
"fail"(デフォルト) | チェーンが停止し、一般的なエラーページが表示されます |
"/unavailable" | 同じサイト内のそのパスに書き換えられます。そのパス上のエッジ関数は呼び出されません |
"bypass" | 失敗した関数をスキップし、チェーンを続行します |
重要なロジックではフェイルクローズにします。内部認証にはデフォルトを、顧客向け認証にはカスタムパスを使用します。ローカライズなど、必須ではない機能ではフェイルオープン(bypass)にします。
環境変数
- 変数には Functions スコープが必要です。
- Build スコープの変数はビルド時のみ存在します。必要に応じて、ビルド時にその値を埋め込んでください。
- 変数を変更するには、新たにデプロイする必要があります。
- デプロイ情報(サイト、リージョン、デプロイ ID)は環境変数ではなく、
contextから取得します。
ローカル開発/デプロイ/ログ
npm install netlify-cli -gを実行し、続けてnetlify devを実行します。http://localhost:8888/<path>にアクセスしてください。- 変更は次のリクエストで反映されます。
--edge-inspectまたは--edge-inspect-brkでデバッグできます。--geo=mock(サンフランシスコ)、または--geo=mock --country=DEで位置情報をモックできます。- ローカルでは
context.userは未定義です。
- 手動デプロイには CLI ≥ 12.2.8 が必要です。デプロイはアトミックで、デプロイプレビューとロールバックにはエッジ関数も含まれます。
- ログ:Netlify UI で Cloud compute > Edge functions に移動します(
https://app.netlify.com/projects/{site_name}/edge-functions)。- 古いデプロイの場合:Deploys → デプロイを開く → Edge Functions。
console.logの出力には関数名のタグが付けられます。- 保持期間は 24 時間以上です(プランによっては 7 日間)。
- Log Drains は Enterprise プランで利用できます。
制限
| 制限 | 値 |
|---|---|
| バンドルサイズ | 圧縮時 20 MB |
| メモリ(デプロイ済み関数のセットあたり) | 512 MB |
| リクエストあたりの CPU 時間 | 50 ms(I/O 待機時間を除く) |
| レスポンスヘッダーのタイムアウト | 40 s |
- エッジ関数が配信するパスには、プリレンダリングは適用されません。
- エッジ関数を生成するフレームワークプラグインが複数あると、競合する場合があります。
ドキュメント:API · 宣言 · オプションの設定 · 制限 · 例
Netlify のルール(edge-functions)
これらは組織内の規約と現場で得たガードレールであり、ドキュメント上の事実ではありません。ctx-gen によってレンダリングされるスキルに統合され、生成されることはありません。 以前の手書きの netlify-edge-functions スキルから抽出したもので、スキルメンテナーが管理しています。
- まずフレームワークのアダプター/リファレンスを確認してください。アダプターが生成するミドルウェアと重複するカスタムエッジ関数は、競合の原因になります。その処理をフレームワークがすでに生成している場合は、エッジ関数を手書きしないでください。
pathの範囲は狭くしてください。path: "/*"は静的アセットを含むすべてのリクエストをインターセプトするため、各リクエストに遅延を加え、そのたびにエッジ呼び出しの料金が発生します。- ルートが設定されていないエッジ関数(config export も netlify.toml の宣言もない場合)はデプロイされますが、ひそかに実行されないままになります。ビルドエラーも警告も出ません。「エッジ関数が何もしない」という場合は、まずルートを確認してください。
- ワークロードの特性に応じて、エッジ関数とサーバーレス関数を選んでください。低遅延のリクエスト/レスポンス操作、位置情報ロジック、認証チェック/リダイレクト、A/B パーソナライズにはエッジ関数を使用してください。長時間実行される処理(最大 15 分)、大きな Node.js 依存関係、データベース負荷の高い処理、バックグラウンド/スケジュールタスク、または 512 MB を超えるメモリが必要な処理には、サーバーレス関数を使用してください。
- エッジレスポンスのキャッシュヘッダーは、config で
cache: "manual"を指定しないと何も効果がありません。両方設定するか、どちらも設定しないかのいずれかです。返されるResponseにCache-Controlを設定しても、関数側でオプトインしない限り効果はありません。 - 宣言の処理順を説明する場合は、順序だけでなく 2 回のループについて述べてください。Netlify は宣言順全体を 2 回実行します。1 回目はキャッシュ用に設定されていないエッジ関数のみを実行し、2 回目はキャッシュ用に設定されたエッジ関数を実行します。「キャッシュなしがキャッシュありより先」という説明だけでは、ループの枠組みが抜けており、不完全です。
- プライベートプロジェクト、またはチームログイン保護が有効なプロジェクトでは、
context.userはリクエストを行った Netlify ユーザーを示します(id、email、expiresAt)。別のログイン機能を追加する代わりに、ユーザーごとのチェックやパーソナライズに使用してください。また、ユーザーの識別にクライアントから送信されたヘッダーや Cookie を決して使用しないでください。エッジ関数の実行前に Netlify がログインを検証します。すべての例でcontext.userがundefinedになる場合(公開プロジェクト、基本パスワード保護、netlify dev)を処理する必要があります。また、emailもundefinedになることがあるため、判断の基準にはidを使用してください。これは Netlify アカウントであり、Netlify Identity ユーザーではありません。