検索 URL の値に & が入って別のパラメーターになったり、A+B が A B として読まれたりします。前者はエンコードする範囲、後者はクエリの読み方の問題です。サイトのツールとブラウザーの URL API で再現してみましょう。
component か full か、まず範囲を決める
JavaScript には URL エンコード関数が2つあります。違いは、予約文字に手を付けるかどうかです。
encodeURIComponent(component モード)は / ? # & = などの予約文字までパーセントエンコードします。値ひとつ、パス断片ひとつを扱うときに使います。encodeURI(full モード)は区切りを保持し、まだパーセントエンコードされていない URL テキストに使います。MDN も2つを用途で区別しています。2つを混同すると、クエリ値の境界が変わることがあります。
クエリ値に予約文字があれば component
再現しやすい例は、パラメータの値の中に & や = が入る場合です。
値として a=1&b=2 を入れるとしましょう。PiPi Worlds の URL エンコードツールで component でエンコードすると a%3D1%26b%3D2 が出ます。= と & がそれぞれ %3D・%26 に変わり、1つの値として安全に収まります。一方 full でエンコードすると a=1&b=2 のままです。これを ?x=a=1&b=2 のように付けると、サーバーは &b=2 を別のパラメータとして読み、データが壊れます。値には component が答えです。
未エンコードの URL テキスト全体なら full
逆の状況は、区切りを作ったものの、日本語などをまだパーセントエンコードしていない URL テキストです。
https://pipi-worlds.com/道?q=こんにちは を full でエンコードすると https://pipi-worlds.com/%E9%81%93?q=%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF になります。://・/・?・= は残り、日本語をパーセントに変えます。component ならスキームの区切りも変換されます。同じ層のURLに encodeURI を繰り返し掛けないでください。 たとえば %20 が %2520 になり、同じURLとして扱うつもりだった値が変わります。一方、URL全体を別のURLのクエリ値に入れる場合は、外側で % を保護する必要があります。次の例のように、渡したい値そのものを searchParams.set に渡します。
URLを別のURLの値に入れるとき
%25 が見えたら、すぐに消すのではなく「何を1つの値として渡すのか」を確認します。 たとえば、次のURL文字列全体を別のURLの next という値に入れる、架空のデータ受け渡しを考えます。説明用の example.com のURLであり、開いたり送信したりする必要はありません。
https://example.com/search?q=A%2BB%20C&lang=ja
この内側のURLでは、q の値は A+B C、lang の値は ja です。%2B は文字の +、%20 は空白を表しています。このURL文字列そのものを外側の next で運ぶなら、次のようにします。WHATWG URL標準の値の直列化に任せ、先に encodeURIComponent は掛けません。
const inner = "https://example.com/search?q=A%2BB%20C&lang=ja";
const outer = new URL("https://example.com/inspect");
outer.searchParams.set("next", inner);
console.log(outer.search);
// ?next=https%3A%2F%2Fexample.com%2Fsearch%3Fq%3DA%252BB%2520C%26lang%3Dja
const restored = new URL(outer.href).searchParams.get("next");
console.log(restored === inner);
// true
console.log(new URL(restored).searchParams.get("q"));
// A+B C
外側で %2B が %252B、%20 が %2520 になるのは、内側の文字列にある % を守るためです。外側を解析して next を取り出すと、元のURL文字列に戻ります。その後で内側のURLを別に解析すると q が A+B C になります。単に同じ値を何度もデコードする手順とは違います。
事前変換を重ねると、何が残る?
比較する失敗例は、set("next", encodeURIComponent(inner)) のように先に変換した文字列を、さらに値として渡す場合です。外側から1回取り出しても https%3A%2F%2F... という変換済み文字列が残り、この例で渡したかった inner と一致しません。
| 外側のsetに渡すもの | 外側からnextを取り出した値 |
|---|---|
inner そのもの | 元のURL文字列と一致 |
encodeURIComponent(inner) | 変換済み文字列が残り、元のURL文字列とは不一致 |
ここでは「元のURL文字列を渡す」という取り決めで比較しています。実際の受信側が別の形式を明示している場合は、その仕様を確認してください。見た目が読めるようになるまで受信側で追加デコードする、という修正は勧めません。
手動で1層だけ戻す
URL エンコードツールで上の試験用URLを入力し、エンコード・Component (値) を選ぶと、次の値になります。これは next= を除いた、外側へ入れる値の部分です。この例では URLSearchParams の値の直列化と同じになりますが、すべての文字で両方式が同じになるという意味ではありません。
https%3A%2F%2Fexample.com%2Fsearch%3Fq%3DA%252BB%2520C%26lang%3Dja
この結果を入力欄に戻してデコード・Component (値) で1回戻すと、上の元のURL文字列になります。「クエリ・フォームの + を空白に」はオフにして比較します。出力にはまだ内側の %2B・%20 が残りますが、この段階では正しい結果です。ツールは入れ子を自動判定せず、next も自動で取り出しません。
2026年10月15日 補足: 以前の「変換済みURLは再エンコードしない」という注意を、同じ層の反復と入れ子の受け渡しに分けました。元の値との一致はこの例の確認方法であり、リンク先の安全性・受信サーバーの処理・署名付きURLの有効性は保証しません。秘密のトークンや実際の顧客URLではなく、上の試験値だけで確認してください。
’+’ が空白になる罠
デコードで人を惑わせるのが + です。同じ a+b が、出所によって違うふうに解釈されます。
+ の意味は「URL かどうか」だけでは決まりません。URL のクエリ文字列を URLSearchParams で読む場合、form-urlencoded 規則で生の + は空白になります。HTML フォームだけの話ではありません。
decodeURIComponent("a+b") // "a+b"
new URLSearchParams("q=a+b").get("q") // "a b"
new URLSearchParams("q=a%2Bb").get("q") // "a+b"
const url = new URL("https://example.com/search");
url.searchParams.set("q", "A+B & C");
url.searchParams.get("q") // "A+B & C"
// 直列化した q の + は %2B、空白は +、& は %26
手作業で q=A+B を組み立て、文字としての + を残したいなら %2B が必要です。より確実なのは URL.searchParams.set にエンコード前の値を渡すことです。本サイトの「+ を空白に」はComponent モードでクエリ・フォームの値を一つだけ手動で戻す場合に使います。Full URL モードはパスを変えないよう、すべての + を保持します。URL 内のクエリは URLSearchParams で読むか、その値だけを Component にコピーしてください。
ツールで確認するときとリンクを作るとき
PiPi Worlds の URL エンコードツール は component/full と + の解釈を比べる手動変換器です。full の encodeURI は &、#、? などの区切りを残すため、新しいクエリ値を安全に組み立てる機能ではありません。 新しいリンクには上記の URL と searchParams.set を使ってください。変換入力はブラウザー内で処理しますが、ページ解析のビコンは別に動く場合があります。値を含む URL を実際に開くと、そのアドレスはリクエストや記録に残り得ます。途中で切れた % はエラーとして表示します。