API キーをサーバーサイドで保持する Next.js 製の Web 逆リンクチェッカー
CORS がオープンになっているため、ブラウザから API に直接アクセスできます。有料キーを使ってこれを行うのは間違いです。このページでは、その問題を解決する軽量なサーバーレイヤーを示します。

神話と現実
| 神話 | 現実 |
|---|---|
| 「CORS がオープンなら、React バンドルにキーを含めても大丈夫だ。」 | CORS がオープンなら呼び出しは成功しますが、誰でもネットワークタブからキーを読み取って残高を使い果たすことができます。 |
| 「ダッシュボードなので、リンクのテーブルはクライアント側でレンダリングする必要がある。」 | サーバー側でフェッチして HTML を送信します。ユーザーは最初の描画で行を表示し、キーはサーバーから一切流出しません。 |
| 「無料のアクションも保護する必要がある。」 | domain、overview、stats、suggest はキーを必要としないため、ブラウザから直接呼び出すことができます。 |
| 「ページビューのたびに再フェッチすべきだ。」 | インデックスは約月に一度再構築されます。24時間のキャッシュでは何も失われません。 |
| 「プロキシはクライアントが送信するものをすべて転送すべきだ。」 | 使用するアクションをホワイトリストに登録してください。転送された action=search は 3 クレジットかかります。 |
ルートハンドラー
App Router、ファイルは app/api/links/route.ts。環境変数には NEXT_PUBLIC_ プレフィックスが付いていないため、クライアントバンドルには含まれません。
import { NextRequest, NextResponse } from "next/server";
const ALLOWED = new Set(["refdomains", "anchors", "toppages"]);
export async function GET(req: NextRequest) {
const p = req.nextUrl.searchParams;
const action = p.get("action") ?? "";
const q = (p.get("q") ?? "").toLowerCase();
if (!ALLOWED.has(action) || !/^[a-z0-9.-]+$/.test(q)) {
return NextResponse.json({ ok: false, error: "bad request" }, { status: 400 });
}
const url = `https://seomasters.dev/api.php?action=${action}&q=${q}&per=100`;
const upstream = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.SEO_API_KEY}` },
next: { revalidate: 86400 },
});
if (upstream.status === 402) {
return NextResponse.json({ ok: false, error: "data temporarily unavailable" }, { status: 503 });
}
return new NextResponse(upstream.body, { status: upstream.status, headers: { "content-type": "application/json" } });
}ハンドラーがこのような見た目である理由
アクションのホワイトリスト
UI のバグが偶然に 3 クレジットかかる search 呼び出しを引き起こさないようにします。
q に対する正規表現
ホスト名のみが通過します。スキーム、パス、クエリ文字列がアップストリームに smuggle されることはありません。
revalidate: 86400
ドメインごとに 1 日 1 回のアップストリーム呼び出し。月次データであれば、より長い期間でも問題ありません。
402 を 503 にマッピング
訪問者に残高が空であることを知らせる必要はありません。あなたは知る必要がありますので、ログに記録してください。

レンダリング:クローラーと顧客が両方とも目にするもの
このダッシュボードがログインの背後にある場合、サーバーレンダリングかクライアントレンダリングかは UX の選択です。無料の検索ページなど、インデックス化したい公開ページがある場合は、最初のテーブルをサーバー側でレンダリングしてください。
ハイドレーション後に埋まる空のシェルとして届くページは、インデックス化と低速な端末の両方にとってリスクとなります。Google は JavaScript を実行できますが、そのステップをキューに入れ、必ずしも迅速に処理できるとは限りません。
無料の概要をサーバー側に保持する
安価で無料の overview 呼び出しをサーバーコンポーネントに配置し、訪問者がボタンをクリックしたときにのみ有料の詳細な取得を行うようにします。この分離により、クレジットの使用量を予測可能に保つことができます。
無料の呼び出しにはコストがかからないため、サーバーレンダリングされた概要ではクレジット消費が増加しません。ボタンとセッションが必要なのは、有料の取得のみです。
空の状態をまず考えましょう。空白のテーブルにアクセスした訪問者はツールが壊れていると判断し、高速なセカンドペイントでもその第一印象は覆せません。
最初の画面には無料アクションを、ドリルダウンには有料アクションを選択します。ページを開いて無料のサマリーを見た訪問者は、誤って有料ボタンを押す理由がありません。
数字だけでなく説明にもリンクする
数字の接続は簡単ですが、それらの説明は難しいため、ツールチップから DR 計算式 にリンクし、インデックスが月次で更新されることに言及してください。
計算式なしでスコアだけを見た訪問者は推測するでしょう。そのため、リンクは画面上の数字の隣に配置すべきです。

ダッシュボードのサーバー側とクライアント側の部分
ほとんどのダッシュボードには両方の種類のコードが必要です。所有権が分離を決定します。キー、データ、アクセスルールにはそれぞれ単一のホームが必要です。公開されている Web 逆リンクチェッカーページも、ログインの背後にあるページと同じ分離が必要です。
サーバー側でフェッチ、クライアント側でソート
バックリンクテーブルは、ユーザーがソート、フィルタリング、ページネーションを行う場合に適切なクライアントコンポーネントです。行はすでにルートにアクセスしたサーバーコンポーネントから来るべきで、ブラウザがキーやアップストリーム URL を保持しないようにします。
10 行のソートはクライアント側で瞬時に行えます。毎回新しいリクエストで 5000 行をソートするのは遅いため、すでに読み込んだコンポーネントに完全なセットを保持してください。
シンプルに保ちましょう。クリックのたびに再フェッチするテーブルよりも、瞬時にソートできるテーブルの方が優れています。
クライアントの境界を低く保つ
境界を木構造の可能な限り下まで押し下げます。動的にインポートされたクライアント専用のウィジェットはブラウザでレンダリングされ、行をフェッチしたコンポーネントはサーバーに残ります。行をプロップスとして渡し、フェッチを境界の上に保持してください。
フェッチを境界の下に移動すると、ブラウザが自身で呼び出しを行います。これはキー漏洩セクションで警告されている間違いです。この境界を変更するたびにネットワークタブを確認してください。
線がどこに引かれるか正確に考えましょう。まず紙に描き、フェッチ用のボックスとインタラクティブなコントロールごとのボックスを描き、それを横切る矢印の数を数えてください。
シェルとセッションチェックはサーバー側で
レイアウトとセッションチェックは訪問者が何を見るかを決めるため、サーバー側にあるべきです。インタラクティブなテーブル、フォーム、チャートはその後ハイドレートできますが、その決定は変わりません。
クライアント側のガードはボタンを非表示にできますが、リクエストを停止することはできません。アクセスが実際に適用されるのは、ルートハンドラーとサーバーレイアウトです。
その後、Cookie なしで同じリクエストを送信し、サーバーがページだけでなくリクエスト自体を拒否することを確認します。
同じルールがプロキシ自体にも適用されます。ブラウザでのみ行われるセッションチェックはルートを保護できないため、クレジットを使用する前にルートもセッションをチェックします。
訪問者ごとではなくドメインごとのキャッシュ
ルートはドメインからアップストリーム URL を構築するため、キャッシュキーもドメインに従います。ある訪問者がサイトを検索すると、同じサイトについて尋ねる次の訪問者のためにページが暖められます。
これがハンドラーでの日次再検証の目的です。同じドメインの 2 人の訪問者で 1 回のアップストリーム呼び出しで済み、1000 人の訪問者が 1000 のドメインでアクセスしても 1000 回の呼び出しで済みます。キャッシュが予算を管理します。
逆のケースにも注意してください。タイムスタンプによってクエリ文字列が変化するページはキャッシュを完全に無効にし、請求書はログよりも先にそれを示します。
インデックスに一致するスケジュールで再検証します。日次のウィンドウは月次データに適しており、より短いウィンドウは新しい事実を追加せずに呼び出し数だけ増やします。
ハードニング、実施する順序
ハンドラーは簡単な部分です。ハードニングはキーが漏洩したり残高が枯渇したりする場所なので、これらを順序立てて作業してください。
自分のネットワークタブをスキャン
自分のダッシュボードで Chrome DevTools を開き、ネットワークタブを読んでください。注意深く見てください。そこに sk_live_ が表示されるリクエストがある場合は、今日キーをローテーションしてください。このチェックは数分で完了し、最も一般的な漏洩を見つけます。
リクエストヘッダーだけでなくレスポンスボディも検索してください。エラーメッセージにキーがエコーされると、それも漏洩になります。
ユーザーごとの呼び出し数を制限
次に、ユーザーごとの制限を追加します。ルートの前に配置します。前述のハンドラーはアクセスできる人のためにクレジットを使用するため、セッションに紐付け、時間あたりの呼び出し数を制限してください。クレジットページの予算計算から制限値を選択します。
最初は寛大にして、後で厳しくします。1 日目に実際のユーザーをブロックする制限は、彼らに別のツールを探すことを教えることになります。
誰がチャージインするかを明記
ランブックに追記する価値のある行もあります:残高が設定レベルを下回ったときに誰がページングされるか、そして誰がチャージインするかです。人物を特定してください。
チャージインの手順をアカウントページのリンクと予想される処理時間とともにチェックリストとして記録してください。1 人の人の頭の中にあるだけのランブックは、その人が休暇を取った最初の瞬間に失敗します。手順自体は数分で完了するため、文書化の方が大きな仕事です。
残高が枯渇する前にログに記録
次に、各アップストリームレスポンスからの _credits を独自のメトリクスにログに記録します。日次サンプリングされた残高のグラフは、HTTP 402 が発生する 1 週間前に警告します。
アラートレベルは自分の週次使用量から選択してください。テンプレートから選ばれた数字は、実際の呼び出し数と一致しないことがほとんどです。
ログだけでなくメモリにも最後の値を保持し、残高がアラートレベルを下回った場合にルートが有料の取得を拒否できるようにします。このチェックは 1 行で、トラフィックのバーストが残りを使用するのを防ぎます。
ホワイトリストを 1 か所に保持
もう 1 つの習慣が報われます。ホワイトリストを共有定数として保持し、ハンドラーとテストの両方でインポートします。これにより、誰かが目に見えるリストを編集し、レビュアーがそれに気づくまで、新しいアクションがプロダクションに到達できなくなります。
1 ファイル、1 インポート。プロキシが受け付けるリストを知るために、3 つのコンポーネントでリストを探す必要はありません。
デプロイするランタイムをテスト
最後に、Node 20 または選択したエッジランタイムの下でハンドラーを実行し、両方をテストします。コードが行うようにアップストリームボディをストリーミングすることはそれぞれで機能しますが、ヘッダーの処理は異なり、テストする価値があります。
無効なアクションと有効なアクションの両方でルートを呼び出し、ステータスコードをアサートするテストを 1 つ追加します。これらの 2 つのテストは、ホワイトリストとヘッダー処理のほとんどの回帰をキャッチします。
プロキシの接続順序
- 1
サーバーにキーを追加
キーをパブリックプレフィックスのない名前でサーバー環境に保存し、開発サーバーを再起動して読み込みます。
- 2
まずホワイトリストを書く
画面が実際に呼び出すアクションをリストアップします。それ以外は、アップストリーム API に到達する前に、自分のルートから 400 が返されます。
- 3
サーバーコンポーネントからルートをフェッチ
データが到着する場所で行をレンダリングします。ブラウザは自分のドメインにのみアクセスするため、アップストリームアドレスはページに含まれません。
- 4
残高を監視
各アップストリームレスポンスで
_creditsをログに記録し、402 が訪問者に 503 として届く前にアクション可能なアラートレベルを設定します。