リダイレクトカタログをインポートする

CloudflareBeginner
オンラインで実践に進む

はじめに

ドキュメントサイトでページを移動した場合でも、古いリンクから正しい場所へアクセスできるようにする必要があります。リダイレクトとは、ブラウザーに別の URL をリクエストさせる HTTP レスポンスです。この実験では、KV に古いパスと新しいドキュメントパスの対応を記録する小規模なカタログを保存します。

まず、用意された JSON データセットを確認して検証してからインポートします。その後、複数のページに分けてカタログを読み取ります。ページネーションとは、結果を一定数ずつ取得し、継続マーカーを使って次の結果を取得する仕組みです。最後に、1 つの転送先を変更し、2 つのエントリを廃止します。その際、同じ namespace にある無関係なレコードは保持します。この方法は、キーを 1 つずつ編集するのではなく、設定の集合を管理するときに役立ちます。

先に KV のガイド付き実験を完了してください。この新しい VM には、/home/labex/project/redirect-catalog に Node.js 22.22.0 とプロジェクトローカルの Wrangler 4.131.1 が用意されています。セットアップでは 5 つの合成リダイレクトを用意しますが、インポートやクラウドリソースの作成は行いません。アカウントの読み取り、Worker への書き込み、KV への書き込みについて、同じ権限を持つ自分の学習用アカウントを使用してください。使い捨ての Worker と namespace が 1 つあれば十分です。この小規模なデータセットに有料プランへのアップグレードやドメインの購入は必要ありません。公開カタログにはサンプルパスだけが含まれます。

リダイレクト用 namespace に接続する

このステップでは、小規模なリダイレクトカタログ用に独立した namespace を接続します。ROUTES binding は、コマンドライン操作と Worker の両方でこの namespace を識別します。各実験では専用のリソースを使用するため、今回のカタログが以前の namespace に影響することはありません。

用意されたプロジェクトに移動します。

cd /home/labex/project/redirect-catalog

一度だけ一意な名前を生成します。openssl rand -hex 6 はランダムなサフィックスを出力し、$(...) はその値を名前に挿入します。シェル変数に保存した名前は、このターミナルで続けて実行するコマンドから利用できます。

WORKER_NAME="labex-routes-$(openssl rand -hex 6)"
printf '%s\n' "$WORKER_NAME"

この VM を認証します。アカウント ID の読み取りに加えて、Workers Scripts Write によりデプロイと削除を行い、Workers KV Write により今回の実験の namespace とキーを管理できます。

npx wrangler login --device --browser=false --scopes account:read user:read workers_scripts:write workers_kv:write

表示されたデバイス用リンクをブラウザーで開き、現在のコードを入力します。要求された権限と学習用アカウントを確認して、Wrangler を承認してください。同意ページにバックグラウンドアクセスが表示される場合もあります。ターミナルに戻り、ログインが完了するまで待ちます。

Create a Feature Flag Store で導入した Worker と KV の書き込み権限をもう一度確認します。承認する前に、学習用アカウントが正しいことを確認してください。

npx wrangler whoami --json

loggedIn: true と、学習用アカウントの name を確認します。アカウントが 1 つだけ表示される場合も確認してください。そのアカウントの id をコピーします。以下の設定に保存する際は、コマンドを実行する前に YOUR_ACCOUNT_ID を置き換えてください。ここでの cat ヒアドキュメントは、2 つの JSON 行の間にあるすべての内容をファイルへ書き込みます。> はファイルを置き換えます。区切り文字を引用していないため、シェルは $WORKER_NAME を展開します。

cat > wrangler.jsonc <<JSON
{
  "name": "$WORKER_NAME",
  "main": "src/index.js",
  "compatibility_date": "2026-07-30",
  "account_id": "YOUR_ACCOUNT_ID",
  "workers_dev": true
}
JSON

そのアカウントに namespace を作成します。namespace のタイトルには Worker と同じ一意な名前を含めるため、後で対応するリソースを識別できます。--update-config=false を指定すると、binding の編集内容が自動的にファイルへ反映されず、自分で確認できます。

npx wrangler kv namespace create "$WORKER_NAME-routes" --update-config=false

出力に新しい namespace ID が表示されます。それをコピーし、次の完全な設定にある YOUR_ACCOUNT_IDYOUR_NAMESPACE_ID を置き換えてください。ROUTES という binding 名はコードから参照するための名前であり、ID は実際の Cloudflare リソースを識別します。

