0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

URL の後ろに ?utm_source= を足したら # の中に入った。パラメータの追加は文字列連結ではなく new URL で

0
Posted at

背景

チラシに載せる QR コードから、どれくらい人が来たのかを知りたいことがあります。
そのために、URL に UTM パラメータを付けて QR コードにするツールを作りました。

UTM パラメータは、utm_source=flyer のように URL のクエリに付ける目印だ。
アクセス解析ツール(Google アナリティクスなど)が読み取って、流入元ごとに集計します。

付けるだけなので url + '?utm_source=flyer' で良さそうに見えます…
ところが試してみると、URL の形によっては静かに壊れます。

結論

パラメータの追加は文字列連結でやらず、new URL() と searchParams.set() に任せます。
# の位置も ? と & の使い分けも、同名パラメータの置き換えも面倒を見てくれます。

ただし searchParams を触ると、クエリ全体が書き直されます。
もともと %20 だった空白が + に変わるので、そこだけは知っておく必要があります。

文字列連結で何が起きるか

URL は、? の後ろがクエリ、# の後ろがフラグメントという順番で決まっています。
フラグメントは、ページ内の位置などを指す部分で、サーバーには送られない。
この順番は RFC 3986 という URL の仕様で決まっています。
https://www.rfc-editor.org/rfc/rfc3986

下記は、ページ内リンクの付いた URL に文字列で足した例です。
raw が元の URL で、Node.js v24.11.1 で実行しました。

const raw = 'https://example.com/lp?a=1#price';

console.log(raw + '?utm_source=flyer');
console.log(raw + '&utm_source=flyer');

const u = new URL(raw + '&utm_source=flyer');
console.log(u.search, u.hash);

出力は下記でした。

https://example.com/lp?a=1#price?utm_source=flyer
https://example.com/lp?a=1#price&utm_source=flyer
?a=1 #price&utm_source=flyer

3行目を見ると、クエリは ?a=1 のままです。
utm_source は # の後ろに入っていて、クエリとしては読めません。
ページは普通に開くので、エラーにならないのが厄介です。
QR コードは印刷してしまうと直せないので、気づいたときには手遅れな気がします…

new URL に任せる

下記は、URL の文字列とパラメータのオブジェクトを受け取って、付け足した URL を返す関数です。
searchParams は、クエリを読み書きするための URL のプロパティだ。

function addParams(raw, params) {
  const u = new URL(raw);
  for (const [k, v] of Object.entries(params)) {
    u.searchParams.set(k, v);
  }
  return u.toString();
}

console.log(addParams('https://example.com/lp?a=1#price', { utm_source: 'flyer', utm_medium: 'qr' }));
console.log(addParams('https://example.com/lp?utm_source=old', { utm_source: 'flyer' }));
console.log(addParams('https://example.com/#/lp', { utm_source: 'flyer' }));

出力は下記でした。

https://example.com/lp?a=1&utm_source=flyer&utm_medium=qr#price
https://example.com/lp?utm_source=flyer
https://example.com/?utm_source=flyer#/lp

1行目は #price の前に入り、? と & も正しく使い分けられています。
2行目は utm_source が2つにならず、set で値が置き換わっています。
3行目は #/lp のような、フラグメントで画面を切り替える形の URL です。
こちらもクエリは # の前に入ります。

副作用: クエリ全体が書き直される

便利な反面、知らないと驚く点が1つありました。
searchParams.set() を呼ぶと、触っていない既存のパラメータも書き直されます。
下記は、上の addParams をそのまま使った例です。

console.log(addParams('https://example.com/lp?q=a%20b', { utm_source: 'flyer' }));
console.log(addParams('https://example.com/lp', { utm_campaign: '秋 セール' }));

出力は下記でした。

https://example.com/lp?q=a+b&utm_source=flyer
https://example.com/lp?utm_campaign=%E7%A7%8B+%E3%82%BB%E3%83%BC%E3%83%AB

q=a%20b が q=a+b に変わりました。
URLSearchParams は、クエリの形式として空白を + で書く決まりになっているからだ。
仕様は WHATWG の URL Standard にあります。
https://url.spec.whatwg.org/#urlencoded-serializing

受け取る側が URLSearchParams なら、+ は空白に戻ります。
ところが decodeURIComponent で戻している場合は、+ が残ります。
下記はその比較です。

console.log(new URLSearchParams('q=a+b').get('q')); // a b
console.log(decodeURIComponent('a+b'));             // a+b

自前のパーサで受けているページに付けるなら、QR コードにする前に一度開いて確かめたほうが良さそうです。
もう1つの手として、utm_campaign の値を英数字とハイフンだけにしておくと、この問題自体を避けられます。
日本語を入れると URL も長くなり、QR コードの模様も細かくなるので、英数字に寄せておくのが無難だと思います。

ツールでの使い方

ツール側もこの方法で組み立てています。
入力欄の値を読んで、値があれば set、空なら delete するだけです。
KEYS は source や medium などの名前の配列で、u は new URL() の結果です。

KEYS.forEach(function (k) {
  var v = $('p_' + k).value.trim();
  if (v) { u.searchParams.set('utm_' + k, v); }
  else { u.searchParams.delete('utm_' + k); }
});

文字列を組み立てるコードが無いので、# のことを考えなくて済みます。

作ったツールはここに置いています。
https://hashitosystem.com/tools/utmqr/

URL にパラメータを足すときは、文字列連結をやめて new URL() を通す。
印刷物に載せる URL なら、できあがった URL の search にパラメータが入っているかを必ず確かめてください。


本記事はAI補助で執筆した、個人開発の紹介記事です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?