記事本文にアフィリンクを焼き込まず、ビルド時にremarkで自動注入する設計にした話

  • #Astro
  • #remark
  • #アフィリエイト
  • #ビルドパイプライン
  • #自動化
記事本文にアフィリンクを焼き込まず、ビルド時にremarkで自動注入する設計にした話

この記事について

版元が運用する旅行系媒体の一つ(以下 TB)では、記事本文に Amazon・楽天のアフィリエイト CTA を直接書き込む運用をしていた。この方式は、リンク切れやアソシエイトタグの変更が起きるたびに、該当する記事を1本ずつ開いて書き直す作業を発生させる。これを避けるため、本文には一切触れず、Astro のビルドパイプライン側(remark プラグイン)で CTA を自動注入する設計に切り替えた記録。

本文直書きの何が問題だったか

同じ商品リンクが何十本もの記事に埋め込まれている状態で、リンク切れが1件発生したりアソシエイトタグを変更したりすると、影響範囲は「その商品を紹介したすべての記事」になる。記事数が増えるほど修正コストが線形に増えていき、直し漏れも起きやすくなる。

本文と CTA が同じファイルに同居している限り、この構造は変えられない。そこで「本文には商品情報を書かず、ビルド時に外から差し込む」方向に設計を変えた。

設計:remarkプラグインでビルド時に注入する

Astro の markdown 処理パイプラインに乗る remark プラグインとして実装した。

export default function remarkAffiliateInject() {
  return (tree, file) => {
    const fm = file?.data?.astro?.frontmatter ?? {};
    if (fm.draft) return; // 下書き記事には注入しない

    const products = resolveProducts(fm);
    if (!products) return; // 対象商品が無ければ何もしない

    const html = products.map((p) => buildAffiliateCard(p, slug)).join('\n');
    // 冪等ガード: 既に注入済みなら二重に差し込まない
    if (alreadyInjected(tree)) return;

    insertAtBestPosition(tree, html);
  };
}

ポイントは3つある。

  • draft 記事や有効な商品リンクが無い記事には注入しない(安全側のデフォルト)
  • 冪等: ビルドを何度実行しても同じ場所に1回だけ挿入される。マーカー属性を目印にして、既に注入済みのノードがあれば何もしない
  • 注入位置は優先順位で決める: <!--more--> マーカー直後(記事の続きに入る手前)→ 最初の <h2> 直後 → 見つからなければ本文末尾、の順にフォールバックする

商品の解決は frontmatter 優先・カテゴリ既定へのフォールバックという2段構成にした。個別記事で affiliate: を明示していればそれを使い、指定が無ければカテゴリごとに登録した既定商品を出す。ただし「無関係な記事にまで広告が付く」事故を避けるため、既定は明示的に登録したカテゴリだけに限定した(オプトイン方式)。

カード生成側の設計判断

カード自体を作る関数側にも、いくつかの実務上の判断が入っている。

  • Amazon・楽天のボタンはそれぞれ独立して出し分ける。片方しか有効なリンクが無ければもう片方は描画しない
  • rel="nofollow sponsored" を必ず付け、カード内に「PR」表記を明示する(景品表示法・ステルスマーケティング規制対応)
  • クリックは共通の遷移用パスを経由させ、どの記事のどのボタンから来たクリックかをクエリパラメータで残す。集計時に「この記事のこのCTAが効いている」を追えるようにするため
  • Amazon 側は個別商品ページの URL ではなく、検索キーワード + アソシエイトタグで組んだ URL を使う。個別 ASIN は出品終了・URL 変更で 404 化しやすいが、検索クエリ形式は同じ理由で腐りにくい
export function buildAffiliateCard(product, slug) {
  const buttons = [];
  if (product.rakuten) buttons.push(makeButton(product.rakuten, '楽天で見る'));
  const amazonHref = product.amazonUrl || amazonSearchUrl(product.amazonSearch);
  if (amazonHref) buttons.push(makeButton(amazonHref, 'Amazonで見る'));
  if (buttons.length === 0) return ''; // 有効なリンクが無ければ描画しない
  return renderCard(product, buttons);
}

やってみてわかったこと

  • 本文と CTA を分離しておくと、ASP の切り替えやアソシエイトタグの変更があっても記事本文を1文字も触らずに対応できる。修正箇所が「注入ロジック1箇所」に閉じる
  • 冪等性を最初から設計に入れておかないと、ビルドのたびに同じカードが重複して差し込まれるバグを生む。ビルドは何度も走るものという前提を置く
  • カテゴリ既定へのフォールバックは便利だが、無条件に全記事へ適用すると無関係な記事に広告が付くスパム化を招く。オプトイン(登録したカテゴリだけ)にしたことで、この事故を防いでいる

更新履歴

  • 2026-08-06: 初稿公開