アフィリンクの「タグ抜け」を公開前に機械検出する — 配信HTMLを見てASINで正規化する

背景

商品リンクからアソシエイトタグが抜けても、リンク自体は普通に開く。
表示も崩れないし、404にもならない。だから壊れていることに気づけない。
気づけるのは「クリックはあるのに成果が0のまま」という、数か月遅れの間接的な形になる。

私が運用している自動更新サイト群では、公開前ゲートにこの検査を入れている。
今日その検査を古い記事群にかけたところ、blog.hashito.biz の記事から17本のタグ抜けリンクが出てきた。
2019〜2021年に手で書いた記事で、Amazonの商品ページからURLをそのままコピーしたものだった。
中には他人のタグ(tag=jpgo-22)が付いたままのURLもあった。検索結果経由でコピーすると混ざる。

この記事では、その検出と修正をどう機械化したかを書く。
題材はAmazonだが、「配信されるHTMLを見て、リンクに必要なパラメータが付いているかを検査する」という形は他でも使える。

なぜ「配信されるHTML」を見るのか

検査対象はソース(Markdownやテンプレート)ではなく、ビルド後に配信されるHTMLにする。
理由は、ソースが正しくても出力が壊れることがあるからだ。

  • テンプレートやプラグインがURLを書き換える(リダイレクタを挟む、rel を付ける、& をエスケープする)
  • 一部のリンクだけコンポーネント経由で、残りは地の文にベタ書き
  • 古い記事の生成物だけが古い形式のまま残る

ソースを見る検査は「書いた内容」を保証するが、収益動線が生きているかどうかを決めるのは読者に届くHTMLのほうだ。

もう1点、href 属性の中だけを見るのも意識的な選択にしている。
地の文に説明としてURL文字列を書いている箇所まで拾うと、直す必要のないものが検出結果に混ざる。
検出結果にノイズが混ざると、そのうち誰も見なくなる。

実際に試す

前提は Node.js 20 以上(String.prototype.matchAllURL を使う。追加の依存はない)。
公開ディレクトリを引数に渡すと、href 属性中の amazon.co.jp リンクを全部拾い、指定タグが付いていないものを報告する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// check-affiliate-tag.mjs
// 使い方: node check-affiliate-tag.mjs <公開ディレクトリ> <アソシエイトタグ>
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join, extname } from 'node:path';

const root = process.argv[2] ?? 'public';
const tag = process.argv[3] ?? 'your-tag-22';

function* htmlFiles(dir) {
for (const name of readdirSync(dir)) {
const p = join(dir, name);
if (statSync(p).isDirectory()) yield* htmlFiles(p);
else if (extname(p) === '.html') yield p;
}
}

