お疲れ様です!IT業界で働くアライグマです!
個人開発している技術ニュースキュレーションサービス「DevPick」では、ユーザーが好みの媒体やキーワードで記事を追えるよう、RSSフィードの配信機能を提供しています。
RSSリーダーからどれくらい記事が読まれているかを把握するため、配信する各記事のリンクに流入計測用のパラメータを付けていたのですが、この安易な付与が思わぬ落とし穴を生み出しかけていました。今回は、パラメータ付きURLが引き起こす問題と、Cookieとリダイレクトを組み合わせて解決した経緯をまとめます。
RSSリーダーからの流入を計測したかっただけなのに
DevPickでは、新着ニュースやトピックごとのフィードをRSS形式で配信しています。RSSリーダーを使っている開発者が手元のリーダーから記事を見つけて、サービス内の個別記事ページを開いてくれる導線です。
サイトを運営していると、「どれくらいの人がRSS経由で訪れてくれているのか」をGoogle Analytics(GA4)などの計測ツールで把握したくなります。そこで最初に選んだのが、最も手軽で一般的な手法でした。
フィードのXMLを出力する際、記事のURL末尾に ?utm_source=rss を付与したのです。
<!-- フィード内の記事リンク -->
<item>
<title>注目の技術ニュース</title>
<link>https://example.com/articles/123?utm_source=rss</link>
</item>
このパラメータがあれば、訪問時にGA4が自動でキャンペーンソースを rss として集約してくれます。実装自体もリンク生成処理にクエリ文字列を少し足すだけで完了し、管理画面でも意図通りに数字が分かれて見えていたため、何一つ問題ないように思えました。
IT女子 アラ美utm_sourceを残したURLが引き起こす2つの副作用
運用を続けていく中で、この「URLにパラメータを残したままページを表示させる」仕組みには、無視できない2つの副作用があることに気づきました。
共有・再訪問による流入元の誤計上(アトリビューション汚染)
ユーザーがRSSリーダーから記事を開いたあと、その記事をSNSでシェアしたり、チャットツールやメモに貼り付けたりすることがあります。
ブラウザのアドレスバーに ?utm_source=rss が残ったままだと、ユーザーは当然そのURLをそのままコピーします。その結果、SNSからリンクを踏んでやってきた別の読者まで、GA4上ではすべて「RSSからの流入」としてカウントされてしまうのです。
また、ユーザーがそのページをブックマークして後日直接アクセスした場合も、URLにパラメータが含まれているため、直接流入ではなくRSS流入として記録され続けます。
以前、配信停止リンクのトークンがアナリティクスに漏れていた話 でもURLクエリの扱いに悩まされましたが、流入経路の計測パラメータもまた、ブラウザのURLに残すことでデータの信憑性をじわじわと損なっていきます。
検索エンジンのクロールとURLの分散
もう一つの問題はSEOとURLの正規化です。
RSSフィードは検索エンジンのボットも巡回します。フィード内のURLに ?utm_source=rss が付いていると、ボットはそのパラメータ付きURLをクロール対象として認識します。
もちろん各ページには <link rel="canonical"> を指定して正規URLを伝えていますが、外部リンクや被リンクがパラメータ付きのまま蓄積されると、検索エンジンの評価が分散したり、インデックス登録に余計な揺らぎを生む要因になりかねません。
「RSSリーダーからの流入である」という情報は、初回アクセス時の計測にさえ使えれば十分です。ブラウザのアドレスバーに居座り続ける必要はまったくありませんでした。



