Competitor research, price monitoring, and market analysis all require product data. Manually copying product names and prices from competitor stores is tedious and error-prone.
What if you could build a Chrome extension that automatically scrapes product information from any Shopify or WooCommerce store? With Claude AI, you can - even if you have limited coding experience.
This guide walks through how to use Claude to build a complete, production-ready product scraper extension from scratch.
What We Are Building
A Chrome extension that:
- Detects whether a store runs on Shopify or WooCommerce
- Extracts product names, prices, URLs, and images
- Handles pagination automatically
- Uses multiple extraction methods with smart fallbacks
- Exports data as CSV or JSON
- Works entirely in the browser with no backend server
By the end, you will have a functional tool for competitive intelligence.
Prerequisites
Before starting, you need:
- Google Chrome or Microsoft Edge browser
- A text editor (VS Code, Sublime Text, or even Notepad)
- Basic familiarity with copying and pasting code
- A Claude account
No prior coding experience is required. Claude will generate all the code.
Step 1: Understanding the Extension Architecture
Before asking Claude to write code, understand what we need.
Chrome Extension Components
Every Manifest V3 Chrome extension needs:
manifest.json: Configuration file that tells Chrome about your extension
popup.html: The user interface that appears when you click the extension icon
popup.js: JavaScript that controls the popup behavior
background.js: A service worker that handles background tasks (like making web requests)
styles.css: Styling for the popup
modules/: Folder containing specialized scraping logic
The Scraping Strategy
Our extension will use multiple methods to extract product data:
- API endpoints: Shopify and WooCommerce expose product data via APIs
- JSON-LD: Structured data embedded in HTML for SEO
- DOM scraping: Parsing the actual HTML elements as a last resort
Each method has trade-offs. APIs are fastest and most reliable but may be restricted. DOM scraping works when APIs fail but is fragile.
Step 2: Ask Claude to Create the Manifest
Start by asking Claude for the extension manifest.
Prompt to Use
“Create a manifest.json file for a Chrome extension (Manifest V3) called ‘Product Scraper’. It should:
- Have permissions to make web requests to any URL
- Include a popup interface
- Use a background service worker
- Have icons at 16, 48, and 128 pixel sizes
- Work on both Shopify and WooCommerce stores”
Claude’s Response
Claude will generate:
{
"manifest_version": 3,
"name": "Product Scraper",
"version": "1.0.0",
"description": "Scrape product names and prices from Shopify and WooCommerce stores",
"permissions": [
"activeTab",
"storage"
],
"host_permissions": [
"https://*/*",
"http://*/*"
],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"background": {
"service_worker": "background.js",
"type": "module"
},
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}
Save this as manifest.json in your extension folder.
Step 3: Ask Claude to Build the Popup Interface
Now create the user interface.
Prompt to Use
“Create popup.html and styles.css for the Product Scraper extension. The popup should include:
- An input field for the store URL
- A ‘Scrape Products’ button
- A status indicator showing progress
- A results preview table showing product name, price, and source
- Export buttons for CSV and JSON
- Clean, modern styling with a max height of 500px
- Error message display area”
Claude’s Response
Claude will provide the HTML:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<h1>Product Scraper</h1>
<div class="input-group">
<input
type="url"
id="storeUrl"
placeholder="https://example-store.com"
>
<button id="scrapeBtn">Scrape Products</button>
</div>
<div id="status" class="status hidden"></div>
<div id="error" class="error hidden"></div>
<div id="results" class="results hidden">
<div class="results-header">
<span id="productCount">0 products found</span>
<div class="export-buttons">
<button id="exportCsv" class="export-btn">Export CSV</button>
<button id="exportJson" class="export-btn">Export JSON</button>
</div>
</div>
<div class="table-container">
<table id="productsTable">
<thead>
<tr>
<th>Product</th>
<th>Price</th>
<th>Source</th>
</tr>
</thead>
<tbody id="productsBody"></tbody>
</table>
</div>
</div>
</div>
<script src="popup.js" type="module"></script>
</body>
</html>
And the CSS:
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
width: 400px;
max-height: 500px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
color: #333;
}
.container {
padding: 16px;
}
h1 {
font-size: 18px;
margin-bottom: 16px;
color: #1a73e8;
}
.input-group {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
input[type="url"] {
flex: 1;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
input[type="url"]:focus {
outline: none;
border-color: #1a73e8;
}
button {
padding: 10px 16px;
background: #1a73e8;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
button:hover {
background: #1557b0;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.status {
padding: 12px;
background: #e8f0fe;
border-radius: 6px;
margin-bottom: 16px;
color: #1a73e8;
}
.error {
padding: 12px;
background: #fce8e6;
border-radius: 6px;
margin-bottom: 16px;
color: #c5221f;
}
.hidden {
display: none;
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
#productCount {
font-weight: 500;
color: #5f6368;
}
.export-buttons {
display: flex;
gap: 8px;
}
.export-btn {
padding: 6px 12px;
font-size: 12px;
background: #34a853;
}
.export-btn:hover {
background: #2d8e47;
}
.table-container {
max-height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 6px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 10px 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background: #f8f9fa;
font-weight: 500;
position: sticky;
top: 0;
}
td {
font-size: 13px;
}
td:first-child {
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
Save these as popup.html and styles.css.
Step 4: Ask Claude for the Popup Controller
The popup needs JavaScript to handle user interactions.
Prompt to Use
“Create popup.js for the Product Scraper extension. It should:
- Send messages to the background service worker to initiate scraping
- Update the UI with progress status
- Display results in the table (limit preview to 50 products)
- Handle CSV export with proper escaping
- Handle JSON export
- Save the last used URL to storage
- Show appropriate error messages”
Claude’s Response
Claude will generate the controller logic:
// popup.js
let scrapedProducts = [];
document.addEventListener('DOMContentLoaded', async () => {
const urlInput = document.getElementById('storeUrl');
const scrapeBtn = document.getElementById('scrapeBtn');
const statusDiv = document.getElementById('status');
const errorDiv = document.getElementById('error');
const resultsDiv = document.getElementById('results');
const productCount = document.getElementById('productCount');
const productsBody = document.getElementById('productsBody');
const exportCsvBtn = document.getElementById('exportCsv');
const exportJsonBtn = document.getElementById('exportJson');
// Load last used URL
const stored = await chrome.storage.local.get('lastUrl');
if (stored.lastUrl) {
urlInput.value = stored.lastUrl;
}
// Scrape button click
scrapeBtn.addEventListener('click', async () => {
const url = urlInput.value.trim();
if (!url) {
showError('Please enter a store URL');
return;
}
if (!isValidUrl(url)) {
showError('Please enter a valid URL');
return;
}
// Save URL
await chrome.storage.local.set({ lastUrl: url });
// Reset UI
hideError();
hideResults();
showStatus('Detecting platform...');
scrapeBtn.disabled = true;
try {
// Send message to background script
const response = await chrome.runtime.sendMessage({
action: 'scrape',
url: url
});
if (response.error) {
showError(response.error);
} else {
scrapedProducts = response.products;
displayResults(scrapedProducts);
}
} catch (error) {
showError('Scraping failed: ' + error.message);
} finally {
scrapeBtn.disabled = false;
hideStatus();
}
});
// Listen for progress updates
chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'progress') {
showStatus(message.text);
}
});
// Export CSV
exportCsvBtn.addEventListener('click', () => {
if (scrapedProducts.length === 0) return;
const headers = ['Title', 'Price', 'Currency', 'URL', 'Image', 'Source', 'Scraped At'];
const rows = scrapedProducts.map(p => [
escapeCsv(p.title),
p.price,
p.currency || '',
p.url,
p.image || '',
p.source_method,
p.scraped_at
]);
const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
downloadFile(csv, 'products.csv', 'text/csv');
});
// Export JSON
exportJsonBtn.addEventListener('click', () => {
if (scrapedProducts.length === 0) return;
const json = JSON.stringify(scrapedProducts, null, 2);
downloadFile(json, 'products.json', 'application/json');
});
// Helper functions
function isValidUrl(string) {
try {
new URL(string);
return true;
} catch (_) {
return false;
}
}
function showStatus(text) {
statusDiv.textContent = text;
statusDiv.classList.remove('hidden');
}
function hideStatus() {
statusDiv.classList.add('hidden');
}
function showError(text) {
errorDiv.textContent = text;
errorDiv.classList.remove('hidden');
}
function hideError() {
errorDiv.classList.add('hidden');
}
function hideResults() {
resultsDiv.classList.add('hidden');
}
function displayResults(products) {
productCount.textContent = `${products.length} products found`;
// Clear existing rows
productsBody.innerHTML = '';
// Show first 50 products
const preview = products.slice(0, 50);
preview.forEach(product => {
const row = document.createElement('tr');
row.innerHTML = `
<td title="${escapeHtml(product.title)}">${escapeHtml(product.title)}</td>
<td>${formatPrice(product.price, product.currency)}</td>
<td>${product.source_method}</td>
`;
productsBody.appendChild(row);
});
resultsDiv.classList.remove('hidden');
}
function formatPrice(price, currency) {
if (price === null || price === undefined) return 'N/A';
const symbol = getCurrencySymbol(currency);
return `${symbol}${price.toFixed(2)}`;
}
function getCurrencySymbol(currency) {
const symbols = {
'USD': '$', 'EUR': '€', 'GBP': '£', 'CAD': 'C$',
'AUD': 'A$', 'JPY': '¥', 'CNY': '¥', 'INR': '₹'
};
return symbols[currency] || (currency ? currency + ' ' : '$');
}
function escapeCsv(str) {
if (str === null || str === undefined) return '';
str = String(str);
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
return '"' + str.replace(/"/g, '""') + '"';
}
return str;
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function downloadFile(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
});
Save this as popup.js.
Step 5: Ask Claude for the Background Service Worker
The background script coordinates the actual scraping.
Prompt to Use
“Create background.js as a service worker for the Product Scraper Chrome extension. It should:
- Listen for scrape requests from the popup
- Detect if the store is Shopify or WooCommerce
- Import and use platform-specific modules
- Coordinate the scraping process
- Send progress updates back to the popup
- Handle errors gracefully
- Add random delays between requests to avoid rate limiting”
Claude’s Response
// background.js
import { scrapeShopify } from './modules/shopify.js';
import { scrapeWooCommerce } from './modules/woocommerce.js';
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'scrape') {
handleScrape(message.url)
.then(products => sendResponse({ products }))
.catch(error => sendResponse({ error: error.message }));
return true; // Keep message channel open for async response
}
});
async function handleScrape(url) {
const baseUrl = new URL(url).origin;
sendProgress('Detecting platform...');
const platform = await detectPlatform(baseUrl);
if (!platform) {
throw new Error('Could not detect platform. Supported: Shopify, WooCommerce');
}
sendProgress(`Detected ${platform}. Starting scrape...`);
let products;
if (platform === 'shopify') {
products = await scrapeShopify(url, sendProgress);
} else if (platform === 'woocommerce') {
products = await scrapeWooCommerce(url, sendProgress);
}
// Deduplicate by URL
const seen = new Set();
products = products.filter(p => {
if (seen.has(p.url)) return false;
seen.add(p.url);
return true;
});
sendProgress(`Scraping complete. Found ${products.length} products.`);
return products;
}
async function detectPlatform(baseUrl) {
try {
// Try Shopify detection
const shopifyCheck = await fetch(`${baseUrl}/products.json?limit=1`);
if (shopifyCheck.ok) {
return 'shopify';
}
} catch (e) {}
try {
// Try fetching homepage and check for Shopify indicators
const homeResponse = await fetch(baseUrl);
const html = await homeResponse.text();
if (html.includes('cdn.shopify.com') || html.includes('Shopify.theme')) {
return 'shopify';
}
// Check for WooCommerce indicators
if (html.includes('woocommerce') || html.includes('wp-content')) {
return 'woocommerce';
}
} catch (e) {}
try {
// Try WooCommerce Store API
const wooCheck = await fetch(`${baseUrl}/wp-json/wc/store/products?per_page=1`);
if (wooCheck.ok) {
return 'woocommerce';
}
} catch (e) {}
return null;
}
function sendProgress(text) {
chrome.runtime.sendMessage({ type: 'progress', text });
}
// Utility: Random delay to avoid rate limiting
export function delay(min = 100, max = 500) {
const ms = Math.floor(Math.random() * (max - min + 1)) + min;
return new Promise(resolve => setTimeout(resolve, ms));
}
Save this as background.js.
Step 6: Ask Claude for Platform-Specific Modules
Now we need the actual scraping logic for each platform.
Prompt for Shopify Module
“Create modules/shopify.js for the Product Scraper extension. It should:
- Try the /products.json API endpoint first
- Fall back to collection endpoints if needed
- Fall back to JSON-LD structured data
- Fall back to DOM scraping as last resort
- Handle pagination (Shopify limits to 250 products per page)
- Extract: id, title, price (minimum variant price), currency, URL, image
- Normalize price to numeric value
- Add timestamps and source method to each product”
Claude’s Response
// modules/shopify.js
import { parsePrice, generateId } from './parser.js';
export async function scrapeShopify(url, sendProgress) {
const baseUrl = new URL(url).origin;
const urlPath = new URL(url).pathname;
let products = [];
// Method 1: Try products.json API
sendProgress('Trying Shopify API...');
products = await tryProductsApi(baseUrl);
if (products.length > 0) {
return products;
}
// Method 2: Try collection endpoint if URL is a collection
if (urlPath.includes('/collections/')) {
sendProgress('Trying collection API...');
const collectionHandle = urlPath.split('/collections/')[1].split('/')[0];
products = await tryCollectionApi(baseUrl, collectionHandle);
if (products.length > 0) {
return products;
}
}
// Method 3: Try JSON-LD from HTML
sendProgress('Trying JSON-LD extraction...');
products = await tryJsonLd(url);
if (products.length > 0) {
return products;
}
// Method 4: DOM scraping fallback
sendProgress('Trying DOM scraping...');
products = await tryDomScraping(url);
return products;
}
async function tryProductsApi(baseUrl) {
const products = [];
let page = 1;
const limit = 250;
try {
while (true) {
const response = await fetch(
`${baseUrl}/products.json?limit=${limit}&page=${page}`
);
if (!response.ok) break;
const data = await response.json();
if (!data.products || data.products.length === 0) break;
for (const product of data.products) {
products.push(formatShopifyProduct(product, baseUrl, 'shopify-api'));
}
if (data.products.length < limit) break;
page++;
await delay(200, 500);
}
} catch (e) {
console.error('Products API failed:', e);
}
return products;
}
async function tryCollectionApi(baseUrl, collectionHandle) {
const products = [];
let page = 1;
const limit = 250;
try {
while (true) {
const response = await fetch(
`${baseUrl}/collections/${collectionHandle}/products.json?limit=${limit}&page=${page}`
);
if (!response.ok) break;
const data = await response.json();
if (!data.products || data.products.length === 0) break;
for (const product of data.products) {
products.push(formatShopifyProduct(product, baseUrl, 'shopify-collection-json'));
}
if (data.products.length < limit) break;
page++;
await delay(200, 500);
}
} catch (e) {
console.error('Collection API failed:', e);
}
return products;
}
async function tryJsonLd(url) {
const products = [];
try {
const response = await fetch(url);
const html = await response.text();
const jsonLdMatches = html.match(
/<script type="application\/ld\+json">([\s\S]*?)<\/script>/gi
);
if (!jsonLdMatches) return products;
for (const match of jsonLdMatches) {
try {
const jsonStr = match
.replace(/<script type="application\/ld\+json">/i, '')
.replace(/<\/script>/i, '');
const data = JSON.parse(jsonStr);
if (data['@type'] === 'Product') {
products.push(formatJsonLdProduct(data, 'shopify-jsonld'));
} else if (Array.isArray(data)) {
for (const item of data) {
if (item['@type'] === 'Product') {
products.push(formatJsonLdProduct(item, 'shopify-jsonld'));
}
}
}
} catch (e) {}
}
} catch (e) {
console.error('JSON-LD extraction failed:', e);
}
return products;
}
async function tryDomScraping(url) {
const products = [];
try {
const response = await fetch(url);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Common Shopify product selectors
const selectors = [
'.product-card',
'.product-item',
'.grid__item[data-product-id]',
'.product-grid-item',
'[data-product-card]'
];
let productElements = [];
for (const selector of selectors) {
productElements = doc.querySelectorAll(selector);
if (productElements.length > 0) break;
}
productElements.forEach(el => {
const titleEl = el.querySelector(
'.product-card__title, .product-item__title, .product__title, h2, h3'
);
const priceEl = el.querySelector(
'.price, .product-price, .money, [data-product-price]'
);
const linkEl = el.querySelector('a[href*="/products/"]');
const imgEl = el.querySelector('img');
if (titleEl && linkEl) {
const priceText = priceEl ? priceEl.textContent : null;
const { price, currency } = parsePrice(priceText);
products.push({
id: generateId(linkEl.href),
title: titleEl.textContent.trim(),
price: price,
currency: currency,
url: new URL(linkEl.href, url).href,
image: imgEl ? imgEl.src : null,
source_method: 'shopify-dom',
scraped_at: new Date().toISOString()
});
}
});
} catch (e) {
console.error('DOM scraping failed:', e);
}
return products;
}
function formatShopifyProduct(product, baseUrl, sourceMethod) {
// Get minimum variant price
let minPrice = null;
let currency = null;
if (product.variants && product.variants.length > 0) {
const prices = product.variants
.map(v => parseFloat(v.price))
.filter(p => !isNaN(p));
if (prices.length > 0) {
minPrice = Math.min(...prices);
}
}
return {
id: String(product.id),
title: product.title,
price: minPrice,
currency: currency,
url: `${baseUrl}/products/${product.handle}`,
image: product.images?.[0]?.src || null,
source_method: sourceMethod,
scraped_at: new Date().toISOString()
};
}
function formatJsonLdProduct(data, sourceMethod) {
let price = null;
let currency = null;
if (data.offers) {
const offer = Array.isArray(data.offers) ? data.offers[0] : data.offers;
price = parseFloat(offer.price) || null;
currency = offer.priceCurrency || null;
}
return {
id: generateId(data.url || data.name),
title: data.name,
price: price,
currency: currency,
url: data.url || null,
image: Array.isArray(data.image) ? data.image[0] : data.image,
source_method: sourceMethod,
scraped_at: new Date().toISOString()
};
}
function delay(min, max) {
const ms = Math.floor(Math.random() * (max - min + 1)) + min;
return new Promise(resolve => setTimeout(resolve, ms));
}
Save this as modules/shopify.js.
Prompt for WooCommerce Module
“Create modules/woocommerce.js for the Product Scraper extension. It should:
- Try the WooCommerce Store API first
- Fall back to REST API v3 if available
- Fall back to JSON-LD structured data
- Fall back to DOM scraping
- Handle pagination
- Extract: id, title, price, currency, URL, image
- Normalize prices and handle sale prices”
Claude’s Response
// modules/woocommerce.js
import { parsePrice, generateId } from './parser.js';
export async function scrapeWooCommerce(url, sendProgress) {
const baseUrl = new URL(url).origin;
let products = [];
// Method 1: Try WooCommerce Store API
sendProgress('Trying WooCommerce Store API...');
products = await tryStoreApi(baseUrl);
if (products.length > 0) {
return products;
}
// Method 2: Try REST API v3
sendProgress('Trying WooCommerce REST API...');
products = await tryRestApi(baseUrl);
if (products.length > 0) {
return products;
}
// Method 3: Try JSON-LD
sendProgress('Trying JSON-LD extraction...');
products = await tryJsonLd(url);
if (products.length > 0) {
return products;
}
// Method 4: DOM scraping
sendProgress('Trying DOM scraping...');
products = await tryDomScraping(url);
return products;
}
async function tryStoreApi(baseUrl) {
const products = [];
let page = 1;
const perPage = 100;
try {
while (true) {
const response = await fetch(
`${baseUrl}/wp-json/wc/store/products?per_page=${perPage}&page=${page}`
);
if (!response.ok) break;
const data = await response.json();
if (!data || data.length === 0) break;
for (const product of data) {
products.push(formatStoreApiProduct(product, baseUrl));
}
if (data.length < perPage) break;
page++;
await delay(200, 500);
}
} catch (e) {
console.error('Store API failed:', e);
}
return products;
}
async function tryRestApi(baseUrl) {
const products = [];
let page = 1;
const perPage = 100;
try {
while (true) {
const response = await fetch(
`${baseUrl}/wp-json/wc/v3/products?per_page=${perPage}&page=${page}`
);
if (!response.ok) break;
const data = await response.json();
if (!data || data.length === 0) break;
for (const product of data) {
products.push(formatRestApiProduct(product, baseUrl));
}
if (data.length < perPage) break;
page++;
await delay(200, 500);
}
} catch (e) {
console.error('REST API failed:', e);
}
return products;
}
async function tryJsonLd(url) {
const products = [];
try {
const response = await fetch(url);
const html = await response.text();
const jsonLdMatches = html.match(
/<script type="application\/ld\+json">([\s\S]*?)<\/script>/gi
);
if (!jsonLdMatches) return products;
for (const match of jsonLdMatches) {
try {
const jsonStr = match
.replace(/<script type="application\/ld\+json">/i, '')
.replace(/<\/script>/i, '');
const data = JSON.parse(jsonStr);
if (data['@type'] === 'Product') {
products.push(formatJsonLdProduct(data));
} else if (data['@graph']) {
for (const item of data['@graph']) {
if (item['@type'] === 'Product') {
products.push(formatJsonLdProduct(item));
}
}
}
} catch (e) {}
}
} catch (e) {
console.error('JSON-LD extraction failed:', e);
}
return products;
}
async function tryDomScraping(url) {
const products = [];
try {
const response = await fetch(url);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Common WooCommerce product selectors
const productElements = doc.querySelectorAll(
'.product, .wc-block-grid__product, li.product'
);
productElements.forEach(el => {
const titleEl = el.querySelector(
'.woocommerce-loop-product__title, .product-title, h2, h3'
);
const priceEl = el.querySelector(
'.price, .woocommerce-Price-amount'
);
const linkEl = el.querySelector('a.woocommerce-LoopProduct-link, a[href*="/product/"]');
const imgEl = el.querySelector('img');
if (titleEl) {
const priceText = priceEl ? priceEl.textContent : null;
const { price, currency } = parsePrice(priceText);
const productUrl = linkEl ? linkEl.href : null;
products.push({
id: generateId(productUrl || titleEl.textContent),
title: titleEl.textContent.trim(),
price: price,
currency: currency,
url: productUrl,
image: imgEl ? imgEl.src : null,
source_method: 'woo-dom',
scraped_at: new Date().toISOString()
});
}
});
} catch (e) {
console.error('DOM scraping failed:', e);
}
return products;
}
function formatStoreApiProduct(product, baseUrl) {
let price = null;
let currency = null;
if (product.prices) {
// Use sale price if available, otherwise regular price
const priceValue = product.prices.sale_price || product.prices.price;
price = parseInt(priceValue) / 100; // Store API returns cents
currency = product.prices.currency_code;
}
return {
id: String(product.id),
title: product.name,
price: price,
currency: currency,
url: product.permalink,
image: product.images?.[0]?.src || null,
source_method: 'woo-store-api',
scraped_at: new Date().toISOString()
};
}
function formatRestApiProduct(product, baseUrl) {
return {
id: String(product.id),
title: product.name,
price: parseFloat(product.price) || null,
currency: null,
url: product.permalink,
image: product.images?.[0]?.src || null,
source_method: 'woo-rest-api',
scraped_at: new Date().toISOString()
};
}
function formatJsonLdProduct(data) {
let price = null;
let currency = null;
if (data.offers) {
const offer = Array.isArray(data.offers) ? data.offers[0] : data.offers;
price = parseFloat(offer.price) || null;
currency = offer.priceCurrency || null;
}
return {
id: generateId(data.url || data.name),
title: data.name,
price: price,
currency: currency,
url: data.url || null,
image: Array.isArray(data.image) ? data.image[0] : data.image,
source_method: 'woo-jsonld',
scraped_at: new Date().toISOString()
};
}
function delay(min, max) {
const ms = Math.floor(Math.random() * (max - min + 1)) + min;
return new Promise(resolve => setTimeout(resolve, ms));
}
Save this as modules/woocommerce.js.
Prompt for Parser Utilities
“Create modules/parser.js with shared utility functions for:
- Parsing price strings into numeric values
- Detecting currency from symbols or codes
- Generating unique IDs from URLs or titles
- Normalizing product data”
Claude’s Response
// modules/parser.js
export function parsePrice(priceString) {
if (!priceString) {
return { price: null, currency: null };
}
const text = priceString.trim();
// Detect currency
let currency = null;
const currencyPatterns = [
{ symbol: '$', code: 'USD' },
{ symbol: '€', code: 'EUR' },
{ symbol: '£', code: 'GBP' },
{ symbol: '¥', code: 'JPY' },
{ symbol: '₹', code: 'INR' },
{ symbol: 'C$', code: 'CAD' },
{ symbol: 'A$', code: 'AUD' },
{ symbol: 'CHF', code: 'CHF' },
{ symbol: 'kr', code: 'SEK' },
{ symbol: 'zł', code: 'PLN' }
];
for (const { symbol, code } of currencyPatterns) {
if (text.includes(symbol)) {
currency = code;
break;
}
}
// Also check for ISO codes
const isoMatch = text.match(/\b(USD|EUR|GBP|CAD|AUD|JPY|CNY|INR|CHF|SEK|NOK|DKK|PLN|BRL|MXN)\b/i);
if (isoMatch) {
currency = isoMatch[1].toUpperCase();
}
// Extract numeric value
// Handle formats like: $1,234.56 | 1.234,56 € | £1,234
let priceText = text
.replace(/[^\d.,]/g, '') // Remove non-numeric except . and ,
.trim();
// Handle European format (1.234,56 -> 1234.56)
if (priceText.match(/^\d{1,3}(\.\d{3})*(,\d{2})?$/)) {
priceText = priceText.replace(/\./g, '').replace(',', '.');
}
// Handle standard format (1,234.56 -> 1234.56)
else if (priceText.match(/^\d{1,3}(,\d{3})*(\.\d{2})?$/)) {
priceText = priceText.replace(/,/g, '');
}
// Handle simple comma decimal (123,45 -> 123.45)
else if (priceText.match(/^\d+,\d{2}$/)) {
priceText = priceText.replace(',', '.');
}
const price = parseFloat(priceText);
return {
price: isNaN(price) ? null : price,
currency: currency
};
}
export function generateId(input) {
if (!input) {
return 'unknown-' + Date.now();
}
const str = String(input).toLowerCase();
// Simple hash function
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(36);
}
export function normalizeUrl(url, baseUrl) {
if (!url) return null;
try {
return new URL(url, baseUrl).href;
} catch (e) {
return url;
}
}
export function cleanTitle(title) {
if (!title) return '';
return title
.replace(/\s+/g, ' ')
.replace(/[\n\r\t]/g, '')
.trim();
}
Save this as modules/parser.js.
Step 7: Create Icons
Ask Claude to help create simple icons or provide guidance.
Prompt
“I need simple icons for my Chrome extension at 16x16, 48x48, and 128x128 pixels. Can you provide SVG code for a simple product/shopping icon that I can convert to PNG?”
Claude will provide SVG code you can convert using online tools or image editors.
For quick testing, you can create placeholder colored squares or use any icon temporarily.
Step 8: Load and Test the Extension
Now assemble everything and test.
Folder Structure
product-scraper-extension/
├── manifest.json
├── popup.html
├── popup.js
├── styles.css
├── background.js
├── modules/
│ ├── parser.js
│ ├── shopify.js
│ └── woocommerce.js
└── icons/
├── icon16.png
├── icon48.png
└── icon128.png
Load in Chrome
- Open
chrome://extensions/ - Enable “Developer mode”
- Click “Load unpacked”
- Select your extension folder
- Pin the extension to your toolbar
Test It
- Click the extension icon
- Enter a Shopify store URL (e.g.,
https://www.allbirds.com) - Click “Scrape Products”
- Watch the progress updates
- View results and export
Step 9: Troubleshooting with Claude
When issues arise, Claude can help debug.
Common Issues and Prompts
Extension not loading:
“My Chrome extension shows an error when loading. The error message is: [paste error]. Here’s my manifest.json: [paste content]. What’s wrong?”
Scraping returns no products:
“My product scraper returns zero products for this URL: [URL]. The platform is detected as Shopify. How can I debug why the API calls are failing?”
Export not working:
“The CSV export downloads but the file is empty or corrupted. Here’s my export function: [paste code]. What could cause this?”
Step 10: Enhance the Extension
Once basic functionality works, ask Claude for enhancements:
- “Add a progress bar showing percentage complete”
- “Add filtering by price range”
- “Add support for BigCommerce stores”
- “Implement caching so repeat scrapes are faster”
- “Add rate limiting configuration options”
What You Built
You now have a functional Chrome extension that:
- Automatically detects Shopify and WooCommerce stores
- Uses multiple extraction methods with intelligent fallbacks
- Handles pagination for large catalogs
- Exports data in CSV and JSON formats
- Works entirely in the browser
This tool enables:
- Competitor price monitoring
- Market research
- Product catalog analysis
- Price comparison projects
Key Takeaway
Claude AI makes complex development projects accessible. By breaking the project into components and asking specific questions, you can build production-quality tools without deep programming expertise.
The key is iterative development:
- Ask Claude for one component at a time
- Test each piece before moving on
- Use Claude to debug issues as they arise
- Enhance incrementally based on your needs
With this approach, Claude becomes a collaborative development partner, turning your ideas into functional software.
Related Posts
How to Connect Claude to Your Google Ads Account via MCP (No Coding Required)
16 min read
How Claude AI Can Help You Set Up Google Ads Tracking in Google Tag Manager
11 min read
How to Use Claude to Build a Better Google Shopping Feed
Need Help With Your Google Ads?
I help e-commerce brands scale profitably with data-driven PPC strategies.
Get In Touch