← knym.net

Next.js MDX ブログに Amazon 商品カードを実装する

·7 min read
Next.jsMDXAmazonブログ運営

WordPress から Next.js + MDX に移行したあと、記事内の Amazon アフィリエイトリンクが全滅した。
WordPress で使っていた Rakuten ウィジェットや Amazon の HTML は、MDX v2 では動かないことが判明。

そこで ASIN を渡すだけで商品情報を自動取得・表示する <AmazonCard> コンポーネントを作った。

Rakuten ウィジェットが MDX で壊れる理由

WordPress からエクスポートした記事には、こんな HTML が入っていた:

<table border="0" cellpadding="0" style="width:500px">
  <tr><td style="font-size:12px">...</td></tr>
</table>

MDX v2 以降では、本文中の HTML は JSX として解釈される。
JSX では style 属性に CSS 文字列は使えない(オブジェクトが必要)し、cellpaddingcellPadding に直さないといけない。
結果、compileMDX がエラーを出してページが 404 になっていた。

対策: 壊れた HTML を削除して、後述の AmazonCard に置き換える。

Amazon Creators API とは

Amazon アソシエイトに紐づく新しい API。
従来の Product Advertising API(PA API v5)とは別で、2025年以降に順次展開されている。

PA API v5Creators API
認証AWS Signature V4LWA(Login with Amazon)
申請条件180日以内に1成約アソシエイトアカウント
SDKpaapi5-nodejs-sdkcreatorsapi-nodejs-sdk

日本向けは バージョン 3.3(FE / Far East)を使う。

セットアップ

  1. ステップ1: SDK をインストール

    pnpm add creatorsapi-nodejs-sdk

    npm に公開されているので pip install 不要で使える(2026年3月時点 v1.2.0)。

  2. ステップ2: Creators API の認証情報を取得

    1. Amazon アソシエイト・セントラル → 「ツール」→「Creators API」
    2. アクセスキーとシークレットキーを発行
    3. 有効化まで最大 48 時間かかる(発行直後は 403 になる)
  3. ステップ3: 環境変数を設定

    プロジェクトルートの .env.local に追記:

    AMAZON_CREATOR_API_ACCESS_KEY=(アクセスキー)
    AMAZON_CREATOR_API_SECRET_KEY=(シークレットキー)
    

    .env.local の変更は dev server の起動時にのみ読み込まれる。
    追記後は pnpm dev の再起動が必要。

  4. ステップ4: next.config.ts に追加

    SDK が Node.js の superagent を使うため、Webpack バンドルから除外する:

    // next.config.ts
    serverExternalPackages: ["shiki", "creatorsapi-nodejs-sdk", "superagent"],
  5. ステップ5: 型定義ファイルを作成

    SDK には TypeScript 型定義がないので types/creatorsapi-nodejs-sdk.d.ts を作る:

    declare module "creatorsapi-nodejs-sdk" {
      export class ApiClient {
        setCredentialId(id: string): void;
        setCredentialSecret(secret: string): void;
        setVersion(version: string): void;
      }
     
      export class GetItemsRequestContent {
        constructor(partnerTag: string, itemIds: string[]);
        resources: string[];
      }
     
      export class DefaultApi {
        constructor(client: ApiClient);
        getItems(
          marketplace: string,
          request: GetItemsRequestContent
        ): Promise<{
          itemsResult?: {
            items?: Array<{
              itemInfo?: { title?: { displayValue?: string } };
              images?: { primary?: { medium?: { url?: string } } };
              offersV2?: {
                listings?: Array<{
                  price?: { displayAmount?: string };
                }>;
              };
            }>;
          };
        }>;
      }
    }

lib/amazon.ts(APIユーティリティ)

import { ApiClient, DefaultApi, GetItemsRequestContent } from "creatorsapi-nodejs-sdk";
 
const MARKETPLACE = "www.amazon.co.jp";
const PARTNER_TAG = "(アソシエイトID)";
const VERSION = "3.3";
 
export type AmazonProduct = {
  title: string;
  imageUrl: string | null;
  price: string | null;
  affiliateUrl: string;
};
 
