概要
DMM商品情報APIから新着・人気商品を取得し、ブログ記事用のHTMLを生成するNode.jsスクリプト。
前提
- Node.js 18以上
api_idとaffiliate_id(API用: 末尾990〜999、リンク用: 通常ID)を取得済み
スクリプト
// 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
補足
- APIリクエスト時のaffiliate_idは末尾990〜999を使う。レスポンスに含まれるアフィリエイトURLもこのIDで生成される。
- ブログに掲載するリンクには通常のアフィリエイトIDを使う。スクリプト内の
replaceAffiliateIdで差し替えている。 - 1リクエスト最大100件。それ以上取得するには
offsetをずらしてループする。 sortをrankにすれば人気順、dateで新着順。keyword,article/article_idでジャンル・女優・メーカー等の絞り込みが可能。IDは各検索API(女優検索、ジャンル検索など)から取得できる。