n8n-nodes-seo-scanner 1.2.1 → 1.2.2
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.
- package/dist/SeoScanner.node.js +109 -52
- package/dist/SeoScanner.node.js.map +1 -1
- package/dist/nodes/SeoScanner/SeoScanner.node.js +109 -52
- package/dist/nodes/SeoScanner/reportTemplate.js +7 -6
- package/dist/reportTemplate.d.ts +1 -1
- package/dist/reportTemplate.js +7 -6
- package/dist/reportTemplate.js.map +1 -1
- package/package.json +4 -2
package/dist/SeoScanner.node.js
CHANGED
|
@@ -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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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) {
|
|
@@ -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);
|