@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.
Files changed (83) hide show
  1. package/dist/accessibility-auditor.js +54 -74
  2. package/dist/ai-login.js +227 -249
  3. package/dist/auth-token-rotator.js +57 -67
  4. package/dist/auth.js +299 -336
  5. package/dist/browser-extension.js +104 -142
  6. package/dist/captcha-solver.js +98 -125
  7. package/dist/color-contrast.js +77 -89
  8. package/dist/content-chunker.js +126 -126
  9. package/dist/content-dedup.js +54 -71
  10. package/dist/cookie-consent.js +68 -83
  11. package/dist/cookie-jar.js +53 -60
  12. package/dist/crawler.js +4494 -5353
  13. package/dist/custom-js-injector.js +45 -61
  14. package/dist/dark-mode-capture.js +67 -81
  15. package/dist/dead-link-detector.js +47 -55
  16. package/dist/element-hash.js +8 -12
  17. package/dist/email-otp.js +154 -195
  18. package/dist/engine-router.js +87 -98
  19. package/dist/export/bulk-export.js +70 -55
  20. package/dist/export/vector-export.js +77 -105
  21. package/dist/extractors/advanced.js +249 -266
  22. package/dist/extractors/artifact-envelope.js +59 -58
  23. package/dist/failure-analysis-agent.js +37 -33
  24. package/dist/feature-flag-detector.js +51 -66
  25. package/dist/form-detector.js +55 -75
  26. package/dist/frontier.js +76 -101
  27. package/dist/gap-pass.js +68 -89
  28. package/dist/graphql-detector.js +62 -72
  29. package/dist/http-engine.js +282 -330
  30. package/dist/incremental-diff.js +54 -87
  31. package/dist/index.js +67 -28
  32. package/dist/input-fuzzer.js +93 -97
  33. package/dist/interactive-probe.js +715 -827
  34. package/dist/job-queue.js +2151 -2169
  35. package/dist/js-coverage.js +52 -56
  36. package/dist/js-router-detector.js +86 -117
  37. package/dist/language-detector.js +51 -63
  38. package/dist/link-classifier.js +32 -50
  39. package/dist/live-view.js +91 -108
  40. package/dist/locator-extractor.js +151 -187
  41. package/dist/logger.js +9 -7
  42. package/dist/markdown-extract.js +48 -61
  43. package/dist/native-crawler.js +780 -946
  44. package/dist/network-observer.js +135 -156
  45. package/dist/network-throttle.js +29 -40
  46. package/dist/openapi-generator.js +77 -92
  47. package/dist/pdf-extractor.js +72 -90
  48. package/dist/performance-metrics.js +80 -102
  49. package/dist/phash.js +125 -144
  50. package/dist/pii-scrubber.js +65 -68
  51. package/dist/playwright-recorder.js +70 -58
  52. package/dist/policies/crawl-safety.js +31 -34
  53. package/dist/profiles/domain-profile.js +130 -125
  54. package/dist/proxy.js +146 -172
  55. package/dist/rate-limiter.js +58 -65
  56. package/dist/redirect-tracker.js +60 -64
  57. package/dist/redis-frontier.js +116 -136
  58. package/dist/redis-queue.js +157 -144
  59. package/dist/response-cache.js +127 -140
  60. package/dist/responsive-detector.js +42 -46
  61. package/dist/retry-policy.js +46 -50
  62. package/dist/robots-txt.js +69 -83
  63. package/dist/screen-reader.js +45 -70
  64. package/dist/screenshot-masker.js +59 -77
  65. package/dist/seo-checker.js +88 -107
  66. package/dist/session-capture.js +192 -212
  67. package/dist/session-export.js +107 -133
  68. package/dist/sitemap-writer.js +38 -41
  69. package/dist/sitemap.js +141 -202
  70. package/dist/skyvern.js +123 -148
  71. package/dist/sqs-queue.js +42 -42
  72. package/dist/sso-token.js +74 -90
  73. package/dist/stealth/human-behavior.js +156 -173
  74. package/dist/telemetry.js +68 -82
  75. package/dist/test-executor.js +309 -335
  76. package/dist/tls-fingerprint.js +77 -88
  77. package/dist/url-pattern-completer.js +71 -90
  78. package/dist/visual-diff-verdict.js +23 -38
  79. package/dist/webhook-notifier.js +71 -69
  80. package/dist/websocket-detector.js +56 -68
  81. package/dist/worker.js +29 -38
  82. package/package.json +1 -1
  83. package/src/job-queue.ts +136 -0
