n8n-nodes-seo-scanner 1.2.1 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,41 +32,90 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
36
39
  exports.SeoScanner = void 0;
37
40
  const n8n_workflow_1 = require("n8n-workflow");
38
41
  const cheerio = __importStar(require("cheerio"));
39
42
  const dns = __importStar(require("dns"));
40
43
  const tls = __importStar(require("tls"));
44
+ const puppeteer_1 = __importDefault(require("puppeteer"));
41
45
  const reportTemplate_1 = require("./reportTemplate");
42
46
  const dnsPromises = dns.promises;
43
47
  const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
44
48
  const GOOGLEBOT_UA = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)';
45
49
  async function fetchPage(url, timeoutMs, opts = {}) {
46
- const { userAgent = USER_AGENT, followRedirects = true, acceptLanguage } = opts;
47
- const controller = new AbortController();
48
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
50
+ const { userAgent = USER_AGENT, followRedirects = true, acceptLanguage, useJsRendering = false } = opts;
49
51
  const start = Date.now();
50
- const headers = { 'User-Agent': userAgent };
51
- if (acceptLanguage)
52
- headers['Accept-Language'] = acceptLanguage;
53
- try {
54
- const res = await fetch(url, {
55
- headers,
56
- redirect: followRedirects ? 'follow' : 'manual',
57
- signal: controller.signal,
58
- });
59
- const timeToFirstByteMs = Date.now() - start;
60
- const html = await res.text();
61
- clearTimeout(timeout);
62
- const responseTimeMs = Date.now() - start;
63
- const responseHeaders = {};
64
- res.headers.forEach((value, key) => { responseHeaders[key.toLowerCase()] = value; });
65
- return { html, finalUrl: res.url, statusCode: res.status, responseTimeMs, timeToFirstByteMs, responseHeaders };
52
+ if (useJsRendering) {
53
+ let browser;
54
+ try {
55
+ browser = await puppeteer_1.default.launch({
56
+ headless: true,
57
+ args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
58
+ });
59
+ const page = await browser.newPage();
60
+ if (userAgent)
61
+ await page.setUserAgent(userAgent);
62
+ if (acceptLanguage)
63
+ await page.setExtraHTTPHeaders({ 'Accept-Language': acceptLanguage });
64
+ let timeToFirstByteMs = 0;
65
+ page.on('response', (response) => {
66
+ if (response.url() === page.url()) {
67
+ timeToFirstByteMs = Date.now() - start;
68
+ }
69
+ });
70
+ const response = await page.goto(url, {
71
+ waitUntil: 'networkidle2',
72
+ timeout: timeoutMs,
73
+ });
74
+ if (!response) {
75
+ throw new Error('No se recibió respuesta al renderizar la página.');
76
+ }
77
+ const html = await page.content();
78
+ const finalUrl = page.url();
79
+ const statusCode = response.status();
80
+ const responseTimeMs = Date.now() - start;
81
+ const headers = response.headers();
82
+ const responseHeaders = {};
83
+ for (const key in headers) {
84
+ responseHeaders[key.toLowerCase()] = headers[key];
85
+ }
86
+ await browser.close();
87
+ return { html, finalUrl, statusCode, responseTimeMs, timeToFirstByteMs: timeToFirstByteMs || responseTimeMs, responseHeaders };
88
+ }
89
+ catch (e) {
90
+ if (browser)
91
+ await browser.close();
92
+ throw e;
93
+ }
66
94
  }
67
- catch (e) {
68
- clearTimeout(timeout);
69
- throw e;
95
+ else {
96
+ const controller = new AbortController();
97
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
98
+ const headers = { 'User-Agent': userAgent };
99
+ if (acceptLanguage)
100
+ headers['Accept-Language'] = acceptLanguage;
101
+ try {
102
+ const res = await fetch(url, {
103
+ headers,
104
+ redirect: followRedirects ? 'follow' : 'manual',
105
+ signal: controller.signal,
106
+ });
107
+ const timeToFirstByteMs = Date.now() - start;
108
+ const html = await res.text();
109
+ clearTimeout(timeout);
110
+ const responseTimeMs = Date.now() - start;
111
+ const responseHeaders = {};
112
+ res.headers.forEach((value, key) => { responseHeaders[key.toLowerCase()] = value; });
113
+ return { html, finalUrl: res.url, statusCode: res.status, responseTimeMs, timeToFirstByteMs, responseHeaders };
114
+ }
115
+ catch (e) {
116
+ clearTimeout(timeout);
117
+ throw e;
118
+ }
70
119
  }
71
120
  }
72
121
  function parseColorToRgb(color) {
@@ -2155,17 +2204,17 @@ ${reportTemplate_1.REPORT_CSS}
2155
2204
  html += `
2156
2205
  <details class="fallos-pagina-details" data-crit="${rcrit.length}" data-imp="${rimp.length}" data-warn="${rwarn.length}" data-info="${rinfo.length}" data-pass="${rpassed.length}">
2157
2206
  <summary class="fallos-pagina-summary">
2158
- <span class="fallos-pagina-label">${escapeHtml(label)}</span>
2159
- <div style="display:flex; height:6px; width:60px; background:var(--bg3); border-radius:3px; overflow:hidden; margin-right:12px;">
2207
+ <span class="fallos-pagina-label" style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escapeHtml(label)}</span>
2208
+ <div style="display:flex; height:6px; width:60px; background:var(--bg3); border-radius:3px; overflow:hidden; flex-shrink:0">
2160
2209
  <div style="width:${pctCrit}%; background:var(--red);"></div>
2161
2210
  <div style="width:${pctImp}%; background:var(--orange);"></div>
2162
2211
  <div style="width:${pctWarn}%; background:var(--yellow);"></div>
2163
2212
  <div style="width:${pctInfo}%; background:var(--blue);"></div>
2164
2213
  <div style="width:${pctPass}%; background:var(--green);"></div>
2165
2214
  </div>
2166
- <span class="fallos-pagina-score ${scoreCls}">${sc}</span>
2167
- <span class="fallos-pagina-count">${totalFallos} fallos</span>
2168
- <button class="ignore-btn" title="Copiar URL para ignorarla y ocultarla de la lista" onclick="event.preventDefault(); window.ignorePage(this, '${attrEsc(label.split('·')[1]?.trim() || label)}');">Ignorar</button>
2215
+ <span class="fallos-pagina-score ${scoreCls}" style="flex-shrink:0">${sc}</span>
2216
+ <span class="fallos-pagina-count" style="flex-shrink:0">${totalFallos} fallos</span>
2217
+ <button class="ignore-btn" style="margin:0;flex-shrink:0" title="Copiar URL para ignorarla y ocultarla de la lista" onclick="event.preventDefault(); window.ignorePage(this, '${attrEsc(label.split('·')[1]?.trim() || label)}');">Ignorar</button>
2169
2218
  </summary>
2170
2219
  <div class="fallos-pagina-body">`;
2171
2220
  [['Critical', rcrit, 'crit', 'tag-crit', 'CRIT'], ['Important', rimp, 'imp', 'tag-imp', 'IMP'], ['Warnings', rwarn, 'warn', 'tag-warn', 'WARN'], ['Info', rinfo, 'info', 'tag-info', 'INFO']].forEach(([title, arr, rowClass, tagCls, lbl]) => {
@@ -2288,16 +2337,16 @@ ${reportTemplate_1.REPORT_CSS}
2288
2337
  const srcShort = img.src ? escapeHtml(img.src.replace(/^https?:\/\/[^/]+/, '').slice(0, 60)) : '—';
2289
2338
  const dims = [img.width, img.height].filter(Boolean).join('×') || '—';
2290
2339
  html += `
2291
- <details class="metric-url-row" style="margin:6px 0;padding:8px 10px;background:var(--bg2);border-radius:4px">
2292
- <summary style="cursor:pointer;font-family:var(--mono);font-size:0.72rem">
2293
- <span class="metric-url-idx">${String(i + 1).padStart(2, '0')}</span>
2294
- <span class="metric-v ${img.hasAlt ? 'ok' : ''}" style="margin-left:6px">${img.hasAlt ? 'alt ✓' : 'sin alt'}</span>
2295
- <span style="color:var(--text3);margin-left:6px">${srcShort}${img.src && img.src.length > 60 ? '…' : ''}</span>
2340
+ <details class="metric-url-row" style="margin:6px 0;padding:8px 10px;background:var(--bg2);border-radius:4px;min-width:0">
2341
+ <summary style="cursor:pointer;font-family:var(--mono);font-size:0.72rem;display:flex;align-items:center;min-width:0">
2342
+ <span class="metric-url-idx" style="flex-shrink:0">${String(i + 1).padStart(2, '0')}</span>
2343
+ <span class="metric-v ${img.hasAlt ? 'ok' : ''}" style="margin-left:6px;flex-shrink:0">${img.hasAlt ? 'alt ✓' : 'sin alt'}</span>
2344
+ <span style="color:var(--text3);margin-left:6px;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${srcShort}${img.src && img.src.length > 60 ? '…' : ''}</span>
2296
2345
  </summary>
2297
- <div style="margin-top:8px;padding-top:8px;border-top:1px solid var(--line);font-size:0.7rem;color:var(--text2)">
2298
- <div class="metric-row"><span class="metric-k">src</span><span style="word-break:break-all">${escapeHtml((img.src || '').slice(0, 120))}${(img.src || '').length > 120 ? '…' : ''}</span></div>
2299
- <div class="metric-row"><span class="metric-k">alt</span><span>${altStr}</span></div>
2300
- <div class="metric-row"><span class="metric-k">dimensions</span><span>${escapeHtml(dims)}</span></div>
2346
+ <div style="margin-top:8px;padding-top:8px;border-top:1px solid var(--line);font-size:0.7rem;color:var(--text2);min-width:0">
2347
+ <div class="metric-row"><span class="metric-k" style="flex-shrink:0;margin-right:8px">src</span><span style="word-break:break-all;flex:1;min-width:0">${escapeHtml((img.src || '').slice(0, 120))}${(img.src || '').length > 120 ? '…' : ''}</span></div>
2348
+ <div class="metric-row"><span class="metric-k" style="flex-shrink:0;margin-right:8px">alt</span><span style="word-break:break-word;flex:1;min-width:0">${altStr}</span></div>
2349
+ <div class="metric-row"><span class="metric-k" style="flex-shrink:0;margin-right:8px">dimensions</span><span style="flex:1;min-width:0">${escapeHtml(dims)}</span></div>
2301
2350
  </div>
2302
2351
  </details>`;
2303
2352
  });
@@ -2338,22 +2387,22 @@ ${reportTemplate_1.REPORT_CSS}
2338
2387
  const hasMeta = hasInternalDetails && 'title' in item;
2339
2388
  if (hasMeta) {
2340
2389
  const linkItem = item;
2341
- html += `<details class="metric-url-row" style="margin:4px 0">
2342
- <summary style="cursor:pointer;display:flex;align-items:center;gap:6px">
2343
- <span class="metric-url-idx">${String(i + 1).padStart(2, '0')}</span>
2390
+ html += `<details class="metric-url-row" style="margin:4px 0;min-width:0">
2391
+ <summary style="cursor:pointer;display:flex;align-items:center;gap:6px;min-width:0">
2392
+ <span class="metric-url-idx" style="flex-shrink:0">${String(i + 1).padStart(2, '0')}</span>
2344
2393
  <a class="metric-url" href="${escapeHtml(item.url)}" target="_blank" rel="noopener" onclick="event.stopPropagation()">${escapeHtml(pathPart)}</a>
2345
- <span class="metric-url-type">int</span>
2346
- ${(linkItem.statusCode ?? 0) >= 400 ? '<span style="color:var(--red)">' + (linkItem.statusCode || '') + '</span>' : ''}
2394
+ <span class="metric-url-type" style="flex-shrink:0">int</span>
2395
+ ${(linkItem.statusCode ?? 0) >= 400 ? '<span style="color:var(--red);flex-shrink:0">' + (linkItem.statusCode || '') + '</span>' : ''}
2347
2396
  </summary>
2348
- <div style="margin:6px 0 6px 20px;padding:6px 8px;background:var(--bg2);border-radius:4px;font-size:0.7rem">
2349
- ${linkItem.anchorText ? `<div class="metric-row"><span class="metric-k">anchor</span><span>${escapeHtml(linkItem.anchorText.slice(0, 80))}</span></div>` : ''}
2350
- <div class="metric-row"><span class="metric-k">title</span><span>${linkItem.title ? escapeHtml(linkItem.title.slice(0, 100)) : '—'}</span></div>
2351
- <div class="metric-row"><span class="metric-k">meta</span><span>${linkItem.metaDescription ? escapeHtml(linkItem.metaDescription.slice(0, 120)) + (linkItem.metaDescription.length > 120 ? '…' : '') : '—'}</span></div>
2397
+ <div style="margin:6px 0 6px 20px;padding:6px 8px;background:var(--bg2);border-radius:4px;font-size:0.7rem;min-width:0">
2398
+ ${linkItem.anchorText ? `<div class="metric-row"><span class="metric-k" style="flex-shrink:0;margin-right:8px">anchor</span><span style="flex:1;min-width:0;word-break:break-word">${escapeHtml(linkItem.anchorText.slice(0, 80))}</span></div>` : ''}
2399
+ <div class="metric-row"><span class="metric-k" style="flex-shrink:0;margin-right:8px">title</span><span style="flex:1;min-width:0;word-break:break-word">${linkItem.title ? escapeHtml(linkItem.title.slice(0, 100)) : '—'}</span></div>
2400
+ <div class="metric-row"><span class="metric-k" style="flex-shrink:0;margin-right:8px">meta</span><span style="flex:1;min-width:0;word-break:break-word">${linkItem.metaDescription ? escapeHtml(linkItem.metaDescription.slice(0, 120)) + (linkItem.metaDescription.length > 120 ? '…' : '') : '—'}</span></div>
2352
2401
  </div>
2353
2402
  </details>`;
2354
2403
  }
2355
2404
  else {
2356
- html += `<div class="metric-url-row"><span class="metric-url-idx">${String(i + 1).padStart(2, '0')}</span><a class="metric-url" href="${escapeHtml(item.url)}" target="_blank" rel="noopener">${escapeHtml(pathPart)}</a><span class="metric-url-type">int</span></div>`;
2405
+ html += `<div class="metric-url-row" style="min-width:0"><span class="metric-url-idx" style="flex-shrink:0">${String(i + 1).padStart(2, '0')}</span><a class="metric-url" href="${escapeHtml(item.url)}" target="_blank" rel="noopener">${escapeHtml(pathPart)}</a><span class="metric-url-type" style="flex-shrink:0">int</span></div>`;
2357
2406
  }
2358
2407
  });
2359
2408
  if (pIntLinks.length > maxShow)
@@ -2388,23 +2437,23 @@ ${reportTemplate_1.REPORT_CSS}
2388
2437
  if (hasMeta) {
2389
2438
  const linkItem = item;
2390
2439
  const urlShort = linkItem.url.length > 50 ? linkItem.url.slice(0, 50) + '…' : linkItem.url;
2391
- html += `<details class="metric-url-row" style="margin:4px 0">
2392
- <summary style="cursor:pointer;display:flex;align-items:center;gap:6px;flex-wrap:wrap">
2393
- <span class="metric-url-idx">${String(i + 1).padStart(2, '0')}</span>
2440
+ html += `<details class="metric-url-row" style="margin:4px 0;min-width:0">
2441
+ <summary style="cursor:pointer;display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0">
2442
+ <span class="metric-url-idx" style="flex-shrink:0">${String(i + 1).padStart(2, '0')}</span>
2394
2443
  <a class="metric-url" href="${escapeHtml(linkItem.url)}" target="_blank" rel="noopener" onclick="event.stopPropagation()">${escapeHtml(urlShort)}</a>
2395
- <span class="metric-url-type">ext</span>
2396
- ${(linkItem.statusCode ?? 0) >= 400 ? '<span style="color:var(--red)">' + (linkItem.statusCode || '') + '</span>' : ''}
2444
+ <span class="metric-url-type" style="flex-shrink:0">ext</span>
2445
+ ${(linkItem.statusCode ?? 0) >= 400 ? '<span style="color:var(--red);flex-shrink:0">' + (linkItem.statusCode || '') + '</span>' : ''}
2397
2446
  </summary>
2398
- <div style="margin:6px 0 6px 20px;padding:6px 8px;background:var(--bg2);border-radius:4px;font-size:0.7rem">
2399
- ${linkItem.anchorText ? `<div class="metric-row"><span class="metric-k">anchor</span><span>${escapeHtml(linkItem.anchorText.slice(0, 80))}</span></div>` : ''}
2400
- <div class="metric-row"><span class="metric-k">title</span><span>${linkItem.title ? escapeHtml(linkItem.title.slice(0, 100)) : '—'}</span></div>
2401
- <div class="metric-row"><span class="metric-k">meta</span><span>${linkItem.metaDescription ? escapeHtml(linkItem.metaDescription.slice(0, 120)) + (linkItem.metaDescription.length > 120 ? '…' : '') : '—'}</span></div>
2447
+ <div style="margin:6px 0 6px 20px;padding:6px 8px;background:var(--bg2);border-radius:4px;font-size:0.7rem;min-width:0">
2448
+ ${linkItem.anchorText ? `<div class="metric-row"><span class="metric-k" style="flex-shrink:0;margin-right:8px">anchor</span><span style="flex:1;min-width:0;word-break:break-word">${escapeHtml(linkItem.anchorText.slice(0, 80))}</span></div>` : ''}
2449
+ <div class="metric-row"><span class="metric-k" style="flex-shrink:0;margin-right:8px">title</span><span style="flex:1;min-width:0;word-break:break-word">${linkItem.title ? escapeHtml(linkItem.title.slice(0, 100)) : '—'}</span></div>
2450
+ <div class="metric-row"><span class="metric-k" style="flex-shrink:0;margin-right:8px">meta</span><span style="flex:1;min-width:0;word-break:break-word">${linkItem.metaDescription ? escapeHtml(linkItem.metaDescription.slice(0, 120)) + (linkItem.metaDescription.length > 120 ? '…' : '') : '—'}</span></div>
2402
2451
  </div>
2403
2452
  </details>`;
2404
2453
  }
2405
2454
  else {
2406
2455
  const u = typeof item === 'object' && item && 'url' in item ? item.url : String(item);
2407
- html += `<div class="metric-url-row"><span class="metric-url-idx">${String(i + 1).padStart(2, '0')}</span><a class="metric-url" href="${escapeHtml(u)}" target="_blank" rel="noopener">${escapeHtml(String(u).length > 50 ? String(u).slice(0, 50) + '…' : u)}</a><span class="metric-url-type">ext</span></div>`;
2456
+ html += `<div class="metric-url-row" style="min-width:0"><span class="metric-url-idx" style="flex-shrink:0">${String(i + 1).padStart(2, '0')}</span><a class="metric-url" href="${escapeHtml(u)}" target="_blank" rel="noopener">${escapeHtml(String(u).length > 50 ? String(u).slice(0, 50) + '…' : u)}</a><span class="metric-url-type" style="flex-shrink:0">ext</span></div>`;
2408
2457
  }
2409
2458
  });
2410
2459
  if (pExtLinks.length > maxShow)
@@ -3297,6 +3346,13 @@ class SeoScanner {
3297
3346
  default: false,
3298
3347
  description: 'Genera un informe HTML (dashboard) con los fallos y recomendaciones. Salida: json.html y json.reportHtml. Para verlo en el navegador: conecta a Respond to Webhook (Respond With = Text, Response Body = {{ $json.html }}) y abre la URL del webhook.',
3299
3348
  },
3349
+ {
3350
+ displayName: 'Usar JavaScript Rendering',
3351
+ name: 'useJsRendering',
3352
+ type: 'boolean',
3353
+ default: false,
3354
+ description: 'Si está activado, usa un navegador invisible (Puppeteer) para cargar la página. Necesario para webs React, Vue o Angular que renderizan meta etiquetas y contenido dinámicamente. Es más lento que la petición HTTP normal.',
3355
+ },
3300
3356
  {
3301
3357
  displayName: 'Opciones de detalle',
3302
3358
  name: 'detailOptions',
@@ -3407,6 +3463,7 @@ class SeoScanner {
3407
3463
  const timeoutMs = timeoutSeconds * 1000;
3408
3464
  const followRedirects = this.getNodeParameter('followRedirects', 0) !== false;
3409
3465
  const acceptLanguage = (this.getNodeParameter('acceptLanguage', 0) || '').trim() || undefined;
3466
+ const useJsRendering = this.getNodeParameter('useJsRendering', 0) ?? false;
3410
3467
  let userAgent = USER_AGENT;
3411
3468
  const simulateGooglebot = this.getNodeParameter('simulateGooglebot', 0) ?? false;
3412
3469
  const customUserAgent = this.getNodeParameter('customUserAgent', 0) ?? false;
@@ -3422,7 +3479,7 @@ class SeoScanner {
3422
3479
  catch {
3423
3480
  }
3424
3481
  }
3425
- const fetchOpts = { userAgent, followRedirects, acceptLanguage };
3482
+ const fetchOpts = { userAgent, followRedirects, acceptLanguage, useJsRendering };
3426
3483
  const generateHtmlReport = this.getNodeParameter('generateHtmlReport', 0) ?? false;
3427
3484
  const detailOpts = this.getNodeParameter('detailOptions', 0) || {};
3428
3485
  const ignoredIssues = (this.getNodeParameter('ignoredIssues', 0, '') || '').split('\n').map(s => s.trim()).filter(Boolean);