cat > wrangler.jsonc <<JSON
{
  "name": "$WORKER_NAME",
  "main": "src/index.js",
  "compatibility_date": "2026-07-30",
  "account_id": "YOUR_ACCOUNT_ID",
  "workers_dev": true,
  "kv_namespaces": [
    { "binding": "ROUTES", "id": "YOUR_NAMESPACE_ID" }
  ]
}
JSON
npx wrangler kv namespace list

今回の実験で作成した namespace のタイトルを探し、ID がファイルの内容と一致することを確認します。他の namespace が表示されても、そのままにしてください。この設定には、後続のコマンドで使用するアカウントとリソースが記録されています。binding は namespace への参照であり、データのコピーではありません。

小規模なカタログを検証してインポートする

このステップでは、1 つのコマンドですべてのエントリを書き込む前に、データセットを確認します。一括操作を使うと繰り返し作業を減らせますが、用意されたデータに含まれる誤りも繰り返し適用されます。まず、準備済みのファイルを読み取ります。

cat redirects.json

各オブジェクトには、route:/old-start のような key と、/docs/start のような value があります。route: プレフィックスはカタログのレコードをグループ化します。これはキーの一部であり、ディレクトリではありません。転送先はこのサイト上のパスであり、任意の外部 URL ではありません。

通常の Node.js 検証スクリプトを作成します。このスクリプトはファイル名を読み取り、配列と各フィールドを確認し、重複キーを拒否します。すべてのエントリが検証に合格した場合にのみ件数を出力します。Set は、すでに確認したキーを記録します。正規表現は、この学習用データセットを単純な古いパスとドキュメントの転送先に限定するためのものです。これはこのアプリケーションのルールであり、KV が課す制限ではありません。

cat > validate-redirects.mjs <<'JS'
import { readFile } from "node:fs/promises";

const filename = process.argv[2] ?? "redirects.json";
const entries = JSON.parse(await readFile(filename, "utf8"));
if (!Array.isArray(entries) || entries.length === 0 || entries.length > 20) {
  throw new Error("Use a non-empty teaching dataset of at most 20 entries.");
}
const seen = new Set();
for (const entry of entries) {
  if (!entry || typeof entry.key !== "string" || !/^route:\/old-[a-z-]+$/.test(entry.key)) {
    throw new Error("Every key must name an old route, such as route:/old-start.");
  }
  if (typeof entry.value !== "string" || !/^\/docs\/[a-z-]+$/.test(entry.value)) {
    throw new Error("Every destination must be a /docs/ path on this site.");
  }
  if (Object.keys(entry).some(key => !["key", "value"].includes(key))) {
    throw new Error("This dataset accepts only key and value fields.");
  }
  if (seen.has(entry.key)) throw new Error(`Duplicate key: ${entry.key}`);
  seen.add(entry.key);
}
console.log(`Validated ${entries.length} unique redirect entries.`);
JS
node validate-redirects.mjs redirects.json

Validated 5 unique redirect entries. と表示されることを確認します。検証に失敗した場合は、インポートする前にファイルを修正してください。重複キーを拒否することが重要です。同じキーを再度書き込むと、その値が置き換えられるためです。

まず、ルート用ではないテスト用レコードをローカルに作成し、その後でカタログをインポートします。このテスト用レコードにより、後のカタログ管理で他のデータが保持されることを確認できます。

npx wrangler kv key put system:owner labex-redirect-demo --binding ROUTES --local
npx wrangler kv bulk put redirects.json --binding ROUTES --local
npx wrangler kv key list --binding ROUTES --local

route: エントリ 5 件と system:owner が表示されることを確認します。bulk put はファイル内のエントリを書き込みますが、namespace 全体を置き換えたり、ファイルにないキーを削除したりはしません。また、すべての場所で同時に確認できる原子的な変更であることも保証しません。

次に、同じ検証済みデータセットを今回の実験のクラウド namespace にインポートします。

npx wrangler kv key put system:owner labex-redirect-demo --binding ROUTES --remote
npx wrangler kv bulk put redirects.json --binding ROUTES --remote
npx wrangler kv key list --binding ROUTES --remote

6 つのキーが表示されることを確認します。明示的な対象指定オプションにより、ローカルでの練習とクラウドへの書き込みを分離できます。エントリを変更する前に、このステップの確認を実行してください。

すべてのページを読み取り、リダイレクトを提供する

このステップでは、すべてのルートキーを一覧表示し、リダイレクトを提供する Worker を作成します。1 回の KV list() 呼び出しでコレクション全体を取得できるとは限りません。cursor は KV が返す継続マーカーです。値を変更せずに返して、次の部分を取得します。

次のハンドラーを作成します。limit: 2 と意図的に小さく設定しているため、5 件のレコードでもページネーションを確認できます。通常の本番コードでは、より大きなページサイズを使用します。この実験ではデータを 20 件に制限しているため、ループを小さく保てます。

