Astro 静的ブログにアフィリエイトリンクを frontmatter で管理する設計

Astro 静的ブログにアフィリエイトリンクを frontmatter で管理する設計 使い方・設定
Astro 静的ブログにアフィリエイトリンクを frontmatter で管理する設計のイメージ

Astro でブログを作るとき、アフィリエイトリンクをどこに書くかは悩みどころだ。本文中にベタ書きすると後からのリンク差し替えが大変で、専用管理ページに外出しすると記事との紐付けが曖昧になる。特に複数カテゴリで異なるアフィリエイトサービスを使い分けるケースでは、管理が複雑になりがちだ。

今回の lifeevent-hub(家族向けライフイベント情報ブログ)の実装では、記事の frontmatter に affiliateItems フィールドを宣言するだけで、記事ページの末尾にアフィリエイトボックスが表示されるパターンを採用した。これによって「記事と紐づくリンクは記事ファイルの中で完結する」という原則を保ちながら、設計変更の影響範囲を最小化できた。

結果として以下が実現できた。

  • リンクの追加・差し替えは frontmatter 編集だけで完結
  • デザイン変更は AffiliateBox コンポーネント 1 箇所で全記事に反映
  • TypeScript の型安全性がある(Astro Content Collections の Zod スキーマ)
  • 審査前は PLACEHOLDER を使って記事と実装を先行できる

なぜ frontmatter で管理するのか

なぜ frontmatter で管理するのかのイメージ

アフィリエイトリンクの管理方法はいくつかある。実際に検討した選択肢と比較を整理しておく。

管理方法 メリット デメリット
本文中にベタ書き 実装が最も簡単 差し替え時に全記事を修正する必要がある
専用管理ファイル(JSON等) 一覧管理しやすい 記事とリンクの紐付けが分離して管理が複雑
CMSの専用フィールド UIで管理しやすい Astroの静的生成と相性が悪い、CMS依存
frontmatter(今回の採用) 記事と同じファイルで完結、型安全 YAMLの記法に慣れが必要

frontmatter を選んだ最大の理由は「記事の文脈に合ったリンクを記事ファイルで管理できる」点だ。例えば引越し記事の frontmatter には引越し比較サービスのリンクを書き、結婚記事には結婚式場のリンクを書く。記事を開けばどのリンクが設定されているか一目瞭然で、カテゴリ別のリンク管理が直感的になる。

また、Astro の Content Collections は Zod スキーマによる型検証を持っているため、href のフィールドに文字列以外が入ることを型レベルで防げる。実際に運用してみると、YAML の記法ミスやフィールド名のタイポをビルド時に検知できるため、本番に壊れたリンクが入るリスクが下がった。

アーキテクチャの全体像:スキーマ → frontmatter → UI

このアフィリエイト管理の核となるのが、3 つのレイヤーの連携だ。

  1. スキーマレイヤー:Content Collections で構造を定義
  2. データレイヤー:frontmatter にリンク情報を記述
  3. UI レイヤー:Astro コンポーネントで描画

この 3 段階を順序立てて実装することで、堅牢性と保守性の両立が実現できる。スキーマが型を保証し、frontmatter がデータを持ち、コンポーネントが描画だけを担う。各レイヤーの関心が分離されているため、変更の影響範囲が明確になる。

Content Collections スキーマの定義

Content Collections スキーマの定義のイメージ

src/content.config.ts にスキーマを定義する。ここが今回の設計の核心部分だ。


// src/content.config.ts
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";

const posts = defineCollection({
  loader: glob({ pattern: "**/*.md", base: "./src/content/posts" }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    category: z.enum(["wedding", "funeral", "birth", "moving", "childcare", "money", "general"]),
    tags: z.array(z.string()).default([]),
    publishedAt: z.string(),
    // アフィリエイトリンクの配列(任意)
    affiliateItems: z
      .array(
        z.object({
          title: z.string(),         // リンクテキスト(サービス名)
          href: z.string().url(),    // A8.net などのアフィリエイトリンク(URL 形式を検証)
          description: z.string().optional(), // 補足説明
          badge: z.string().optional(),       // 「無料」「公式」などのバッジ
        }),
      )
      .optional(),
    affiliateBoxTitle: z.string().optional(), // ボックスのタイトル(デフォルトあり)
  }),
});

