概要

DMM商品情報APIから新着・人気商品を取得し、ブログ記事用のHTMLを生成するNode.jsスクリプト。

前提

スクリプト

// update-blog.mjs
// DMM商品情報APIから商品を取得し、記事HTMLを生成する

const API_ID = process.env.API_ID;
const AFFILIATE_ID_API = process.env.AFFILIATE_ID_API; // 末尾990〜999(APIリクエスト用)
const AFFILIATE_ID_SITE = process.env.AFFILIATE_ID_SITE; // 通常ID(リンク掲載用)

const BASE_URL = "https://api.dmm.com/affiliate/v3/ItemList";

/**
 * 商品情報APIから商品を取得する
 * @param {Object} options - 検索条件
 * @returns {Promise<Object>} APIレスポンス
 */
async function fetchItems(options = {}) {
  const params = new URLSearchParams({
    api_id: API_ID,
    affiliate_id: AFFILIATE_ID_API,
    site: options.site ?? "FANZA",
    output: "json",
    hits: String(options.hits ?? 20),
    sort: options.sort ?? "date",
    ...options.service && { service: options.service },
    ...options.floor && { floor: options.floor },
    ...options.keyword && { keyword: options.keyword },
    ...options.gte_date && { gte_date: options.gte_date },
  });

  const res = await fetch(`${BASE_URL}?${params}`);
  if (!res.ok) throw new Error(`API error: ${res.status}`);
  const json = await res.json();

  return json.result;
}

/**
 * アフィリエイトURLを生成する
 * レスポンスに含まれるaffiliateURLはAPIリクエスト用IDで生成されているため、
 * ブログ掲載用に通常のアフィリエイトIDで差し替える
 */
function replaceAffiliateId(url) {
  return url.replace(AFFILIATE_ID_API, AFFILIATE_ID_SITE);
}

/**
 * 商品リストからブログ記事用HTMLを組み立てる
 */
function buildArticleHTML(items) {
  const cards = items.map((item) => {
    const affiliateURL = replaceAffiliateId(item.affiliateURL);
    const imageURL = item.imageURL?.large ?? item.imageURL?.small ?? "";
    const price = item.prices?.price ?? "---";
    const review = item.review
      ? `★${item.review.average}(${item.review.count}件)`
      : "";
    const genres = (item.iteminfo?.genre ?? [])
      .map((g) => g.name)
      .join("、");

    return `
<div class="item-card">
  <a href="${affiliateURL}" rel="nofollow" target="_blank">
    <img src="${imageURL}" alt="${item.title}" loading="lazy" />
  </a>
  <h3><a href="${affiliateURL}" rel="nofollow">${item.title}</a></h3>
  <p class="price">${price}</p>
  ${review ? `<p class="review">${review}</p>` : ""}
  ${genres ? `<p class="genres">${genres}</p>` : ""}
  <p class="date">${item.date ?? ""}</p>
</div>`;
  });

  return `<div class="item-grid">\n${cards.join("\n")}\n</div>`;
}

// --- メイン処理 ---
async function main() {
  // 今日の日付から7日前を算出
  const since = new Date();
  since.setDate(since.getDate() - 7);
  const gteDate = since.toISOString().slice(0, 19); // ISO8601(TZなし)

  const result = await fetchItems({
    site: "FANZA",
    service: "digital",
    floor: "videoa",
    sort: "date",
    hits: 30,
    gte_date: gteDate,
  });

  if (!result.items?.length) {
    console.log("新着商品なし");
    return;
  }

  console.log(`取得件数: ${result.result_count} / 全体: ${result.total_count}`);

  const html = buildArticleHTML(result.items);

  // ファイルに出力(実際のブログではCMS APIへPOSTするなど)
  const fs = await import("node:fs/promises");
  const outPath = `./output/article-${new Date().toISOString().slice(0, 10)}.html`;
  await fs.mkdir("./output", { recursive: true });
  await fs.writeFile(outPath, html, "utf-8");
  console.log(`記事HTML出力: ${outPath}`);
}

main().catch(console.error);

実行方法

API_ID=YOUR_API_ID \
AFFILIATE_ID_API=affiliate-990 \
AFFILIATE_ID_SITE=affiliate-001 \
node update-blog.mjs

補足