cat > src/index.js <<'JS'
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    try {
      if (url.pathname === "/catalog") {
        const names = [];
        let cursor;
        let complete = false;
        let pages = 0;
        do {
          const page = await env.ROUTES.list({ prefix: "route:", limit: 2, cursor });
          names.push(...page.keys.map(key => key.name));
          pages += 1;
          complete = page.list_complete;
          cursor = complete ? undefined : page.cursor;
          if ((!complete && !cursor) || pages > 20) {
            return Response.json({ error: "Catalog could not be completed" }, { status: 503 });
          }
        } while (!complete);
        return Response.json({ keys: names, pages });
      }
      if (url.pathname.startsWith("/docs/")) {
        return new Response(`Example destination: ${url.pathname}`);
      }
      const target = await env.ROUTES.get(`route:${url.pathname}`);
      if (target === null) return new Response("Not found", { status: 404 });
      if (!/^\/docs\/[a-z-]+$/.test(target)) {
        return Response.json({ error: "Invalid redirect destination" }, { status: 500 });
      }
      return Response.redirect(new URL(target, url.origin).href, 302);
    } catch {
      return Response.json({ error: "Redirect storage unavailable" }, { status: 503 });
    }
  }
};
JS

do...while ループは少なくとも 1 ページを取得し、list_complete が true になるまで続行します。すべてのリクエストで prefix: "route:" を指定するため、owner のテスト用レコードがカタログに入ることはありません。names.push(...) は各ページのキー名を結果に追加します。

keys 配列が空でも、一覧取得が完了したとは限りません。削除または有効期限切れのエントリがあると、返されるキーがないページの後に、さらにページが続く場合があります。そのため、このループでは配列の長さではなく list_complete を使います。ページ数の上限と cursor の欠落チェックにより、この小規模なデモで一覧取得を完了できない場合でも、制御されたエラーを返します。詳しくは KV listing and pagination を参照してください。

その他のパスでは、Worker が一致するルートキーを読み取ります。存在しないルートには 404 を返し、対応する転送先には Location ヘッダー付きの 302 レスポンスを返します。実行時にも転送先を検証するため、KV の値が誤って編集されても、訪問者を別のサイトへリダイレクトすることはありません。/docs/ のレスポンスは、転送先のパスを示す簡単なプレースホルダーであり、完全なドキュメントサイトではありません。

npx wrangler dev --local --ip 0.0.0.0 --port 8080 > local.log 2>&1 &
DEV_PID=$!
cat local.log

準備完了のメッセージが表示されたら、カタログ全体を確認します。

curl -i http://127.0.0.1:8080/catalog

5 つのソート済みルートキーと、少なくとも 3 ページが表示されることを確認します。空のページが追加で表示される場合もあります。重要なのは、system:owner が含まれない完全なキー一覧が得られることです。

curl -i http://127.0.0.1:8080/old-start

HTTP 302Location: http://127.0.0.1:8080/docs/start が表示されることを確認します。curl はデフォルトではリダイレクトを追跡せず、リダイレクトレスポンスを表示します。後で比較できるよう、ローカルデータセットは変更しないでください。

指定したルートを更新し、他のデータを保持する

このステップでは、namespace 全体を置き換えずに、クラウド上のカタログを変更します。新しい開始ページは /docs/getting-started です。一方、2 つの一時的なページはリダイレクトしないようにします。

npx wrangler kv key put route:/old-start /docs/getting-started --binding ROUTES --remote

指定したキーへの書き込みでは、他のルートは変更されません。複数のキーを削除する場合、Wrangler は完全なキー名の JSON 配列を受け取ります。削除を実行する前に、この小さな廃止対象リストを読み取ります。

cat > retired-keys.json <<'JSON'
["route:/old-contact", "route:/old-event"]
JSON
cat retired-keys.json
npx wrangler kv bulk delete retired-keys.json --binding ROUTES --remote

確認を求められた場合は、binding と表示された操作が今回の実験用の使い捨て namespace を指していることを確認してください。リストには 2 つのルートキーだけが含まれており、system:owner は含まれていません。

npx wrangler kv key list --binding ROUTES --remote
npx wrangler kv key get system:owner --binding ROUTES --remote --text

残りのルートが 3 つあり、値 labex-redirect-demo が変更されていないことを確認します。元の bulk import はここで再実行しないでください。古い値によって更新内容が元に戻り、廃止したキーも復元されてしまいます。

Worker をデプロイし、ROUTES binding を確認して、実際の公開アドレスをコピーします。