export const collections = { posts };

affiliateItems.optional() にしているのは重要なポイントだ。アフィリエイトリンクを設定しない記事に対しても frontmatter でこのフィールドを必須にしてしまうと、全記事に affiliateItems: [] を書かなければならなくなる。オプショナルにしておくと、フィールド自体を書かない記事には表示されないため、リンクがない記事は何も変更せずに済む。

hrefz.string().url() を使うことで、URL として不正な文字列(例:PLACEHOLDER や空文字)がビルド時に型エラーとして検知される。PLACEHOLDER 運用をする場合は https://example.com/PLACEHOLDER_SUUMO のように正規の URL 形式にしておくと、スキーマを通過しつつ後から本物のリンクに差し替えやすい。

affiliateBoxTitle.optional() にしており、コンポーネント側でデフォルト値(「関連サービス・比較ツール」)を持つ設計にしている。ボックスのタイトルを変えたい記事だけ frontmatter に書けばよい。

記事の frontmatter に書く

記事の frontmatter に書くのイメージ

スキーマを定義したあとは、記事の MD ファイルの frontmatter に書くだけだ。特別なビルドコマンドも Astro 設定の追加も不要で、frontmatter を書いた瞬間からコンポーネントに反映される。


---
title: 引越しの費用相場と節約ポイント
description: 引越し費用は時期・距離・荷物量によって変わる。相場を把握して賢く節約する方法を解説。
category: moving
publishedAt: "2026-03-01"
affiliateBoxTitle: "引越し・保険の比較サービス"
affiliateItems:
  - title: "引越し侍 — 業界最大級の業者ネットワーク"
    href: "https://px.a8.net/..."
    description: "全国 300 社以上から最安値を比較。即時に概算金額も確認可能。"
  - title: "保険スクエアbang!— 火災保険を無料診断"
    href: "https://px.a8.net/..."
    description: "引越し時に見直したい火災保険。複数社を無料で比較できる。"
    badge: "無料"
---

本文...

YAML の配列として複数のリンクを並べるだけだ。badge フィールドがあるアイテムだけバッジが表示され、description フィールドがあるアイテムだけ説明文が表示される。必須は titlehref の2フィールドのみなので、最小限の設定で動かすこともできる。

実際に lifeevent-hub で設定してみると、「引越し費用の記事には引越し比較サービス、保険の記事には保険比較サービス」という自然なリンク設計ができた。カテゴリが同じでも記事の内容に応じてリンクを変えられるのが、専用管理ファイルにはない柔軟性だ。

ページレイアウトで出力する

ページレイアウトで出力するのイメージ

記事ページのレイアウト([...slug].astro)で、affiliateItems があれば AffiliateBox コンポーネントを呼び出す。


---
// src/pages/posts/[...slug].astro
import AffiliateBox from "@/components/AffiliateBox.astro";
const { post } = Astro.props;
---

<article>
  <!-- 本文 -->
  <Content />

  <!-- アフィリエイトボックス(定義がある記事のみ) -->
  {
    post.data.affiliateItems && post.data.affiliateItems.length > 0 && (
      <AffiliateBox
        title={post.data.affiliateBoxTitle}
        items={post.data.affiliateItems}
      />
    )
  }
</article>

post.data.affiliateItems は Zod スキーマから型推論されるので、.title.href のアクセスに TypeScript の補完が効く。post.data.affiliateItemsundefined の記事では条件が false になるため、コンポーネントが表示されない。