// href="..." の中身だけを見る(地の文のURL文字列は対象外にする)
const HREF = /href=["']([^"']*amazon\.co\.jp[^"']*)["']/gi;

const bad = [];
let total = 0;

for (const file of htmlFiles(root)) {
const html = readFileSync(file, 'utf8');
for (const m of html.matchAll(HREF)) {
total++;
const url = m[1].replace(/&amp;/g, '&'); // HTML属性のエスケープを戻す
const params = new URL(url, 'https://www.amazon.co.jp').searchParams;
if (params.get('tag') !== tag) bad.push({ file, url, tag: params.get('tag') });
}
}

console.log(`amazon.co.jp リンク ${total}本 / タグ不備 ${bad.length}本`);
for (const b of bad.slice(0, 10)) {
console.log(` ${b.tag === null ? 'タグ無し' : `他人のタグ(${b.tag})`}: ${b.url.slice(0, 70)}`);
}
process.exit(bad.length === 0 ? 0 : 1);

実行すると次のように出る(このブログの修正前の状態で実測した出力)。

1
2
3
4
5
$ node check-affiliate-tag.mjs public post03f-22
amazon.co.jp リンク 17本 / タグ不備 17
タグ無し: https://www.amazon.co.jp/FC-%E5%B7%A5%E5%A0%B4-R3%E9%96%8B%E7%99%BA%E3
タグ無し: https://www.amazon.co.jp/gp/product/B013QV28CW/ref=ppx_yo_dt_b_asin_ti
タグ無し: https://www.amazon.co.jp/gp/product/B00P9BVKOK/ref=ppx_yo_dt_b_asin_ti

&amp; を戻してから URL に渡しているのがポイントで、これを飛ばすと
?ie=UTF8&amp;tag=... のようなHTML属性表記でパラメータ名が amp;tag になり、
タグが付いているのに「タグ無し」と判定される。
検査の誤検知は、放置よりたちが悪い(本物の違反まで無視されるようになる)。

process.exit で終了コードを返しているので、そのままデプロイ前ゲートに置ける。

1
npm run build && node check-affiliate-tag.mjs public post03f-22 && firebase deploy --only hosting

見つけたものをASINで正規化する

検出できたら次は修正だが、商品ページからコピーしたURLは検索経由のパラメータで長い。

1
https://www.amazon.co.jp/gp/product/B013QV28CW/ref=ppx_yo_dt_b_asin_title_o08_s00?ie=UTF8&psc=1

この ref= 以降はコピー元のセッションに紐づく履歴で、他人が踏んでも意味がない。
必要なのは ASIN(10文字の商品ID) だけなので、そこだけ抜いて短い正規形に直す。
/dp/<ASIN>/gp/product/<ASIN> の両方が同じ商品を指すので、どちらの形も同じ扱いにする。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// canonicalize-amazon.mjs
import { readFileSync, writeFileSync } from 'node:fs';

const TAG = 'your-tag-22';
const AMAZON = /https:\/\/www\.amazon\.co\.jp\/[^\s"')\]<>]*/g;
const ASIN = /\/(?:dp|gp\/product)\/([A-Z0-9]{10})/;

export function canonicalize(text) {
return text.replace(AMAZON, (url) => {
const m = ASIN.exec(url);
if (m) return `https://www.amazon.co.jp/dp/${m[1]}/?tag=${TAG}`;
if (url.includes(`tag=${TAG}`)) return url;
return url + (url.includes('?') ? '&' : '?') + `tag=${TAG}`;
});
}

for (const file of process.argv.slice(2)) {
const before = readFileSync(file, 'utf8');
const after = canonicalize(before);
if (before !== after) { writeFileSync(file, after); console.log('rewrote', file); }
}

動作を確かめる(node --test で通る最小の確認)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// canonicalize-amazon.test.mjs
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { canonicalize } from './canonicalize-amazon.mjs';

test('gp/product 形式をASINの正規形に直す', () => {
const src = 'https://www.amazon.co.jp/gp/product/B013QV28CW/ref=ppx_yo?ie=UTF8&psc=1';
assert.equal(canonicalize(src), 'https://www.amazon.co.jp/dp/B013QV28CW/?tag=your-tag-22');
});

test('他人のタグ付きURLも自分のタグの正規形になる', () => {
const src = 'https://www.amazon.co.jp/dp/4779305918/ref=asc_df?tag=jpgo-22&psc=1';
assert.equal(canonicalize(src), 'https://www.amazon.co.jp/dp/4779305918/?tag=your-tag-22');
});

test('ASINが無い検索URLにはタグを足すだけ', () => {
const src = 'https://www.amazon.co.jp/s?k=arduino';
assert.equal(canonicalize(src), 'https://www.amazon.co.jp/s?k=arduino&tag=your-tag-22');
});
1
2
3
4
5
6
7
$ node --test
ok 1 - gp/product 形式をASINの正規形に直す
ok 2 - 他人のタグ付きURLも自分のタグの正規形になる
ok 3 - ASINが無い検索URLにはタグを足すだけ
# tests 3
# pass 3
# fail 0

ASINを持たない検索URL(/s?k=...)にはパラメータを足すだけにしている。
検索結果へのリンクを商品ページに勝手に化けさせると、記事の文脈と合わなくなるためだ。
「直せるものだけ直し、判断が要るものは形を変えない」を境界にしておくと、一括置換を安心して回せる。

なお正規形の ?tag= は消えやすい。リンクを貼り直すたびに落ちるので、
検査を残しておくことのほうが、一度きれいにすることより効く。

運用で分かったこと

  • 修正は10ファイル・17リンクで済んだが、気づくまでに数年かかっていた。壊れても何も起きない不具合はこうなる
  • 検出を「表示の壊れ」ではなく「終了コード」にすると、人が見ていない時間帯の自動デプロイでも止まる
  • ソースではなく配信物を見る検査は、ビルドやテーマの変更にも巻き込まれずに済む

同じ考え方で商品リンクを機械生成しているサイトの実装は
ガジェットレビュー(gadget.autoarticles.net)にまとめてある。
こちらは記事生成の時点でリンクを組み立てるので、そもそもタグが抜けない形にしてある。
既存の手書き記事が多い場合は、この記事の検査を先に入れるほうが早い。


アフィリンクの「タグ抜け」を公開前に機械検出する — 配信HTMLを見てASINで正規化する
https://blog.hashito.biz/2026/07/31/affiliate-tag-missing-detect-canonicalize-asin/
著者
hashito
作成日
2026年7月31日
著作権