function createApiClient(): ApiClient {
  const client = new ApiClient();
  client.setCredentialId(process.env.AMAZON_CREATOR_API_ACCESS_KEY!);
  client.setCredentialSecret(process.env.AMAZON_CREATOR_API_SECRET_KEY!);
  client.setVersion(VERSION);
  return client;
}
 
export async function getAmazonProduct(asin: string): Promise<AmazonProduct | null> {
  if (!process.env.AMAZON_CREATOR_API_ACCESS_KEY) return null;
 
  try {
    const client = createApiClient();
    const api = new DefaultApi(client);
 
    const request = new GetItemsRequestContent(PARTNER_TAG, [asin]);
    request.resources = [
      "images.primary.medium",
      "itemInfo.title",
      "offersV2.listings.price",
    ];
 
    const response = await api.getItems(MARKETPLACE, request);
    const item = response?.itemsResult?.items?.[0];
    if (!item) return null;
 
    return {
      title: item.itemInfo?.title?.displayValue ?? asin,
      imageUrl: item.images?.primary?.medium?.url ?? null,
      price: item.offersV2?.listings?.[0]?.price?.displayAmount ?? null,
      affiliateUrl: `https://www.amazon.co.jp/dp/${asin}/?tag=${PARTNER_TAG}`,
    };
  } catch (err) {
    console.error(`[AmazonCard] getItems failed for ${asin}:`, err);
    return null;
  }
}

components/AmazonCard.tsx(Server Component)

import { getAmazonProduct } from "@/lib/amazon";
 
type Props = {
  asin: string;
  title?: string;
};
 
export async function AmazonCard({ asin, title: fallbackTitle }: Props) {
  const product = await getAmazonProduct(asin);
 
  const title = product?.title ?? fallbackTitle ?? asin;
  const imageUrl = product?.imageUrl ?? null;
  const price = product?.price ?? null;
  const url = product?.affiliateUrl
    ?? `https://www.amazon.co.jp/dp/${asin}/?tag=(アソシエイトID)`;
 
  return (
    <a
      href={url}
      target="_blank"
      rel="noopener noreferrer sponsored"
      className="not-prose my-6 flex items-center gap-4 rounded-lg border ..."
    >
      {imageUrl ? (
        <img src={imageUrl} alt={title} width={64} height={64} className="..." />
      ) : (
        <div className="...">(プレースホルダー)</div>
      )}
      <div className="flex-1 min-w-0">
        <p className="text-sm font-medium ...">{title}</p>
        {price && <p className="mt-1 text-sm ...">{price}</p>}
        <p className="mt-1 text-xs text-amber-600 ...">Amazon で見る →</p>
      </div>
    </a>
  );
}

async な Server Component なので、ビルド時または SSR 時に API を叩いて商品データを埋め込む。
"use client" は不要。

MDX に登録して記事で使う

// components/Mdx.tsx
import { AmazonCard } from "./AmazonCard";
 
export const mdxComponents = {
  AmazonCard,
  // ... 他のコンポーネント
};

MDX 記事内で以下のように使える:

<AmazonCard asin="B08CXRJFNG" title="YAMANI スウェー プロテクター" />

API が有効な場合 → 商品画像・価格・タイトルを自動取得して表示
API が無効または 403 の場合 → title prop のテキストのみ表示(フォールバック)

ハマりポイントまとめ

API 発行直後は 403 になる
「AccessDeniedException」が返ってくるが、これは有効化待ちの状態。48時間待てば解消する。

.env.local を更新したら再起動が必要
Next.js の dev server は .env.local を起動時にしか読まない。
変数を追加・変更したら再起動しないと反映されない。

Rakuten の HTML ウィジェットは MDX で使えない
style="..." 文字列が JSX 非対応でコンパイルエラーになる。削除して <AmazonCard> に置き換えること。

SDK に TypeScript 型定義がない
types/creatorsapi-nodejs-sdk.d.ts を自前で用意する必要がある。

まとめ

ASIN と title を渡すだけで商品カードが生成できるようになった。
API が有効になれば画像・価格も自動取得されるので、手動で用意するものがなくなる。

WordPress 時代のアフィリエイトリンクは MDX 移行時に全滅しやすいので、
この構成に統一しておくと記事を書くたびの手間が減る。