@detiq/crawler 1.1.9 → 1.1.11
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/accessibility-auditor.js +54 -74
- package/dist/ai-login.js +227 -249
- package/dist/auth-token-rotator.js +57 -67
- package/dist/auth.js +299 -336
- package/dist/browser-extension.js +104 -142
- package/dist/captcha-solver.js +98 -125
- package/dist/color-contrast.js +77 -89
- package/dist/content-chunker.js +126 -126
- package/dist/content-dedup.js +54 -71
- package/dist/cookie-consent.js +68 -83
- package/dist/cookie-jar.js +53 -60
- package/dist/crawler.js +4494 -5353
- package/dist/custom-js-injector.js +45 -61
- package/dist/dark-mode-capture.js +67 -81
- package/dist/dead-link-detector.js +47 -55
- package/dist/element-hash.js +8 -12
- package/dist/email-otp.js +154 -195
- package/dist/engine-router.js +87 -98
- package/dist/export/bulk-export.js +70 -55
- package/dist/export/vector-export.js +77 -105
- package/dist/extractors/advanced.js +249 -266
- package/dist/extractors/artifact-envelope.js +59 -58
- package/dist/failure-analysis-agent.js +37 -33
- package/dist/feature-flag-detector.js +51 -66
- package/dist/form-detector.js +55 -75
- package/dist/frontier.js +76 -101
- package/dist/gap-pass.js +68 -89
- package/dist/graphql-detector.js +62 -72
- package/dist/http-engine.js +282 -330
- package/dist/incremental-diff.js +54 -87
- package/dist/index.js +67 -28
- package/dist/input-fuzzer.js +93 -97
- package/dist/interactive-probe.js +715 -827
- package/dist/job-queue.js +2151 -2169
- package/dist/js-coverage.js +52 -56
- package/dist/js-router-detector.js +86 -117
- package/dist/language-detector.js +51 -63
- package/dist/link-classifier.js +32 -50
- package/dist/live-view.js +91 -108
- package/dist/locator-extractor.js +151 -187
- package/dist/logger.js +9 -7
- package/dist/markdown-extract.js +48 -61
- package/dist/native-crawler.js +780 -946
- package/dist/network-observer.js +135 -156
- package/dist/network-throttle.js +29 -40
- package/dist/openapi-generator.js +77 -92
- package/dist/pdf-extractor.js +72 -90
- package/dist/performance-metrics.js +80 -102
- package/dist/phash.js +125 -144
- package/dist/pii-scrubber.js +65 -68
- package/dist/playwright-recorder.js +70 -58
- package/dist/policies/crawl-safety.js +31 -34
- package/dist/profiles/domain-profile.js +130 -125
- package/dist/proxy.js +146 -172
- package/dist/rate-limiter.js +58 -65
- package/dist/redirect-tracker.js +60 -64
- package/dist/redis-frontier.js +116 -136
- package/dist/redis-queue.js +157 -144
- package/dist/response-cache.js +127 -140
- package/dist/responsive-detector.js +42 -46
- package/dist/retry-policy.js +46 -50
- package/dist/robots-txt.js +69 -83
- package/dist/screen-reader.js +45 -70
- package/dist/screenshot-masker.js +59 -77
- package/dist/seo-checker.js +88 -107
- package/dist/session-capture.js +192 -212
- package/dist/session-export.js +107 -133
- package/dist/sitemap-writer.js +38 -41
- package/dist/sitemap.js +141 -202
- package/dist/skyvern.js +123 -148
- package/dist/sqs-queue.js +42 -42
- package/dist/sso-token.js +74 -90
- package/dist/stealth/human-behavior.js +156 -173
- package/dist/telemetry.js +68 -82
- package/dist/test-executor.js +309 -335
- package/dist/tls-fingerprint.js +77 -88
- package/dist/url-pattern-completer.js +71 -90
- package/dist/visual-diff-verdict.js +23 -38
- package/dist/webhook-notifier.js +71 -69
- package/dist/websocket-detector.js +56 -68
- package/dist/worker.js +29 -38
- package/package.json +1 -1
- package/src/job-queue.ts +136 -0
package/dist/screen-reader.js
CHANGED
|
@@ -1,72 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
'').substring(0, 80);
|
|
35
|
-
// Skip empty, noise patterns, or very long labels (likely container text)
|
|
36
|
-
if (!raw || noiseLabel.test(raw) || raw.length > 70)
|
|
37
|
-
return;
|
|
38
|
-
// Deduplicate by role+normalised label
|
|
39
|
-
const key = `${role}:${raw.toLowerCase()}`;
|
|
40
|
-
if (seen.has(key))
|
|
41
|
-
return;
|
|
42
|
-
seen.add(key);
|
|
43
|
-
let expectedData;
|
|
44
|
-
if (tag === 'input') {
|
|
45
|
-
const input = el;
|
|
46
|
-
if (input.type === 'email' || /email/i.test(raw))
|
|
47
|
-
expectedData = 'valid_email';
|
|
48
|
-
else if (input.type === 'password' || /password/i.test(raw))
|
|
49
|
-
expectedData = 'password';
|
|
50
|
-
else if (input.type === 'tel' || /phone|mobile/i.test(raw))
|
|
51
|
-
expectedData = 'phone';
|
|
52
|
-
else if (input.type === 'date' || /\bdate\b/i.test(raw))
|
|
53
|
-
expectedData = 'date';
|
|
54
|
-
else if (/\b(code|otp|token)\b/i.test(raw))
|
|
55
|
-
expectedData = 'one_time_code';
|
|
56
|
-
else if (/\b(search|query)\b/i.test(raw))
|
|
57
|
-
expectedData = 'search_term';
|
|
58
|
-
}
|
|
59
|
-
out.push({ role, label: raw, selectorHint: tag, expectedData });
|
|
60
|
-
});
|
|
61
|
-
// Prioritise: form fields > buttons > links; within each group keep order
|
|
62
|
-
const priority = (e) => {
|
|
63
|
-
if (['email', 'password', 'text', 'tel', 'date', 'number', 'select', 'textarea'].includes(e.role))
|
|
64
|
-
return 0;
|
|
65
|
-
if (e.role === 'button' || e.role === 'submit')
|
|
66
|
-
return 1;
|
|
67
|
-
return 2;
|
|
68
|
-
};
|
|
69
|
-
out.sort((a, b) => priority(a) - priority(b));
|
|
70
|
-
return out.slice(0, 25); // cap at 25 — enough context, not overwhelming
|
|
1
|
+
async function readScreen(page) {
|
|
2
|
+
return page.evaluate(() => {
|
|
3
|
+
const out = [];
|
|
4
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5
|
+
const noiseLabel = /^(cookie|consent|privacy policy|terms of use|copyright|©|\bv\d+\.\d+|got it|dismiss|skip to|powered by|copied|\s*$)/i;
|
|
6
|
+
const nodes = document.querySelectorAll(
|
|
7
|
+
'input:not([type=hidden]), select, textarea, button, a[href], [role="button"], [role="combobox"], [role="slider"], [role="switch"], [role="tab"], [role="menuitem"], [role="searchbox"], [role="spinbutton"]'
|
|
8
|
+
);
|
|
9
|
+
nodes.forEach((el) => {
|
|
10
|
+
const htmlEl = el;
|
|
11
|
+
const style = window.getComputedStyle(htmlEl);
|
|
12
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return;
|
|
13
|
+
const rect = el.getBoundingClientRect();
|
|
14
|
+
const hasAccessibleLabel = !!(el.getAttribute("aria-label") || el.getAttribute("aria-labelledby"));
|
|
15
|
+
if (rect.width === 0 && rect.height === 0 && !hasAccessibleLabel) return;
|
|
16
|
+
const tag = el.tagName.toLowerCase();
|
|
17
|
+
const role = el.getAttribute("role") || (tag === "input" ? el.type || "text" : tag);
|
|
18
|
+
const raw = (el.getAttribute("aria-label") || el.getAttribute("placeholder") || (el.getAttribute("title") ?? "") || htmlEl.innerText?.replace(/\s+/g, " ").trim() || el.getAttribute("name") || "").substring(0, 80);
|
|
19
|
+
if (!raw || noiseLabel.test(raw) || raw.length > 70) return;
|
|
20
|
+
const key = `${role}:${raw.toLowerCase()}`;
|
|
21
|
+
if (seen.has(key)) return;
|
|
22
|
+
seen.add(key);
|
|
23
|
+
let expectedData;
|
|
24
|
+
if (tag === "input") {
|
|
25
|
+
const input = el;
|
|
26
|
+
if (input.type === "email" || /email/i.test(raw)) expectedData = "valid_email";
|
|
27
|
+
else if (input.type === "password" || /password/i.test(raw)) expectedData = "password";
|
|
28
|
+
else if (input.type === "tel" || /phone|mobile/i.test(raw)) expectedData = "phone";
|
|
29
|
+
else if (input.type === "date" || /\bdate\b/i.test(raw)) expectedData = "date";
|
|
30
|
+
else if (/\b(code|otp|token)\b/i.test(raw)) expectedData = "one_time_code";
|
|
31
|
+
else if (/\b(search|query)\b/i.test(raw)) expectedData = "search_term";
|
|
32
|
+
}
|
|
33
|
+
out.push({ role, label: raw, selectorHint: tag, expectedData });
|
|
71
34
|
});
|
|
35
|
+
const priority = (e) => {
|
|
36
|
+
if (["email", "password", "text", "tel", "date", "number", "select", "textarea"].includes(e.role)) return 0;
|
|
37
|
+
if (e.role === "button" || e.role === "submit") return 1;
|
|
38
|
+
return 2;
|
|
39
|
+
};
|
|
40
|
+
out.sort((a, b) => priority(a) - priority(b));
|
|
41
|
+
return out.slice(0, 25);
|
|
42
|
+
});
|
|
72
43
|
}
|
|
44
|
+
export {
|
|
45
|
+
readScreen
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=screen-reader.js.map
|
|
@@ -1,80 +1,62 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Screenshot masker — blur or black-box PII regions in captured screenshots.
|
|
3
|
-
*
|
|
4
|
-
* Strategy: inject a CSS overlay into the page before screenshot capture that
|
|
5
|
-
* covers detected PII-likely elements (input[type=email], input[type=password],
|
|
6
|
-
* elements with PII-containing text) with a blur or solid black mask.
|
|
7
|
-
*
|
|
8
|
-
* This is a CSS-based approach (no image processing library needed):
|
|
9
|
-
* 1. Identify likely-PII DOM elements via selectors
|
|
10
|
-
* 2. Add an overlaid <div> with blur/black filter at each element's bounding box
|
|
11
|
-
* 3. Take screenshot
|
|
12
|
-
* 4. Remove overlays
|
|
13
|
-
*
|
|
14
|
-
* Falls back gracefully if elements are not found or overlay injection fails.
|
|
15
|
-
*/
|
|
16
|
-
// Default PII-sensitive selectors
|
|
17
1
|
const DEFAULT_PII_SELECTORS = [
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
2
|
+
'input[type="email"]',
|
|
3
|
+
'input[type="password"]',
|
|
4
|
+
'input[type="tel"]',
|
|
5
|
+
'input[autocomplete="cc-number"]',
|
|
6
|
+
'input[autocomplete="cc-csc"]',
|
|
7
|
+
'input[autocomplete="ssn"]',
|
|
8
|
+
'[data-pii="true"]',
|
|
9
|
+
'[data-sensitive="true"]'
|
|
26
10
|
];
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
await page.evaluate((ids) => {
|
|
75
|
-
for (const id of ids)
|
|
76
|
-
document.getElementById(id)?.remove();
|
|
77
|
-
}, overlayIds).catch(() => { });
|
|
78
|
-
}
|
|
79
|
-
return { maskedCount, screenshot };
|
|
11
|
+
async function maskScreenshot(page, opts = {}) {
|
|
12
|
+
const { extraSelectors = [], style = "blur", blurPx = 8 } = opts;
|
|
13
|
+
const selectors = [...DEFAULT_PII_SELECTORS, ...extraSelectors];
|
|
14
|
+
let maskedCount = 0;
|
|
15
|
+
const overlayIds = [];
|
|
16
|
+
try {
|
|
17
|
+
const result = await page.evaluate(({ selectors: selectors2, style: style2, blurPx: blurPx2 }) => {
|
|
18
|
+
const ids = [];
|
|
19
|
+
for (const sel of selectors2) {
|
|
20
|
+
try {
|
|
21
|
+
const elements = Array.from(document.querySelectorAll(sel));
|
|
22
|
+
for (const el of elements) {
|
|
23
|
+
const rect = el.getBoundingClientRect();
|
|
24
|
+
if (rect.width === 0 || rect.height === 0) continue;
|
|
25
|
+
const overlay = document.createElement("div");
|
|
26
|
+
const id = `__zeta_mask_${Math.random().toString(36).slice(2)}`;
|
|
27
|
+
overlay.id = id;
|
|
28
|
+
overlay.style.cssText = [
|
|
29
|
+
"position: fixed",
|
|
30
|
+
`top: ${rect.top + window.scrollY}px`,
|
|
31
|
+
`left: ${rect.left + window.scrollX}px`,
|
|
32
|
+
`width: ${rect.width}px`,
|
|
33
|
+
`height: ${rect.height}px`,
|
|
34
|
+
"z-index: 2147483647",
|
|
35
|
+
style2 === "blur" ? `backdrop-filter: blur(${blurPx2}px); -webkit-backdrop-filter: blur(${blurPx2}px); background: rgba(0,0,0,0.1);` : "background: #000000;",
|
|
36
|
+
"pointer-events: none"
|
|
37
|
+
].join("; ");
|
|
38
|
+
document.body.appendChild(overlay);
|
|
39
|
+
ids.push(id);
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return ids;
|
|
45
|
+
}, { selectors, style, blurPx });
|
|
46
|
+
overlayIds.push(...result);
|
|
47
|
+
maskedCount = result.length;
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
const screenshot = await page.screenshot({ type: "png", fullPage: false });
|
|
51
|
+
if (overlayIds.length > 0) {
|
|
52
|
+
await page.evaluate((ids) => {
|
|
53
|
+
for (const id of ids) document.getElementById(id)?.remove();
|
|
54
|
+
}, overlayIds).catch(() => {
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return { maskedCount, screenshot };
|
|
80
58
|
}
|
|
59
|
+
export {
|
|
60
|
+
maskScreenshot
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=screenshot-masker.js.map
|
package/dist/seo-checker.js
CHANGED
|
@@ -1,110 +1,91 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
let isIndexable = true;
|
|
20
|
-
// 1. X-Robots-Tag
|
|
21
|
-
const robotsHeader = responseHeaders['x-robots-tag'] ?? '';
|
|
22
|
-
if (/noindex/i.test(robotsHeader)) {
|
|
23
|
-
isIndexable = false;
|
|
24
|
-
issues.push({ rule: 'x-robots-noindex', severity: 'critical', message: 'X-Robots-Tag: noindex found in response header', value: robotsHeader });
|
|
25
|
-
}
|
|
26
|
-
// 2-9. Page-level checks
|
|
27
|
-
const pageData = await page.evaluate(() => {
|
|
28
|
-
const meta = (name) => document.querySelector(`meta[name="${name}"]`)?.getAttribute('content') ?? '';
|
|
29
|
-
const og = (prop) => document.querySelector(`meta[property="og:${prop}"]`)?.getAttribute('content') ?? '';
|
|
30
|
-
const canonical = document.querySelector('link[rel="canonical"]')?.getAttribute('href') ?? '';
|
|
31
|
-
const hreflang = {};
|
|
32
|
-
document.querySelectorAll('link[rel="alternate"][hreflang]').forEach((el) => {
|
|
33
|
-
hreflang[el.getAttribute('hreflang')] = el.getAttribute('href') ?? '';
|
|
34
|
-
});
|
|
35
|
-
const jsonLd = Array.from(document.querySelectorAll('script[type="application/ld+json"]')).map((s) => s.textContent ?? '');
|
|
36
|
-
return {
|
|
37
|
-
robotsMeta: meta('robots'),
|
|
38
|
-
description: meta('description'),
|
|
39
|
-
title: document.title ?? '',
|
|
40
|
-
canonical,
|
|
41
|
-
hreflang,
|
|
42
|
-
ogTitle: og('title'),
|
|
43
|
-
ogDescription: og('description'),
|
|
44
|
-
ogImage: og('image'),
|
|
45
|
-
hasJsonLd: jsonLd.length > 0,
|
|
46
|
-
jsonLd,
|
|
47
|
-
};
|
|
48
|
-
}).catch(() => ({
|
|
49
|
-
robotsMeta: '', description: '', title: '', canonical: '', hreflang: {},
|
|
50
|
-
ogTitle: '', ogDescription: '', ogImage: '', hasJsonLd: false, jsonLd: [],
|
|
51
|
-
}));
|
|
52
|
-
// 2. Meta robots
|
|
53
|
-
if (/noindex/i.test(pageData.robotsMeta)) {
|
|
54
|
-
isIndexable = false;
|
|
55
|
-
issues.push({ rule: 'meta-robots-noindex', severity: 'critical', message: 'Meta robots noindex found', value: pageData.robotsMeta });
|
|
56
|
-
}
|
|
57
|
-
// 3. Canonical
|
|
58
|
-
let isSelfCanonical;
|
|
59
|
-
if (pageData.canonical) {
|
|
60
|
-
isSelfCanonical = pageData.canonical === url || pageData.canonical.replace(/\/$/, '') === url.replace(/\/$/, '');
|
|
61
|
-
if (!isSelfCanonical) {
|
|
62
|
-
issues.push({ rule: 'cross-canonical', severity: 'warning', message: 'Cross-canonical points elsewhere', value: pageData.canonical });
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
issues.push({ rule: 'missing-canonical', severity: 'warning', message: 'No canonical link tag' });
|
|
67
|
-
}
|
|
68
|
-
// 4. Title
|
|
69
|
-
if (!pageData.title) {
|
|
70
|
-
issues.push({ rule: 'missing-title', severity: 'critical', message: 'Page has no title tag' });
|
|
71
|
-
}
|
|
72
|
-
else if (pageData.title.length > 60) {
|
|
73
|
-
issues.push({ rule: 'title-too-long', severity: 'warning', message: `Title ${pageData.title.length} chars (>60)`, value: pageData.title });
|
|
74
|
-
}
|
|
75
|
-
// 5. Meta description
|
|
76
|
-
if (!pageData.description) {
|
|
77
|
-
issues.push({ rule: 'missing-description', severity: 'warning', message: 'Missing meta description' });
|
|
78
|
-
}
|
|
79
|
-
else if (pageData.description.length > 160) {
|
|
80
|
-
issues.push({ rule: 'description-too-long', severity: 'info', message: `Description ${pageData.description.length} chars (>160)` });
|
|
81
|
-
}
|
|
82
|
-
// 6. OG tags
|
|
83
|
-
if (!pageData.ogTitle)
|
|
84
|
-
issues.push({ rule: 'missing-og-title', severity: 'info', message: 'Missing og:title' });
|
|
85
|
-
if (!pageData.ogImage)
|
|
86
|
-
issues.push({ rule: 'missing-og-image', severity: 'info', message: 'Missing og:image' });
|
|
87
|
-
// 7. Structured data
|
|
88
|
-
if (!pageData.hasJsonLd)
|
|
89
|
-
issues.push({ rule: 'no-structured-data', severity: 'info', message: 'No JSON-LD structured data found' });
|
|
90
|
-
// 8. URL depth
|
|
91
|
-
try {
|
|
92
|
-
const depth = new URL(url).pathname.split('/').filter(Boolean).length;
|
|
93
|
-
if (depth > 5)
|
|
94
|
-
issues.push({ rule: 'deep-url', severity: 'info', message: `URL depth ${depth} (>5 levels)`, value: url });
|
|
95
|
-
}
|
|
96
|
-
catch { }
|
|
97
|
-
const criticals = issues.filter(i => i.severity === 'critical').length;
|
|
98
|
-
const warnings = issues.filter(i => i.severity === 'warning').length;
|
|
99
|
-
const score = Math.max(0, 100 - criticals * 20 - warnings * 5 - issues.filter(i => i.severity === 'info').length * 2);
|
|
1
|
+
async function checkSeo(page, url, responseHeaders = {}) {
|
|
2
|
+
const issues = [];
|
|
3
|
+
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4
|
+
let isIndexable = true;
|
|
5
|
+
const robotsHeader = responseHeaders["x-robots-tag"] ?? "";
|
|
6
|
+
if (/noindex/i.test(robotsHeader)) {
|
|
7
|
+
isIndexable = false;
|
|
8
|
+
issues.push({ rule: "x-robots-noindex", severity: "critical", message: "X-Robots-Tag: noindex found in response header", value: robotsHeader });
|
|
9
|
+
}
|
|
10
|
+
const pageData = await page.evaluate(() => {
|
|
11
|
+
const meta = (name) => document.querySelector(`meta[name="${name}"]`)?.getAttribute("content") ?? "";
|
|
12
|
+
const og = (prop) => document.querySelector(`meta[property="og:${prop}"]`)?.getAttribute("content") ?? "";
|
|
13
|
+
const canonical = document.querySelector('link[rel="canonical"]')?.getAttribute("href") ?? "";
|
|
14
|
+
const hreflang = {};
|
|
15
|
+
document.querySelectorAll('link[rel="alternate"][hreflang]').forEach((el) => {
|
|
16
|
+
hreflang[el.getAttribute("hreflang")] = el.getAttribute("href") ?? "";
|
|
17
|
+
});
|
|
18
|
+
const jsonLd = Array.from(document.querySelectorAll('script[type="application/ld+json"]')).map((s) => s.textContent ?? "");
|
|
100
19
|
return {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
20
|
+
robotsMeta: meta("robots"),
|
|
21
|
+
description: meta("description"),
|
|
22
|
+
title: document.title ?? "",
|
|
23
|
+
canonical,
|
|
24
|
+
hreflang,
|
|
25
|
+
ogTitle: og("title"),
|
|
26
|
+
ogDescription: og("description"),
|
|
27
|
+
ogImage: og("image"),
|
|
28
|
+
hasJsonLd: jsonLd.length > 0,
|
|
29
|
+
jsonLd
|
|
109
30
|
};
|
|
31
|
+
}).catch(() => ({
|
|
32
|
+
robotsMeta: "",
|
|
33
|
+
description: "",
|
|
34
|
+
title: "",
|
|
35
|
+
canonical: "",
|
|
36
|
+
hreflang: {},
|
|
37
|
+
ogTitle: "",
|
|
38
|
+
ogDescription: "",
|
|
39
|
+
ogImage: "",
|
|
40
|
+
hasJsonLd: false,
|
|
41
|
+
jsonLd: []
|
|
42
|
+
}));
|
|
43
|
+
if (/noindex/i.test(pageData.robotsMeta)) {
|
|
44
|
+
isIndexable = false;
|
|
45
|
+
issues.push({ rule: "meta-robots-noindex", severity: "critical", message: "Meta robots noindex found", value: pageData.robotsMeta });
|
|
46
|
+
}
|
|
47
|
+
let isSelfCanonical;
|
|
48
|
+
if (pageData.canonical) {
|
|
49
|
+
isSelfCanonical = pageData.canonical === url || pageData.canonical.replace(/\/$/, "") === url.replace(/\/$/, "");
|
|
50
|
+
if (!isSelfCanonical) {
|
|
51
|
+
issues.push({ rule: "cross-canonical", severity: "warning", message: "Cross-canonical points elsewhere", value: pageData.canonical });
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
issues.push({ rule: "missing-canonical", severity: "warning", message: "No canonical link tag" });
|
|
55
|
+
}
|
|
56
|
+
if (!pageData.title) {
|
|
57
|
+
issues.push({ rule: "missing-title", severity: "critical", message: "Page has no title tag" });
|
|
58
|
+
} else if (pageData.title.length > 60) {
|
|
59
|
+
issues.push({ rule: "title-too-long", severity: "warning", message: `Title ${pageData.title.length} chars (>60)`, value: pageData.title });
|
|
60
|
+
}
|
|
61
|
+
if (!pageData.description) {
|
|
62
|
+
issues.push({ rule: "missing-description", severity: "warning", message: "Missing meta description" });
|
|
63
|
+
} else if (pageData.description.length > 160) {
|
|
64
|
+
issues.push({ rule: "description-too-long", severity: "info", message: `Description ${pageData.description.length} chars (>160)` });
|
|
65
|
+
}
|
|
66
|
+
if (!pageData.ogTitle) issues.push({ rule: "missing-og-title", severity: "info", message: "Missing og:title" });
|
|
67
|
+
if (!pageData.ogImage) issues.push({ rule: "missing-og-image", severity: "info", message: "Missing og:image" });
|
|
68
|
+
if (!pageData.hasJsonLd) issues.push({ rule: "no-structured-data", severity: "info", message: "No JSON-LD structured data found" });
|
|
69
|
+
try {
|
|
70
|
+
const depth = new URL(url).pathname.split("/").filter(Boolean).length;
|
|
71
|
+
if (depth > 5) issues.push({ rule: "deep-url", severity: "info", message: `URL depth ${depth} (>5 levels)`, value: url });
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
const criticals = issues.filter((i) => i.severity === "critical").length;
|
|
75
|
+
const warnings = issues.filter((i) => i.severity === "warning").length;
|
|
76
|
+
const score = Math.max(0, 100 - criticals * 20 - warnings * 5 - issues.filter((i) => i.severity === "info").length * 2);
|
|
77
|
+
return {
|
|
78
|
+
url,
|
|
79
|
+
isIndexable,
|
|
80
|
+
canonicalUrl: pageData.canonical || void 0,
|
|
81
|
+
isSelfCanonical,
|
|
82
|
+
hreflang: pageData.hreflang,
|
|
83
|
+
issues,
|
|
84
|
+
score,
|
|
85
|
+
checkedAt
|
|
86
|
+
};
|
|
110
87
|
}
|
|
88
|
+
export {
|
|
89
|
+
checkSeo
|
|
90
|
+
};
|
|
91
|
+
//# sourceMappingURL=seo-checker.js.map
|