このレイアウトのポイントは「affiliateItems.length > 0 で空配列もガードしている」点だ。PLACEHOLDER で後からリンクを差し替える運用では、一時的に affiliateItems: [] のままになることがある。空配列チェックを入れておくと、ボックスは表示されるがリンクが0件という中途半端な表示を防げる。

AffiliateBox コンポーネントの実装例

AffiliateBox コンポーネントの実装例のイメージ

---
// src/components/AffiliateBox.astro
interface AffiliateItem {
  title: string;
  href: string;
  description?: string;
  badge?: string;
}

interface Props {
  title?: string;
  items: AffiliateItem[];
}

const { title = "関連サービス・比較ツール", items } = Astro.props;
---

<aside class="affiliate-box">
  <h2 class="affiliate-box-title">{title}</h2>
  <ul class="affiliate-list">
    {items.map((item) => (
      <li class="affiliate-item">
        <a href={item.href} target="_blank" rel="noopener noreferrer sponsored">
          <span class="affiliate-item-title">
            {item.title}
            {item.badge && <span class="badge">{item.badge}</span>}
          </span>
        </a>
        {item.description && (
          <p class="affiliate-item-desc">{item.description}</p>
        )}
      </li>
    ))}
  </ul>
</aside>

<style>
.affiliate-box { margin-top: 3rem; padding: 1.5rem; border: 1px solid #eee; border-radius: 8px; background-color: #f9f9f9; }
.affiliate-box-title { font-size: 1.4rem; margin-bottom: 1.5rem; color: #333; border-bottom: 2px solid #ddd; padding-bottom: 0.5rem; }
.affiliate-list { list-style: none; padding: 0; margin: 0; }
.affiliate-item { margin-bottom: 1.5rem; background-color: #fff; border: 1px solid #e0e0e0; border-radius: 6px; padding: 1rem; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
.affiliate-item:last-child { margin-bottom: 0; }
.affiliate-item a { text-decoration: none; display: block; color: #007bff; font-weight: bold; font-size: 1.1rem; }
.affiliate-item a:hover { text-decoration: underline; }
.affiliate-item-title { display: flex; align-items: center; gap: 0.5rem; }
.badge { background-color: #28a745; color: #fff; padding: 0.2em 0.6em; border-radius: 4px; font-size: 0.8rem; white-space: nowrap; }
.affiliate-item-desc { margin-top: 0.5rem; color: #555; font-size: 0.95rem; line-height: 1.5; }
</style>

rel="noopener noreferrer sponsored"sponsored は Google のリンク属性ガイドラインに従ったものだ。アフィリエイトリンクには sponsored を付けることが推奨されており、付け忘れると Google からのペナルティリスクがある。noopener noreferrer はセキュリティ上の定番設定で、新しいタブで開くリンクに対してリファラー情報の漏洩と window.opener への参照を防ぐ。

コンポーネントを一箇所に集約している利点は、将来的なデザイン変更が容易な点だ。例えばアフィリエイトボックスのデザインをカード型からリスト型に変えたいときも、AffiliateBox.astro を修正するだけで全記事に反映される。Astro のコンポーネント設計の恩恵がそのまま活きる形だ。

複数カテゴリに展開した実例

lifeevent-hub では以下のカテゴリにアフィリエイトリンクを設定した。各カテゴリで対象サービスが異なるため、frontmatter 管理の柔軟性が特に役立っている。

カテゴリ 設定したサービス
結婚・ご祝儀記事 Hanayume(A8.net)
出産祝い記事 シャディギフトモール(A8.net)、ベビープラネット
引越し記事 引越し侍、SUUMO引越し、保険スクエアbang!
お金・保険記事 保険ランドリー、FP カフェ

各記事の frontmatter を編集するだけで設定できるので、カテゴリに合ったリンクを個別にチューニングしやすい。例えば引越し記事でも「費用比較系」には一括見積もりサービス、「手続きチェックリスト系」には保険比較サービスを優先するといった使い分けができる。

実際に運用してみて気づいたのは、「同じカテゴリでも記事の内容によって最適なリンクが違う」という点だ。引越しカテゴリの記事でも費用を重視した記事には見積もり比較、手続きを重視した記事にはチェックリストサービスと使い分けると、読者の検索意図に合ったリンクになる。この粒度での使い分けは、専用の管理ファイルよりも frontmatter の方が圧倒的に管理しやすい。

PLACEHOLDER を使った段階的な設定方法

アフィリエイトリンクは最初から全部揃っているわけではない。A8.net などのアフィリエイトネットワークの審査が通るまでリンクが発行されないため、記事を先に書いておき、リンクは後から設定するという進め方が現実的だ。


affiliateItems:
  - title: "SUUMO引越し — 大手から地域密着まで比較"
    href: "https://example.com/affiliate/PLACEHOLDER_SUUMO"
    description: "SUUMOの引越し一括見積もり。業者の口コミも確認できる。"

https://example.com/affiliate/PLACEHOLDER_SUUMO のような URL 形式の識別子を入れておけば、z.string().url() バリデーションを通過しつつ、後から grep -r "PLACEHOLDER" src/content/posts/ でまだリンクを設定していない記事を一覧できる。審査が通り次第、順番に本物のリンクに差し替えていく流れができる。

PLACEHOLDER 運用の重要なポイントは「href に本番リンクを入れる前にサイトを公開しない」ことだ。example.com に向いたリンクがある記事を公開しても問題はないが、本番アフィリエイトリンクとして機能しないため収益に繋がらない。Astro のビルド時に grep で PLACEHOLDER を検知してビルドを止めるスクリプトを入れておくと、誤公開を防げる。

実際に lifeevent-hub では、A8.net の審査を申請した時点でコンテンツはほぼ完成していたため、審査通過から数日以内に全記事のリンクを差し替えることができた。frontmatter 管理のおかげで修正が frontmatter だけで完結し、本文のHTMLを一切触らずに済んだ。

アフィリエイト審査のための記事品質チェックリスト

frontmatter の設計が完成しても、アフィリエイトネットワークの審査に通らなければリンクが発行されない。実際に A8.net の審査を通過した際の経験から、審査前に確認すべきポイントをまとめておく。

  • 記事数: 最低でも10記事以上の公開済み記事があること
  • オリジナルコンテンツ: 他サイトからのコピーではなく、自分の言葉で書かれた記事であること
  • サイトのナビゲーション: トップページ・カテゴリページ・記事ページが適切に繋がっていること
  • 問い合わせページ: 運営者情報と連絡先が明記されていること
  • プライバシーポリシー: アフィリエイトサービスを利用していることの明示

Astro の静的サイト生成を使っている場合、rel="sponsored" の設定は審査前から入れておくことを推奨する。Google のガイドラインへの準拠はアフィリエイトネットワークの審査でも好意的に評価されることが多い。

✅ まとめ

  • Astro Content Collections の Zod スキーマに affiliateItems を定義して、frontmatter から宣言する設計が管理しやすい
  • スキーマ → frontmatter → UI の 3 レイヤー構成で関心を分離すると、変更の影響範囲が明確になる
  • href: z.string().url() で URL 形式を型レベルで検証し、不正なリンクをビルド時に検知できる
  • リンクの追加・差し替えは frontmatter 編集だけで完結し、デザイン変更は AffiliateBox コンポーネント 1 箇所で全記事に反映される
  • rel="sponsored" を忘れずに付けることでGoogleのガイドライン準拠を担保する
  • PLACEHOLDER パターンで「先に記事、後からリンク設定」を管理できる
  • 空配列チェックを入れて、リンク未設定記事での中途半端な表示を防ぐ

次のアクションとして、まず content.config.tsaffiliateItems スキーマを追加して、1 記事だけ frontmatter に書いて動作確認してみることをおすすめする。コンポーネントは後から作り込めばよいし、審査申請前に PLACEHOLDER を使って記事とリンク設計を先行させておくと、審査通過後のリンク設定がスムーズになる。

コメント