← back to Dw Scrape Scheduler
adapters/shopify.js
52 lines
// Shopify feed-first adapter — paginate <base>/products.json?limit=250&page=N.
// $0 local HTTP, no login, no paid service. Retry-with-backoff on transient 503
// (Shopify/CF blips — learned on Astek 2026-07-13). Returns normalized products.
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124 Safari/537.36';
async function fetchPage(url, attempts = 4) {
let last = 0;
for (let i = 0; i < attempts; i++) {
const res = await fetch(url, { headers: { 'User-Agent': UA } });
if (res.ok) return res;
last = res.status;
if (i < attempts - 1) await new Promise(r => setTimeout(r, 1500 * (i + 1)));
}
throw new Error(`HTTP ${last} after ${attempts} attempts`);
}
// base = e.g. https://www.ellipopp.com (trailing slash tolerated)
async function scrape(base, { maxPages = 60, politeMs = 150 } = {}) {
const root = base.replace(/\/+$/, '');
const feed = `${root}/products.json`;
const out = [];
for (let page = 1; page <= maxPages; page++) {
const res = await fetchPage(`${feed}?limit=250&page=${page}`);
const json = await res.json();
const products = json.products || [];
if (products.length === 0) break;
for (const p of products) {
const v0 = (p.variants || [])[0] || {};
const img0 = (p.images || [])[0] || {};
out.push({
mfr_sku: v0.sku || String(p.id),
handle: p.handle,
title: p.title,
product_type: p.product_type || '',
vendor: p.vendor || '',
price: v0.price != null ? Number(v0.price) : null,
body_html: p.body_html || '',
image_url: img0.src || null,
all_images: (p.images || []).map(i => i.src),
product_url: `${root}/products/${p.handle}`,
tags: typeof p.tags === 'string' ? p.tags.split(',').map(s => s.trim()).filter(Boolean) : (p.tags || []),
raw: p,
});
}
if (products.length < 250) break;
await new Promise(r => setTimeout(r, politeMs));
}
return out;
}
module.exports = { scrape, platform: 'shopify' };