← back to Designer Wallcoverings
feat(farrow-ball): feed-first collection extraction returns real products
160fad08e0e1a5b2a073ce96aec1c8c70290e4dc · 2026-06-09 15:21:35 -0700 · Steve
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M DW-Programming/ImportNewSkufromURL/lib/scrapers/farrow-ball-collections-scraper.ts
Diff
commit 160fad08e0e1a5b2a073ce96aec1c8c70290e4dc
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Jun 9 15:21:35 2026 -0700
feat(farrow-ball): feed-first collection extraction returns real products
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
.../scrapers/farrow-ball-collections-scraper.ts | 267 ++++++++++++---------
1 file changed, 160 insertions(+), 107 deletions(-)
diff --git a/DW-Programming/ImportNewSkufromURL/lib/scrapers/farrow-ball-collections-scraper.ts b/DW-Programming/ImportNewSkufromURL/lib/scrapers/farrow-ball-collections-scraper.ts
index 8b70a5b1..3882aa02 100644
--- a/DW-Programming/ImportNewSkufromURL/lib/scrapers/farrow-ball-collections-scraper.ts
+++ b/DW-Programming/ImportNewSkufromURL/lib/scrapers/farrow-ball-collections-scraper.ts
@@ -1,121 +1,174 @@
-import { brightDataProxy } from '../brightdata-proxy';
/**
- * Farrow Ball Collections Scraper Skill
- * Focuses specifically on collections/categories scraping for Farrow Ball
+ * Farrow & Ball Collections Scraper — FEED-FIRST (Magento SSR HTML)
+ *
+ * farrow-ball.com is Magento (Adobe Commerce). The category grid is fully
+ * server-rendered as `<li class="item product product-item">` cards — no
+ * browser, no Algolia/LiveSearch round-trip needed. We fetch the category page
+ * and regex the cards for REAL products:
+ * - name → `.product-item-link` text (+ `alt="Purnon 62-04"` on the image)
+ * - color → derived from the F&B reference code on the card
+ * - code → `.product-item-code-value` (e.g. 6204)
+ * - href → product-item-link href (/us/wallpaper/<slug>)
+ * - image → product image src
+ *
+ * Returns an ARRAY of collection objects each carrying `.products[]` (kravet
+ * contract). F&B's wallpaper grid is one flat "All Patterns" collection.
+ *
+ * NO fake/fallback data (CLAUDE.md): if the page yields nothing, return [].
*/
-import { scrapeFarrowBallCollections } from '../lib/scrapers/farrow-ball-collections-scraper';
-import { loadVendorSchema, extractWithSchema, getVendorWaitTime } from '../schema-helper';
-import { ensureArray, safeLength } from '../scraper-wrapper';
+const FB_UA =
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
-export interface FarrowBallCollectionsResult {
+export interface FarrowBallCollectionProduct {
+ name: string;
+ title: string;
+ pattern: string;
+ color: string;
+ sku: string;
+ code: string;
+ url: string;
+ href: string;
+ image?: string;
+ imageUrl?: string;
vendor: string;
- timestamp: string;
- collections: any[];
- totals: {
- collections: number;
- };
+ vendor_id: string;
+ category: string;
+ collection: string;
+ source: string;
+ extracted_at: string;
}
-/**
- * Main collections skill function for Farrow Ball
- */
-export async function runFarrowBallCollectionsSkill(options: {
- maxCollections?: number;
-} = {}): Promise<FarrowBallCollectionsResult> {
- const { maxCollections = 15 } = options;
-
- console.log('🎯 FARROW BALL COLLECTIONS SKILL ACTIVATED');
- console.log('━'.repeat(60));
- console.log(`📂 Max collections: ${maxCollections}`);
- console.log('');
-
- const result: FarrowBallCollectionsResult = {
- vendor: 'Farrow Ball',
- timestamp: new Date().toISOString(),
- collections: [],
- totals: {
- collections: 0
- }
- };
+export interface FarrowBallCollection {
+ name: string;
+ title: string;
+ url: string;
+ href: string;
+ productCount: number;
+ image?: string;
+ products: FarrowBallCollectionProduct[];
+ vendor: string;
+ vendor_id: string;
+ extracted_at: string;
+}
- try {
- console.log('📂 Scraping collections from Farrow Ball...');
-
- const collections = await scrapeFarrowBallCollections();
-
- // Apply limit
- const limitedCollections = collections.slice(0, maxCollections);
- result.collections = limitedCollections;
- result.totals.collections = limitedCollections.length;
-
- console.log(`✅ COLLECTIONS: Found ${limitedCollections.length} collections`);
-
- if (limitedCollections.length > 0) {
- console.log('\n📂 Sample collections:');
- limitedCollections.slice(0, 3).forEach((c, i) => {
- const name = c.name || c.title || c.collectionName || `Collection ${i + 1}`;
- console.log(`${i + 1}. ${name}`);
- });
- }
-
- console.log('\n📊 FARROW BALL COLLECTIONS SUMMARY');
- console.log('━'.repeat(60));
- console.log(`✅ Collections found: ${result.totals.collections}`);
-
- return result;
-
- } catch (error) {
- console.error('❌ Farrow Ball Collections skill error:', error);
- console.error('Error details:', error.message);
- return result;
- }
+function decodeEntities(s: string): string {
+ return (s || '')
+ .replace(/&/g, '&')
+ .replace(/ /g, ' ')
+ .replace(/'|'/g, "'")
+ .replace(/"/g, '"')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/\s+/g, ' ')
+ .trim();
}
-/**
- * Collections skill registration object
- */
-export const farrowballCollectionsSkill = {
- name: 'Farrow Ball Collections Scraper',
- vendor: 'farrow-ball',
- description: 'Collections and categories scraper for Farrow Ball',
-
- // Main execute function
- execute: async (options = {}) => {
- return await runFarrowBallCollectionsSkill(options);
- },
-
- // Direct collections function
- collections: async (maxCollections = 15) => {
- const result = await runFarrowBallCollectionsSkill({ maxCollections });
- return result.collections;
- },
-
- // Test function to verify skill is working
- test: async () => {
- console.log('🧪 Testing Farrow Ball Collections skill...');
- const result = await runFarrowBallCollectionsSkill({ maxCollections: 3 });
-
- const success = result.totals.collections > 0;
- console.log(success ? '✅ Test PASSED' : '❌ Test FAILED');
- return success;
+export async function scrapeFarrowBallCollections(params?: {
+ url?: string;
+ maxResults?: number;
+}): Promise<FarrowBallCollection[]> {
+ const maxProducts = params?.maxResults || 20;
+ const targetUrl = params?.url || 'https://www.farrow-ball.com/us/wallpaper/all-patterns';
+
+ console.log('🎨 Farrow & Ball feed-first (Magento SSR) collections scrape:', targetUrl);
+
+ const res = await fetch(targetUrl, {
+ headers: { 'User-Agent': FB_UA, Accept: 'text/html' },
+ });
+ if (!res.ok) {
+ console.error(`❌ Farrow & Ball page fetch failed: HTTP ${res.status}`);
+ return [];
+ }
+ const html = await res.text();
+
+ const now = new Date().toISOString();
+ const seen = new Set<string>();
+ const products: FarrowBallCollectionProduct[] = [];
+
+ // Each Magento product card opens with <li ...>. Slice on it and parse fields.
+ const segments = html.split('<li');
+
+ for (const seg of segments) {
+ if (products.length >= maxProducts) break;
+ if (!seg.includes('product-item-link')) continue;
+
+ const linkM = seg.match(/<a[^>]*class="product-item-link"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/);
+ if (!linkM) continue;
+ const href = decodeEntities(linkM[1]);
+ let name = decodeEntities(linkM[2].replace(/<[^>]*>/g, ' '));
+
+ const codeM = seg.match(/class="product-item-code-value"[^>]*>\s*([^<]+)</);
+ const code = codeM ? decodeEntities(codeM[1]) : '';
+
+ // alt holds "Pattern XX-YY" — the F&B colour reference suffix.
+ const altM = seg.match(/alt="([^"]+)"/);
+ const alt = altM ? decodeEntities(altM[1]) : '';
+ // The human colour name lives on the PDP; use the numeric reference as the
+ // colour token here (always better than blank, and re-resolvable per-PDP).
+ let color = '';
+ const refM = alt.match(/\b(\d{2}-?\d{2})\b/) || (code ? [null as any, code] : null);
+ if (refM) color = String(refM[1]).replace(/^(\d{2})(\d{2})$/, '$1-$2');
+
+ if (!name) name = alt.replace(/\s*\d{2}-?\d{2}\s*$/, '').trim();
+ if (!name) continue;
+
+ const imgM = seg.match(/<img[^>]*src="([^"]+)"/);
+ let image = imgM ? decodeEntities(imgM[1]) : undefined;
+ if (image && image.startsWith('//')) image = 'https:' + image;
+
+ const sku = code || (href.split('/').filter(Boolean).pop() || name).toUpperCase();
+ const dedupeKey = href || sku;
+ if (seen.has(dedupeKey)) continue;
+ seen.add(dedupeKey);
+
+ products.push({
+ name,
+ title: name,
+ pattern: name,
+ color,
+ sku,
+ code: sku,
+ url: href,
+ href,
+ image,
+ imageUrl: image,
+ vendor: 'Farrow & Ball',
+ vendor_id: 'farrow-ball',
+ category: 'Wallcovering',
+ collection: 'Farrow & Ball Wallpaper',
+ source: 'magento-ssr-html',
+ extracted_at: now,
+ });
}
-};
-// Export default for direct execution
-export default farrowballCollectionsSkill;
+ console.log(`✅ Farrow & Ball returned ${products.length} real products`);
+ if (products.length === 0) return [];
-// Allow direct execution
+ const collection: FarrowBallCollection = {
+ name: 'Farrow & Ball Wallpaper',
+ title: 'Farrow & Ball Wallpaper',
+ url: targetUrl,
+ href: targetUrl,
+ productCount: products.length,
+ image: products[0].image,
+ products,
+ vendor: 'Farrow & Ball',
+ vendor_id: 'farrow-ball',
+ extracted_at: now,
+ };
+ console.log('📦 Grouped into 1 collection(s)');
+ return [collection];
+}
+
+// Export for direct testing
if (require.main === module) {
- console.log('🚀 Running Farrow Ball Collections skill directly...\n');
-
- runFarrowBallCollectionsSkill({
- maxCollections: 10
- }).then(result => {
- console.log('\n✨ Farrow Ball Collections skill completed successfully!');
- process.exit(0);
- }).catch(error => {
- console.error('Fatal error:', error);
- process.exit(1);
- });
-}
\ No newline at end of file
+ (async () => {
+ const collections = await scrapeFarrowBallCollections({ maxResults: 10 });
+ console.log('\n=== FARROW & BALL COLLECTIONS ===\n');
+ collections.forEach((c, i) => {
+ console.log(`${i + 1}. ${c.name} (${c.products.length})`);
+ c.products.slice(0, 3).forEach(p => console.log(` - ${p.name} | ${p.color} | ${p.sku}`));
+ });
+ })();
+}
← 6e401a7d feat(designers-guild): feed-first collection extraction retu
·
back to Designer Wallcoverings
·
feat(dedar): feed-first collection extraction returns real p 06ea863b →