2026-06-30 10:42:08 +03:00
|
|
|
// Usage: node render-page.cjs <url>
|
|
|
|
|
// Печатает JSON: { html, visiblePhones: [...] } — отрендеренный DOM и видимые tel:-номера.
|
|
|
|
|
const { chromium } = require('playwright');
|
|
|
|
|
|
|
|
|
|
(async () => {
|
|
|
|
|
const url = process.argv[2];
|
2026-06-30 10:45:14 +03:00
|
|
|
// Защита от 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);
|
|
|
|
|
}
|
2026-06-30 10:42:08 +03:00
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
|
|
|
try {
|
|
|
|
|
const page = await browser.newPage();
|
2026-07-07 18:16:42 +03:00
|
|
|
// 'domcontentloaded' вместо 'networkidle': «шумные» сайты (аналитика, чат-виджеты, коллтрекинг)
|
|
|
|
|
// никогда не «затихают» — networkidle упирался в 30с и рендер падал по таймауту, обнуляя весь сбор
|
|
|
|
|
// номеров с сайта. Берём DOM рано, затем короткая пауза, чтобы скрипты коллтрекинга
|
|
|
|
|
// (calltouch/comagic/…) успели подставить свой номер в видимые tel:-ссылки.
|
|
|
|
|
try {
|
|
|
|
|
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 12000 });
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// даже если навигация не уложилась в таймаут — забираем то, что успело отрендериться
|
|
|
|
|
}
|
|
|
|
|
await page.waitForTimeout(2500);
|
2026-06-30 10:42:08 +03:00
|
|
|
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); });
|