// Usage: node render-page.cjs // Печатает JSON: { html, visiblePhones: [...] } — отрендеренный DOM и видимые tel:-номера. const { chromium } = require('playwright'); (async () => { const url = process.argv[2]; // Защита от SSRF/локального доступа: только http/https. let parsed; try { parsed = new URL(url); } catch (e) { console.error('bad url'); process.exit(2); } if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { console.error('bad scheme'); process.exit(2); } const browser = await chromium.launch({ headless: true }); try { const page = await browser.newPage(); // 'domcontentloaded' вместо 'networkidle': «шумные» сайты (аналитика, чат-виджеты, коллтрекинг) // никогда не «затихают» — networkidle упирался в 30с и рендер падал по таймауту, обнуляя весь сбор // номеров с сайта. Берём DOM рано, затем короткая пауза, чтобы скрипты коллтрекинга // (calltouch/comagic/…) успели подставить свой номер в видимые tel:-ссылки. try { await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 12000 }); } catch (e) { // даже если навигация не уложилась в таймаут — забираем то, что успело отрендериться } await page.waitForTimeout(2500); const html = await page.content(); const visiblePhones = await page.$$eval('a[href^="tel:"]', els => els.map(e => (e.textContent || '').trim()).filter(Boolean) ); process.stdout.write(JSON.stringify({ html, visiblePhones })); } finally { await browser.close(); } })().catch(e => { console.error(String(e)); process.exit(1); });