#!/usr/bin/env node // Site Inspector - compact DOM pattern discovery for casino listing pages // Usage: node site-inspector.js "" const puppeteer = require('puppeteer-extra'); puppeteer.use(require('puppeteer-extra-plugin-stealth')()); (async () => { var url = process.argv[2]; if (!url) { console.log('Usage: node site-inspector.js ""'); process.exit(1); } var b = await puppeteer.launch({ headless: 'new', executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', args: ['--no-sandbox'] }); var p = await b.newPage(); await p.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'); console.error('\n>> Loading: ' + url); await p.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 }); await new Promise(r => setTimeout(r, 4000)); var title = await p.title(); console.log('\n=== Title: ' + title.substring(0, 80) + '\n'); var HOST = url.replace(/https?:\/\//, '').split('/')[0].replace('www.', ''); // --- Phase 1: find card-like containers with imgs + off-site links --- var cards = await p.evaluate(function (h) { var res = []; var main = document.querySelector('main, article, .content, .container, [class*="wrapper"]') || document.body; // look at children of main for card-level divs var candidates = []; var level1 = main.querySelectorAll(':scope > [class]'); level1.forEach(function (d) { candidates.push(d); }); if (candidates.length < 3) { document.querySelectorAll('[class]').forEach(function (d) { candidates.push(d); }); } for (var i = 0; i < candidates.length && res.length < 20; i++) { var card = candidates[i]; var imgs = card.querySelectorAll('img[alt]'); var alts = []; for (var j = 0; j < imgs.length; j++) { var a = (imgs[j].alt || '').trim(); if (a.length >= 2 && a.length < 80) alts.push(a); } if (!alts.length) continue; // check for off-site links inside card var extOrigins = []; var seen = {}; var links = card.querySelectorAll('a[href]'); for (var k = 0; k < links.length && extOrigins.length < 5; k++) { try { var u = new URL(links[k].href, document.baseURI); var host = u.hostname.replace('www.', ''); if (host !== h && !/google|facebook|twitter|instagram/i.test(host) && !seen[host]) { extOrigins.push(u.origin); seen[host] = true; } } catch (e) {} } var hasBonusText = /(?:\d+\s*free\s*spins|FS|[£€$]\d+)/.test(card.textContent || ''); res.push({ cls: String(card.className).substring(0, 80), tag: card.tagName, alts: alts.slice(0, 6), ext: extOrigins.slice(0, 3), bonus: hasBonusText }); } return res; }, HOST); // --- Phase 2: top alt-text patterns across page --- var allAlts = await p.evaluate(function () { var counts = {}; document.querySelectorAll('img[alt]').forEach(function (img) { var a = (img.alt || '').trim(); if (a.length >= 2 && a.length < 80) counts[a] = (counts[a] || 0) + 1; }); var entries = Object.entries(counts); entries.sort(function (a, b) { return b[1] - a[1]; }); return entries.slice(0, 25); }); // --- Output --- if (cards.length) { console.log('=== CARD CONTAINERS ===\n'); for (var c = 0; c < cards.length; c++) { var cd = cards[c]; console.log('Card ' + (c + 1) + ': <' + cd.tag + ' class="' + cd.cls + '">'); console.log(' imgs: ' + JSON.stringify(cd.alts)); if (cd.ext.length) console.log(' ext: ' + cd.ext.join(', ')); if (cd.bonus) console.log(' bonus: text found'); console.log(''); } } else { console.log('(no card containers detected)\n'); } if (allAlts.length) { console.log('=== ALT TEXT PATTERNS ===\n'); for (var k = 0; k < allAlts.length; k++) { var t = allAlts[k][0], n = allAlts[k][1]; var mark = /casino/i.test(t) ? ' *CASINO*' : ''; console.log(' x' + String(n).padStart(3, ' ') + ' "' + t + '"' + mark); } } // --- recommended selector hints --- console.log('\n=== HINTS ===\n'); var casinoCards = cards.filter(function (cd) { return cd.alts.some(function (a) { return /casino/i.test(a); }); }); if (casinoCards.length) { var clsArr = casinoCards[0].cls.trim().split(/\s+/); console.log('Card class hint: .' + clsArr[0]); if (clsArr.length > 1) console.log('Full class: ' + casinoCards[0].cls); console.log('Has ext links: ' + !!casinoCards[0].ext.length); var sampleAlt = casinoCards[0].alts.filter(function (a) { return /casino/i.test(a); })[0] || ''; if (sampleAlt) { console.log('Sample alt: "' + sampleAlt + '"'); // suggest pattern var stripped = sampleAlt.replace(/\s+C[aA]sino\b.*/i, ''); console.log('Brand would be: "' + stripped + '"'); } } else { console.log('No casino-card containers found.'); console.log('Check alt-text list for patterns.'); } await p.close(); await b.close(); })();