@@ -1,72 +1,47 @@
1
- export async function readScreen(page) {
2
- return page.evaluate(() => {
3
- const out = [];
4
- const seen = new Set();
5
- // Noise patterns to skip — cookie banners, footer chrome, version strings
6
- // Intentionally excludes standalone "close" and "copy" — too many false positives
7
- // (Close account, Close preview, Copy link, Copy to clipboard are all valid test targets)
8
- const noiseLabel = /^(cookie|consent|privacy policy|terms of use|copyright|©|\bv\d+\.\d+|got it|dismiss|skip to|powered by|copied|\s*$)/i;
9
- // Include ARIA-role elements — Headless UI, Radix, Ant Design, Shadcn all use these instead of native tags
10
- const nodes = document.querySelectorAll('input:not([type=hidden]), select, textarea, button, a[href], ' +
11
- '[role="button"], [role="combobox"], [role="slider"], [role="switch"], ' +
12
- '[role="tab"], [role="menuitem"], [role="searchbox"], [role="spinbutton"]');
13
- nodes.forEach((el) => {
14
- const htmlEl = el;
15
- // Skip invisible elements
16
- const style = window.getComputedStyle(htmlEl);
17
- if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0')
18
- return;
19
- const rect = el.getBoundingClientRect();
20
- // Allow zero-size elements that carry an accessible label (sr-only icon buttons, skip-nav links)
21
- const hasAccessibleLabel = !!(el.getAttribute('aria-label') || el.getAttribute('aria-labelledby'));
22
- if (rect.width === 0 && rect.height === 0 && !hasAccessibleLabel)
23
- return;
24
- // Derive role
25
- const tag = el.tagName.toLowerCase();
26
- const role = el.getAttribute('role') ||
27
- (tag === 'input' ? (el.type || 'text') : tag);
28
- // Derive the best human-readable label (prefer aria > placeholder > inner text)
29
- const raw = (el.getAttribute('aria-label') ||
30
- el.getAttribute('placeholder') ||
31
- (el.getAttribute('title') ?? '') ||
32
- htmlEl.innerText?.replace(/\s+/g, ' ').trim() ||
33
- el.getAttribute('name') ||
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
- 'input[type="email"]',
19
- 'input[type="password"]',
20
- 'input[type="tel"]',
21
- 'input[autocomplete="cc-number"]',
22
- 'input[autocomplete="cc-csc"]',
23
- 'input[autocomplete="ssn"]',
24
- '[data-pii="true"]',
25
- '[data-sensitive="true"]',
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
- export async function maskScreenshot(page, opts = {}) {
28
- const { extraSelectors = [], style = 'blur', blurPx = 8 } = opts;
29
- const selectors = [...DEFAULT_PII_SELECTORS, ...extraSelectors];
30
- let maskedCount = 0;
31
- const overlayIds = [];
32
- // Inject CSS overlays
33
- try {
34
- const result = await page.evaluate(({ selectors, style, blurPx }) => {
35
- const ids = [];
36
- for (const sel of selectors) {
37
- try {
38
- const elements = Array.from(document.querySelectorAll(sel));
39
- for (const el of elements) {
40
- const rect = el.getBoundingClientRect();
41
- if (rect.width === 0 || rect.height === 0)
42
- continue;
43
- const overlay = document.createElement('div');
44
- const id = `__zeta_mask_${Math.random().toString(36).slice(2)}`;
45
- overlay.id = id;
46
- overlay.style.cssText = [
47
- 'position: fixed',
48
- `top: ${rect.top + window.scrollY}px`,
49
- `left: ${rect.left + window.scrollX}px`,
50
- `width: ${rect.width}px`,
51
- `height: ${rect.height}px`,
52
- 'z-index: 2147483647',
53
- style === 'blur'
54
- ? `backdrop-filter: blur(${blurPx}px); -webkit-backdrop-filter: blur(${blurPx}px); background: rgba(0,0,0,0.1);`
55
- : 'background: #000000;',
56
- 'pointer-events: none',
57
- ].join('; ');
58
- document.body.appendChild(overlay);
59
- ids.push(id);
60
- }
61
- }
62
- catch { /* selector error */ }
63
- }
64
- return ids;
65
- }, { selectors, style, blurPx });
66
- overlayIds.push(...result);
67
- maskedCount = result.length;
68
- }
69
- catch { /* non-fatal */ }
70
- // Take screenshot with overlays active
71
- const screenshot = await page.screenshot({ type: 'png', fullPage: false });
72
- // Remove overlays
73
- if (overlayIds.length > 0) {
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
@@ -1,110 +1,91 @@
1
- /**
2
- * SEO indexability checker — determines if a page is crawlable and indexable.
3
- *
4
- * Checks:
5
- * 1. X-Robots-Tag response header (noindex, nofollow, noarchive)
6
- * 2. <meta name="robots"> tag (noindex, nofollow)
7
- * 3. Canonical URL (self-canonical vs cross-canonical)
8
- * 4. rel="nofollow" on outbound links
9
- * 5. hreflang tags (multilingual)
10
- * 6. Open Graph completeness (og:title, og:description, og:image)
11
- * 7. Structured data presence (JSON-LD)
12
- * 8. Meta description
13
- * 9. Title tag length (< 60 chars recommended)
14
- * 10. URL structure (query params, fragments, excessive depth)
15
- */
16
- export async function checkSeo(page, url, responseHeaders = {}) {
17
- const issues = [];
18
- const checkedAt = new Date().toISOString();
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
- url,
102
- isIndexable,
103
- canonicalUrl: pageData.canonical || undefined,
104
- isSelfCanonical,
105
- hreflang: pageData.hreflang,
106
- issues,
107
- score,
108
- checkedAt,
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