クエリパラメータをやめ、中継エンドポイントと短命Cookieへ切り替える
求めていたのは、次の3つの条件をすべて満たす設計でした。
- RSSリーダーからクリックされた事実は、確実にアナリティクスへ送ること
- ブラウザのアドレスバーには、最初からクリーンな正規URL(
/articles/123)を表示させること - 中継処理が検索エンジンにインデックスされたり、キャッシュされたりしないこと
これを実現するために、「専用の中継エンドポイント」と「短命なセッションCookie」を組み合わせた仕組みを導入しました。
中継エンドポイントでCookieをセットして302リダイレクト
まず、RSSフィードに書き出すリンク先を、直接の記事URLではなく専用の中継用URL(/rss/articles/[id])へ切り替えました。
このエンドポイントは記事の画面を描画せず、Cookieを1つセットして即座に正規の記事URLへ302リダイレクトを返します。
// /rss/articles/[id]/route.ts の中継処理例
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
if (!/^[1-9]\d*$/.test(id)) {
return new Response(null, { status: 404 });
}
const target = new URL(`/articles/${id}`, request.url);
const response = NextResponse.redirect(target, { status: 302 });
// 検索エンジンのインデックスやキャッシュを防ぐ
response.headers.set("Cache-Control", "no-store");
response.headers.set("X-Robots-Tag", "noindex");
// 対象記事専用の短命Cookie(有効期限60秒)をセット
response.cookies.set("rss_landing", id, {
httpOnly: true,
secure: request.nextUrl.protocol === "https:",
sameSite: "lax",
path: "/articles",
maxAge: 60,
});
return response;
}
この中継レスポンスには X-Robots-Tag: noindex と Cache-Control: no-store を付与し、ボットに登録されたりブラウザに中間状態を保持されたりしないように保護します。
着地先でCookieを検知して計測し、その場で消去する
リダイレクト先である /articles/[id] の処理では、リクエストに含まれる rss_landing Cookieを確認します。
Cookieの値が開こうとしている記事IDと一致していれば、「今まさにRSSからリダイレクトされてきた初回アクセス」と判定できます。判定が済んだら、レスポンスヘッダーでCookieを即座に破棄(有効期限0秒)します。
// プロキシ・ミドルウェア側でのCookie判定と即時破棄
const isRssLanding = articleMatch !== null &&
request.cookies.get("rss_landing")?.value === articleMatch[1];
if (isRssLanding) {
// サーバー側の描画処理へ目印ヘッダーを渡す
requestHeaders.set("x-rss-landing", "1");
}
const response = NextResponse.next({ request: { headers: requestHeaders } });
// 着地と同時にCookieを削除(再読み込みや別タブへ引き継がない)
if (isRssLanding) {
response.cookies.set("rss_landing", "", {
httpOnly: true,
secure: request.nextUrl.protocol === "https:",
sameSite: "lax",
path: "/articles",
maxAge: 0,
});
}
フロントエンドの計測スクリプトでは、この目印があるときだけGA4の設定に campaign_source: 'rss' を渡します。
ブラウザのアドレスバーには最初から https://example.com/articles/123 という純粋な正規URLしか表示されません。ユーザーがそのままURLをコピーしてSNSに投稿しても、パラメータのないクリーンなURLが共有されるため、二次拡散で計測が汚染される心配がなくなりました。



古いRSSリーダーのリンクも308リダイレクトで正規化する
新しい中継エンドポイントの仕組みを作ったことで、これから配信されるフィードはすべてクリーンなURLで着地するようになりました。
しかし、これだけで完了とは言えません。RSSリーダーの多くは、取得済みのフィードデータをローカルやクラウド側に長期間キャッシュしているからです。
ユーザーが昨日や先週に受信したフィードの一覧から記事を開いた場合、そこに含まれているのは過去に配信した ?utm_source=rss 付きのURLです。これを放置すると、せっかく導入した仕組みをすり抜けて、依然としてパラメータ付きURLが画面に表示されてしまいます。
そこで、Webサーバーのミドルウェア(リバースプロキシ)の先頭に、過去のURLを正規化するリダイレクト処理を設置しました。
// ミドルウェアでの古いutm_source付きリンクの救済処理
if (/^\/articles\/[1-9]\d*$/.test(request.nextUrl.pathname) &&
request.nextUrl.searchParams.get("utm_source") === "rss") {
const canonical = request.nextUrl.clone();
canonical.searchParams.delete("utm_source");
// クエリが空になったら末尾の「?」もきれいに除去する
if ([...canonical.searchParams.keys()].length === 0) {
canonical.search = "";
}
// 検索エンジンにも正規化を伝えるため308で恒久転送
return NextResponse.redirect(canonical, { status: 308 });
}
記事URLにアクセスされた際、utm_source=rss が付いていれば、そのパラメータだけを削り落として 308 Permanent Redirect で即座に転送します。
ステータスコードに 308 を選んだのは、ブラウザに対してだけでなく、検索エンジンのクローラーに対しても「このパラメータ付きURLは正規URLと同一であり、恒久的に移行した」と明確に伝えるためです。
これで、手元のリーダーに過去のリンクが残っている読者がアクセスしてきた場合でも、画面が表示されたときには自動的に正規URLへ置き換わるようになりました。



まとめ
外部サービスやフィードからのアクセスを分析する際、URLに直接クエリパラメータを付与するのは最も手軽なアプローチです。
しかし、その手軽さと引き換えに、「URLがSNSやチャットでそのまま共有される」「検索エンジンのインデックスや評価が分散する」といった思わぬ副作用を引き起こすリスクを抱えることになります。
今回の対応で意識したポイントは次の3点です。
- 計測と表示の分離:流入経路の特定は専用の中継エンドポイントとCookieで行い、画面に表示されるURLには計測用パラメータを持ち込まない
- 状態の即時破棄:判定用のCookieは着地した瞬間に消去し、リロードや別タブでの回遊に誤って引き継がれないようにする
- 過去のキャッシュへの配慮:RSSリーダーなどに残った古いリンクも、ミドルウェアの308リダイレクトで正規URLへ自動誘導する
URLは単なる画面のアドレスではなく、読者がコピーして誰かに伝えるための共有手段であり、検索エンジンにとっても重要なインデックスの基準です。計測の都合でURLを汚してしまわないよう、今後もクリーンなURL設計を意識していきたいと思います。