npx wrangler deploy
WORKER_URL="https://YOUR_WORKER.YOUR_SUBDOMAIN.workers.dev"
curl -i "$WORKER_URL/catalog"

まず /catalog が HTTP 200 と期待する JSON キー一覧を返すことを確認します。Cloudflare のエラーページが返された場合は、少し待って読み取り専用のリクエストを繰り返してください。削除したルートの結果が正しいのは、HTTP 404 とアプリの本文 Not found が返る場合です。ステータスコードだけでは判断できません。

カタログに含まれるのが route:/old-pricingroute:/old-startroute:/old-support だけであることを確認します。変更したパスと廃止したパスをテストします。

curl -i "$WORKER_URL/old-start"
curl -i "$WORKER_URL/old-contact"
curl -i "$WORKER_URL/old-event"

開始パスが /docs/getting-started にリダイレクトされ、廃止した 2 つのパスが 404 を返すことを確認します。新しいクラウドデータがまだ表示されない場合は、KV の伝播を待ってから読み取り専用の確認を再実行してください。接続に失敗しただけでは、廃止が成功したことにはなりません。

curl -i http://127.0.0.1:8080/catalog

ローカル開発環境では、元の 5 つのルートが引き続き表示されます。この違いにより、管理コマンドがクラウドストアを対象にしていたことを確認できます。Dashboard で同じアカウントを選択し、Storage & databases → Workers KV を開いて、今回の実験用 namespace を確認します。3 つのルートエントリと保持された owner のテスト用レコードを、コマンド出力と比較してください。この確認は読み取り専用です。生成された namespace 名と ID は、今回の実行に固有のものです。

KV Pairs を選択して以下のレコードを確認します。メンテナンスコマンドの完了前に namespace を開いた場合は、Refresh で更新してください。

残された 3 つのルートと変更されていない所有者レコード

使い捨てのクラウドリソースを削除する

このステップでは、Wrangler が認証された状態のまま、2 つのリソースを削除します。namespace は Worker より長く存続できるため、アプリケーションだけを削除してもデータは消去されません。

このターミナルで起動したローカル開発プロセスを停止します。

kill "$DEV_PID"

削除する前に、保存したリソース参照を確認します。

cat wrangler.jsonc

labex-routes-... という Worker 名と、ROUTES namespace の ID を確認します。この設定で選択されている Worker を削除します。

npx wrangler delete

確認を求められた場合は、表示された名前が今回の実験と一致することを確認し、y で承認します。次に、ROUTES が参照している namespace だけを削除します。

npx wrangler kv namespace delete --binding ROUTES

確認プロンプトが表示されたら、承認する前に namespace を確認してください。独立した確認処理で削除対象のリソースが存在しないことを識別できるよう、wrangler.jsonc はそのまま残します。

npx wrangler kv namespace list

今回の実験で作成した namespace が表示されず、無関係な namespace は残っていることを確認します。Dashboard の一覧を更新し、今回の実験用 Worker と namespace が消えていることも確認してください。リクエストの失敗やログインの期限切れだけでは、削除が成功したことを証明できません。ログアウトする前に、このステップの確認を実行して、認証済みのインベントリを調べられるようにしてください。

VM の認証を終了する

このステップでは、クリーンアップの確認が完了した後に Wrangler との接続を解除します。ログアウトすると、この VM に保存された Wrangler の認証が終了します。ただし、クラウドリソースが削除されたり、通常の Dashboard ブラウザーセッションからログアウトしたりすることはありません。

npx wrangler logout
npx wrangler whoami --json

構造化された結果で "loggedIn": false と表示されることを確認します。認証されていない状態でのこのコマンドは、終了ステータスが 0 以外になる場合がありますが、ここでは想定された動作です。接続エラーだけが表示され、認証状態が明示されない場合は、接続が回復してから再試行してください。

残っているローカルファイルとローカル KV の状態は、この使い捨て VM に属するものです。これらは、すでに削除したクラウドリソースとは別のものです。これで実験を終了できます。

まとめ

小規模なリダイレクトデータセットを一括書き込みする前に検証し、ローカルとクラウドの対象を明示的に分け、プレフィックス付き KV 一覧のすべてのページを取得しました。1 つのルートを変更し、2 つのキーを正確に廃止しながら、無関係な owner レコードを保持しました。デプロイ後のレスポンスで、新しい転送先と廃止したルートが存在しないことを確認し、ローカルカタログには元のデータが残っていることも確認しました。

最後に、使い捨ての Worker と namespace を削除し、ログアウトしました。次は、一時的に以前のバージョンが返される可能性がある設定の読み取りを扱います。