[object Object]

← 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

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(/&amp;/g, '&')
+    .replace(/&#x20;/g, ' ')
+    .replace(/&#x27;|&#39;/g, "'")
+    .replace(/&quot;/g, '"')
+    .replace(/&lt;/g, '<')
+    .replace(/&gt;/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 →