@rankcli/agent-runtime 0.0.37 → 0.0.39

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/index.js CHANGED
@@ -55,7 +55,8 @@ async function httpGet(url, config = {}) {
55
55
  timeout = DEFAULT_TIMEOUT,
56
56
  validateStatus = (status) => status >= 200 && status < 300,
57
57
  maxRedirects = 5,
58
- params
58
+ params,
59
+ maxBytes
59
60
  } = config;
60
61
  let finalUrl = url;
61
62
  if (params) {
@@ -82,7 +83,7 @@ async function httpGet(url, config = {}) {
82
83
  if (contentType.includes("application/json")) {
83
84
  data = await response.json();
84
85
  } else {
85
- data = await response.text();
86
+ data = await readText(response, maxBytes);
86
87
  }
87
88
  clearTimeout(timeoutId);
88
89
  if (!validateStatus(response.status)) {
@@ -109,6 +110,23 @@ async function httpGet(url, config = {}) {
109
110
  throw error;
110
111
  }
111
112
  }
113
+ async function readText(response, maxBytes) {
114
+ if (!maxBytes || !response.body) return response.text();
115
+ const reader = response.body.getReader();
116
+ const chunks = [];
117
+ let received = 0;
118
+ while (received < maxBytes) {
119
+ const { done, value } = await reader.read();
120
+ if (done) break;
121
+ chunks.push(value);
122
+ received += value.byteLength;
123
+ }
124
+ if (received >= maxBytes) await reader.cancel().catch(() => void 0);
125
+ const decoder = new TextDecoder();
126
+ let text = "";
127
+ for (const chunk of chunks) text += decoder.decode(chunk, { stream: true });
128
+ return text + decoder.decode();
129
+ }
112
130
  async function httpHead(url, config = {}) {
113
131
  const {
114
132
  headers = {},
@@ -245,7 +263,7 @@ async function crawlUrl(params) {
245
263
  async function extractMeta(params) {
246
264
  const { html, url } = params;
247
265
  try {
248
- const $ = cheerio42.load(html);
266
+ const $ = cheerio43.load(html);
249
267
  const meta = {
250
268
  title: $("title").text().trim() || void 0,
251
269
  description: $('meta[name="description"]').attr("content")?.trim(),
@@ -290,7 +308,7 @@ async function extractMeta(params) {
290
308
  async function analyzeHeadings(params) {
291
309
  const { html } = params;
292
310
  try {
293
- const $ = cheerio42.load(html);
311
+ const $ = cheerio43.load(html);
294
312
  const headings = [];
295
313
  $("h1, h2, h3, h4, h5, h6").each((_, el) => {
296
314
  const tag = el.tagName.toLowerCase();
@@ -311,7 +329,7 @@ async function analyzeHeadings(params) {
311
329
  async function extractImages2(params) {
312
330
  const { html } = params;
313
331
  try {
314
- const $ = cheerio42.load(html);
332
+ const $ = cheerio43.load(html);
315
333
  const images = [];
316
334
  $("img").each((_, el) => {
317
335
  images.push({
@@ -333,7 +351,7 @@ async function extractImages2(params) {
333
351
  async function extractLinks2(params) {
334
352
  const { html, baseUrl } = params;
335
353
  try {
336
- const $ = cheerio42.load(html);
354
+ const $ = cheerio43.load(html);
337
355
  const links = [];
338
356
  const baseHostname = new URL(baseUrl).hostname;
339
357
  $("a[href]").each((_, el) => {
@@ -368,7 +386,7 @@ async function extractLinks2(params) {
368
386
  async function extractSchema2(params) {
369
387
  const { html } = params;
370
388
  try {
371
- const $ = cheerio42.load(html);
389
+ const $ = cheerio43.load(html);
372
390
  const schemas = [];
373
391
  $('script[type="application/ld+json"]').each((_, el) => {
374
392
  try {
@@ -437,7 +455,7 @@ async function checkSitemap2(params) {
437
455
  timeout: 1e4
438
456
  });
439
457
  if (response.status === 200 && response.data.includes("<?xml")) {
440
- const $ = cheerio42.load(response.data, { xmlMode: true });
458
+ const $ = cheerio43.load(response.data, { xmlMode: true });
441
459
  const urlCount = $("url").length || $("sitemap").length;
442
460
  return {
443
461
  success: true,
@@ -466,12 +484,12 @@ async function checkSitemap2(params) {
466
484
  };
467
485
  }
468
486
  }
469
- var cheerio42;
487
+ var cheerio43;
470
488
  var init_crawl = __esm({
471
489
  "src/tools/crawl.ts"() {
472
490
  "use strict";
473
491
  init_http();
474
- cheerio42 = __toESM(require("cheerio"));
492
+ cheerio43 = __toESM(require("cheerio"));
475
493
  }
476
494
  });
477
495
 
@@ -868,7 +886,7 @@ function analyzeRobotsTxtForAI(robotsTxt) {
868
886
  return { allowed, blocked, recommendations };
869
887
  }
870
888
  function detectRenderingMode(html) {
871
- const $ = cheerio43.load(html);
889
+ const $ = cheerio44.load(html);
872
890
  const signals = [];
873
891
  const hasNextData = html.includes("__NEXT_DATA__");
874
892
  const hasNuxtData = html.includes("__NUXT__");
@@ -909,7 +927,7 @@ function detectRenderingMode(html) {
909
927
  };
910
928
  }
911
929
  function analyzeContentStructure(html) {
912
- const $ = cheerio43.load(html);
930
+ const $ = cheerio44.load(html);
913
931
  const jsonLdScripts = $('script[type="application/ld+json"]');
914
932
  let hasStructuredData = jsonLdScripts.length > 0;
915
933
  let hasFAQSchema = false;
@@ -959,7 +977,7 @@ function analyzeContentStructure(html) {
959
977
  };
960
978
  }
961
979
  function analyzeCitationReadiness(html) {
962
- const $ = cheerio43.load(html);
980
+ const $ = cheerio44.load(html);
963
981
  const bodyText = $("body").text();
964
982
  const hasCitations = $('cite, blockquote, [class*="citation"], [class*="reference"]').length > 0 || /\[\d+\]|\(\d{4}\)/.test(bodyText);
965
983
  const externalLinks = $('a[href^="http"]').filter((_, el) => {
@@ -992,7 +1010,7 @@ function analyzeCitationReadiness(html) {
992
1010
  };
993
1011
  }
994
1012
  function analyzeEntityExtraction(html) {
995
- const $ = cheerio43.load(html);
1013
+ const $ = cheerio44.load(html);
996
1014
  const bodyText = $("body").text();
997
1015
  const definedTerms = [];
998
1016
  $("dfn, abbr[title]").each((_, el) => {
@@ -1028,7 +1046,7 @@ function analyzeEntityExtraction(html) {
1028
1046
  };
1029
1047
  }
1030
1048
  function calculateLLMSignals(structure, citation, entity, html) {
1031
- const $ = cheerio43.load(html);
1049
+ const $ = cheerio44.load(html);
1032
1050
  let contentClarity = 0;
1033
1051
  if (structure.headingHierarchy === "good") contentClarity += 30;
1034
1052
  else if (structure.headingHierarchy === "needs-work") contentClarity += 15;
@@ -1334,11 +1352,11 @@ Allow: /
1334
1352
  Sitemap: ${siteUrl}/sitemap.xml
1335
1353
  `;
1336
1354
  }
1337
- var cheerio43, AI_CRAWLERS, AI_CRAWLERS_INFO;
1355
+ var cheerio44, AI_CRAWLERS, AI_CRAWLERS_INFO;
1338
1356
  var init_geo_analyzer = __esm({
1339
1357
  "src/analyzers/geo-analyzer.ts"() {
1340
1358
  "use strict";
1341
- cheerio43 = __toESM(require("cheerio"));
1359
+ cheerio44 = __toESM(require("cheerio"));
1342
1360
  AI_CRAWLERS = {
1343
1361
  // OpenAI
1344
1362
  GPTBot: { userAgent: "GPTBot", company: "OpenAI", purpose: "ChatGPT training & browsing" },
@@ -1697,7 +1715,7 @@ function generateCWVIssues(lcp, fidInp, cls, ttfb, url) {
1697
1715
  return issues;
1698
1716
  }
1699
1717
  function analyzeCoreWebVitals(html, url, headers) {
1700
- const $ = cheerio44.load(html);
1718
+ const $ = cheerio45.load(html);
1701
1719
  const lcp = analyzeLCP(html, $);
1702
1720
  const fidInp = analyzeFID_INP(html, $);
1703
1721
  const cls = analyzeCLS(html, $);
@@ -1742,11 +1760,11 @@ function analyzeCoreWebVitals(html, url, headers) {
1742
1760
  issues
1743
1761
  };
1744
1762
  }
1745
- var cheerio44;
1763
+ var cheerio45;
1746
1764
  var init_core_web_vitals_analyzer = __esm({
1747
1765
  "src/analyzers/core-web-vitals-analyzer.ts"() {
1748
1766
  "use strict";
1749
- cheerio44 = __toESM(require("cheerio"));
1767
+ cheerio45 = __toESM(require("cheerio"));
1750
1768
  }
1751
1769
  });
1752
1770
 
@@ -2081,7 +2099,7 @@ function validateSchema(data, type) {
2081
2099
  };
2082
2100
  }
2083
2101
  function analyzeStructuredData2(html, url) {
2084
- const $ = cheerio45.load(html);
2102
+ const $ = cheerio46.load(html);
2085
2103
  const schemas = [];
2086
2104
  const issues = [];
2087
2105
  const recommendations = [];
@@ -2345,11 +2363,11 @@ function generateSchemaTemplate(pageType, options) {
2345
2363
  }
2346
2364
  return JSON.stringify(result, null, 2);
2347
2365
  }
2348
- var cheerio45, SCHEMA_REQUIREMENTS;
2366
+ var cheerio46, SCHEMA_REQUIREMENTS;
2349
2367
  var init_structured_data_analyzer = __esm({
2350
2368
  "src/analyzers/structured-data-analyzer.ts"() {
2351
2369
  "use strict";
2352
- cheerio45 = __toESM(require("cheerio"));
2370
+ cheerio46 = __toESM(require("cheerio"));
2353
2371
  SCHEMA_REQUIREMENTS = {
2354
2372
  "Organization": {
2355
2373
  required: ["name", "url"],
@@ -2465,7 +2483,7 @@ function getImageFormat2(src) {
2465
2483
  }
2466
2484
  }
2467
2485
  function analyzeImages3(html, url) {
2468
- const $ = cheerio46.load(html);
2486
+ const $ = cheerio47.load(html);
2469
2487
  const issues = [];
2470
2488
  const recommendations = [];
2471
2489
  let score = 100;
@@ -2683,11 +2701,11 @@ function generateResponsiveImage(options) {
2683
2701
  />
2684
2702
  </picture>`;
2685
2703
  }
2686
- var cheerio46, MODERN_FORMATS3, LEGACY_FORMATS2, MAX_RECOMMENDED_DIMENSION;
2704
+ var cheerio47, MODERN_FORMATS3, LEGACY_FORMATS2, MAX_RECOMMENDED_DIMENSION;
2687
2705
  var init_image_optimization_analyzer = __esm({
2688
2706
  "src/analyzers/image-optimization-analyzer.ts"() {
2689
2707
  "use strict";
2690
- cheerio46 = __toESM(require("cheerio"));
2708
+ cheerio47 = __toESM(require("cheerio"));
2691
2709
  MODERN_FORMATS3 = ["webp", "avif"];
2692
2710
  LEGACY_FORMATS2 = ["jpg", "jpeg", "png", "gif", "bmp"];
2693
2711
  MAX_RECOMMENDED_DIMENSION = 2e3;
@@ -2728,7 +2746,7 @@ function analyzeAnchorText2(text) {
2728
2746
  };
2729
2747
  }
2730
2748
  function analyzeInternalLinking(html, url) {
2731
- const $ = cheerio47.load(html);
2749
+ const $ = cheerio48.load(html);
2732
2750
  const issues = [];
2733
2751
  const recommendations = [];
2734
2752
  let score = 100;
@@ -2967,11 +2985,11 @@ function suggestInternalLinks(content, availablePages) {
2967
2985
  }
2968
2986
  return suggestions.sort((a, b) => b.relevance - a.relevance).filter((s, i, arr) => arr.findIndex((x) => x.suggestedUrl === s.suggestedUrl) === i).slice(0, 10);
2969
2987
  }
2970
- var cheerio47, GENERIC_ANCHOR_TEXTS;
2988
+ var cheerio48, GENERIC_ANCHOR_TEXTS;
2971
2989
  var init_internal_linking_analyzer = __esm({
2972
2990
  "src/analyzers/internal-linking-analyzer.ts"() {
2973
2991
  "use strict";
2974
- cheerio47 = __toESM(require("cheerio"));
2992
+ cheerio48 = __toESM(require("cheerio"));
2975
2993
  GENERIC_ANCHOR_TEXTS = [
2976
2994
  "click here",
2977
2995
  "read more",
@@ -3165,7 +3183,7 @@ function analyzeMobileSpecific($) {
3165
3183
  };
3166
3184
  }
3167
3185
  function analyzeMobileSEO(html, url) {
3168
- const $ = cheerio48.load(html);
3186
+ const $ = cheerio49.load(html);
3169
3187
  const issues = [];
3170
3188
  const recommendations = [];
3171
3189
  let score = 100;
@@ -3296,11 +3314,11 @@ function analyzeMobileSEO(html, url) {
3296
3314
  recommendations
3297
3315
  };
3298
3316
  }
3299
- var cheerio48;
3317
+ var cheerio49;
3300
3318
  var init_mobile_seo_analyzer = __esm({
3301
3319
  "src/analyzers/mobile-seo-analyzer.ts"() {
3302
3320
  "use strict";
3303
- cheerio48 = __toESM(require("cheerio"));
3321
+ cheerio49 = __toESM(require("cheerio"));
3304
3322
  }
3305
3323
  });
3306
3324
 
@@ -3312,6 +3330,7 @@ __export(index_exports, {
3312
3330
  LOCATION_CODES: () => LOCATION_CODES,
3313
3331
  OG_IMAGE_SPECS: () => OG_IMAGE_SPECS,
3314
3332
  PRIORITY_WEIGHTS: () => PRIORITY_WEIGHTS,
3333
+ REDUNDANT_WHEN_PRESENT: () => REDUNDANT_WHEN_PRESENT,
3315
3334
  RankTracker: () => RankTracker,
3316
3335
  SEO_SCOPES: () => SEO_SCOPES,
3317
3336
  SITE_PROFILE_QUESTIONS: () => SITE_PROFILE_QUESTIONS,
@@ -3395,6 +3414,7 @@ __export(index_exports, {
3395
3414
  createPullRequest: () => createPullRequest,
3396
3415
  createSEOCommit: () => createSEOCommit,
3397
3416
  createSEOCommits: () => createSEOCommits,
3417
+ dedupeIssues: () => dedupeIssues,
3398
3418
  describeConcerns: () => describeConcerns,
3399
3419
  detectDuplicates: () => detectDuplicates,
3400
3420
  detectFramework: () => detectFramework4,
@@ -3531,6 +3551,7 @@ __export(index_exports, {
3531
3551
  loadAgent: () => loadAgent,
3532
3552
  loadAgentByName: () => loadAgentByName,
3533
3553
  mergePages: () => mergePages,
3554
+ metaRefreshTarget: () => metaRefreshTarget,
3534
3555
  openAIKeywordLLM: () => openAIKeywordLLM,
3535
3556
  optimizeForAI: () => optimizeForAI,
3536
3557
  optimizeReadme: () => optimizeReadme,
@@ -6353,7 +6374,7 @@ var ISSUE_DEFINITIONS = {
6353
6374
  severity: "warning",
6354
6375
  category: "performance",
6355
6376
  title: "HTML document too large",
6356
- description: "HTML size exceeds 100KB.",
6377
+ description: "HTML size exceeds 300KB.",
6357
6378
  impact: "Increases load time and memory usage.",
6358
6379
  howToFix: "Reduce inline CSS/JS, remove unnecessary HTML."
6359
6380
  },
@@ -9095,7 +9116,7 @@ async function analyzePerformance(url) {
9095
9116
  details: { ttfb: `${ttfbTime}ms` }
9096
9117
  });
9097
9118
  }
9098
- if (htmlSize > 100 * 1024) {
9119
+ if (htmlSize > 300 * 1024) {
9099
9120
  issues.push({
9100
9121
  ...ISSUE_DEFINITIONS.HTML_TOO_LARGE,
9101
9122
  affectedUrls: [url],
@@ -9996,7 +10017,9 @@ function analyzeAnchorText(html, url) {
9996
10017
  } catch {
9997
10018
  internalLinks++;
9998
10019
  }
9999
- const hasAccessibleText = text.length > 0 || ariaLabel || title;
10020
+ const imgAlt = $(el).find("img[alt]").map((_2, img) => ($(img).attr("alt") || "").trim()).get().join(" ").trim();
10021
+ const svgTitle = $(el).find("svg title").text().trim();
10022
+ const hasAccessibleText = text.length > 0 || ariaLabel || title || imgAlt.length > 0 || svgTitle.length > 0;
10000
10023
  if (!hasAccessibleText) {
10001
10024
  emptyAnchors++;
10002
10025
  if (emptyAnchorUrls.length < 5) {
@@ -11257,7 +11280,7 @@ function analyzeLocalSEO(html, url) {
11257
11280
  const detectedPhones = detectPhones(html);
11258
11281
  const detectedAddresses = detectAddresses(html);
11259
11282
  const socialProfiles = detectSocialProfiles(html);
11260
- if (!hasLocalBusiness) {
11283
+ if (!hasLocalBusiness && (detectedPhones.length > 0 || detectedAddresses.length > 0)) {
11261
11284
  issues.push({
11262
11285
  ...ISSUE_DEFINITIONS.LOCAL_BUSINESS_SCHEMA_MISSING,
11263
11286
  affectedUrls: [url]
@@ -12320,7 +12343,17 @@ function analyzeKeywords(html, url) {
12320
12343
  const sortPhrases = (phrases, n) => {
12321
12344
  return Array.from(phrases).filter(([_, count]) => count >= 2).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([phrase, count]) => ({ phrase, count, wordCount: n }));
12322
12345
  };
12323
- const potentialStuffing = topKeywords.filter((k) => k.density > 3).map((k) => k.word);
12346
+ const brandLabels = /* @__PURE__ */ new Set();
12347
+ try {
12348
+ const host = new URL(url).hostname.toLowerCase().replace(/^www\./, "");
12349
+ for (const label of host.split(".").slice(0, -1)) brandLabels.add(label);
12350
+ } catch {
12351
+ }
12352
+ for (const name of [$('meta[property="og:site_name"]').attr("content"), $('meta[name="application-name"]').attr("content")]) {
12353
+ for (const token of tokenize(name || "")) brandLabels.add(token);
12354
+ }
12355
+ const isBrandWord = (word) => [...brandLabels].some((label) => label.includes(word) || word.includes(label));
12356
+ const potentialStuffing = totalWords >= 200 ? topKeywords.filter((k) => k.density > 4 && !isBrandWord(k.word)).map((k) => k.word) : [];
12324
12357
  if (potentialStuffing.length > 0) {
12325
12358
  issues.push({
12326
12359
  ...ISSUE_DEFINITIONS.KEYWORD_STUFFING,
@@ -12547,13 +12580,6 @@ function checkAMP(html, url) {
12547
12580
  }
12548
12581
  };
12549
12582
  }
12550
- const isArticle = $("article").length > 0 || $('meta[property="og:type"][content="article"]').length > 0 || html.toLowerCase().includes("blog") || html.toLowerCase().includes("news");
12551
- if (isArticle) {
12552
- issues.push({
12553
- ...ISSUE_DEFINITIONS.AMP_MISSING,
12554
- affectedUrls: [url]
12555
- });
12556
- }
12557
12583
  return {
12558
12584
  issues,
12559
12585
  data: {
@@ -13265,13 +13291,13 @@ async function analyzeSiteMaturity(html, url) {
13265
13291
  contentToAuthorityRatio
13266
13292
  };
13267
13293
  const { strategy, recommendations } = determineStrategy(maturityLevel, authorityIndicators, contentMetrics);
13268
- if (maturityLevel === "new") {
13294
+ if (maturityLevel === "new" && ageSource === "wayback" && estimatedAgeDays !== null) {
13269
13295
  issues.push({
13270
13296
  code: "NEW_SITE_STRATEGY",
13271
13297
  severity: "notice",
13272
13298
  category: "content",
13273
13299
  title: "New website detected - Authority-first strategy recommended",
13274
- description: "Your site appears to be new (<90 days). Standard SEO advice won't work well for new sites.",
13300
+ description: `The Wayback Machine first saw this site ${estimatedAgeDays} days ago. Standard SEO advice won't work well for a site under 90 days old.`,
13275
13301
  impact: "Creating lots of content without authority is wasted effort. Content from new sites rarely ranks.",
13276
13302
  howToFix: "Focus on building 10+ quality backlinks to ONE great page before expanding content. See strategic recommendations.",
13277
13303
  affectedUrls: [url],
@@ -13282,24 +13308,7 @@ async function analyzeSiteMaturity(html, url) {
13282
13308
  }
13283
13309
  });
13284
13310
  }
13285
- if (contentToAuthorityRatio === "content-heavy") {
13286
- issues.push({
13287
- code: "CONTENT_AUTHORITY_IMBALANCE",
13288
- severity: "warning",
13289
- category: "content",
13290
- title: "Content-to-authority imbalance",
13291
- description: `Site has ~${indexedPages} pages but limited backlink signals. Content is unlikely to rank.`,
13292
- impact: "Most of your content may be invisible to search engines due to low domain authority.",
13293
- howToFix: "Stop publishing new content. Focus 100% on link building until you see ranking improvements.",
13294
- affectedUrls: [url],
13295
- details: {
13296
- indexedPages,
13297
- contentToAuthorityRatio,
13298
- maturityLevel
13299
- }
13300
- });
13301
- }
13302
- if (maturityLevel === "growing" && !htmlSignals.hasBlog) {
13311
+ if (maturityLevel === "growing" && ageSource === "wayback" && !htmlSignals.hasBlog) {
13303
13312
  issues.push({
13304
13313
  code: "GROWING_SITE_NO_BLOG",
13305
13314
  severity: "notice",
@@ -13793,8 +13802,8 @@ function analyzeRendering(html) {
13793
13802
  const hasSemanticStructure = hasH1 && (hasMain || hasArticle);
13794
13803
  const title = $("title").text().trim();
13795
13804
  const h1Text = $("h1").first().text().trim();
13796
- const firstParagraph = $("p").first().text().trim();
13797
- const keyContentInHTML = title.length > 10 && h1Text.length > 5 && firstParagraph.length > 50;
13805
+ const hasSubstantialParagraph = $("p").toArray().some((p) => $(p).text().trim().length > 50);
13806
+ const keyContentInHTML = title.length > 0 && h1Text.length > 0 && hasSubstantialParagraph;
13798
13807
  const EXPECTED_MIN_CONTENT = 1e3;
13799
13808
  const htmlContentRatio = Math.min(100, mainContentCharCount / EXPECTED_MIN_CONTENT * 100);
13800
13809
  let aiReadableScore = 50;
@@ -14014,9 +14023,9 @@ function analyzeClientRendering(html, url) {
14014
14023
  howToFix: "Ensure the initial HTML contains: meaningful <title>, descriptive <h1>, and a substantial first paragraph with your key message.",
14015
14024
  affectedUrls: [url],
14016
14025
  details: {
14017
- hasTitle: $("title").text().trim().length > 10,
14018
- hasH1: $("h1").text().trim().length > 5,
14019
- hasFirstParagraph: $("p").first().text().trim().length > 50
14026
+ hasTitle: $("title").text().trim().length > 0,
14027
+ hasH1: $("h1").text().trim().length > 0,
14028
+ hasSubstantialParagraph: $("p").toArray().some((p) => $(p).text().trim().length > 50)
14020
14029
  }
14021
14030
  });
14022
14031
  }
@@ -14565,6 +14574,107 @@ function analyzeResponsiveImages(html, url) {
14565
14574
  // src/audit/checks/bing-optimization.ts
14566
14575
  var cheerio28 = __toESM(require("cheerio"));
14567
14576
  init_http();
14577
+
14578
+ // src/audit/checks/site-verification.ts
14579
+ init_http();
14580
+ var UNCHECKED = {
14581
+ dnsChecked: false,
14582
+ googleDns: false,
14583
+ bingDns: false,
14584
+ bingSiteAuthFile: false
14585
+ };
14586
+ var DNS_TIMEOUT_MS = 2e3;
14587
+ var FILE_TIMEOUT_MS = 5e3;
14588
+ var CACHE_TTL_MS = 10 * 60 * 1e3;
14589
+ var cache = /* @__PURE__ */ new Map();
14590
+ function lookupSiteVerification(url) {
14591
+ let parsed;
14592
+ try {
14593
+ parsed = new URL(url);
14594
+ } catch {
14595
+ return Promise.resolve(UNCHECKED);
14596
+ }
14597
+ const host = parsed.hostname.toLowerCase();
14598
+ const now = Date.now();
14599
+ const cached = cache.get(host);
14600
+ if (cached && cached.expires > now) return cached.result;
14601
+ const result = Promise.all([readTxtRecords(host), fetchBingSiteAuth(parsed.origin)]).then(
14602
+ ([txt, bingSiteAuthFile]) => ({
14603
+ dnsChecked: txt.checked,
14604
+ googleDns: txt.records.some((record) => /^google-site-verification=/i.test(record)),
14605
+ bingDns: txt.records.some((record) => /^MS=/i.test(record)),
14606
+ bingSiteAuthFile
14607
+ })
14608
+ );
14609
+ cache.set(host, { expires: now + CACHE_TTL_MS, result });
14610
+ return result;
14611
+ }
14612
+ var resolveTxtLoader;
14613
+ function loadResolveTxt() {
14614
+ resolveTxtLoader ??= (async () => {
14615
+ if (typeof process === "undefined" || !process.versions?.node) return null;
14616
+ try {
14617
+ const specifier = "node:dns";
14618
+ const dns = await import(specifier);
14619
+ const resolveTxt = dns?.promises?.resolveTxt ?? dns?.default?.promises?.resolveTxt;
14620
+ return typeof resolveTxt === "function" ? resolveTxt : null;
14621
+ } catch {
14622
+ return null;
14623
+ }
14624
+ })();
14625
+ return resolveTxtLoader;
14626
+ }
14627
+ function domainCandidates(host) {
14628
+ if (/^[\d.]+$/.test(host) || host.includes(":")) return [];
14629
+ const labels = host.split(".").filter(Boolean);
14630
+ const names = [];
14631
+ for (let n = 2; n <= labels.length && names.length < 4; n++) {
14632
+ names.push(labels.slice(-n).join("."));
14633
+ }
14634
+ return names.length > 0 ? names : [host];
14635
+ }
14636
+ async function readTxtRecords(host) {
14637
+ const resolveTxt = await loadResolveTxt();
14638
+ if (!resolveTxt) return { checked: false, records: [] };
14639
+ const lookups = await Promise.all(domainCandidates(host).map((name) => resolveTxtWithin(resolveTxt, name)));
14640
+ return {
14641
+ checked: lookups.every((records) => records !== null),
14642
+ records: lookups.flatMap((records) => records ?? [])
14643
+ };
14644
+ }
14645
+ var NO_RECORD_CODES = /* @__PURE__ */ new Set(["ENODATA", "ENOTFOUND"]);
14646
+ async function resolveTxtWithin(resolveTxt, name) {
14647
+ let timer;
14648
+ const timeout = new Promise((resolve) => {
14649
+ timer = setTimeout(() => resolve(null), DNS_TIMEOUT_MS);
14650
+ });
14651
+ try {
14652
+ return await Promise.race([
14653
+ resolveTxt(name).then(
14654
+ (records) => records.map((chunks) => chunks.join("").trim()),
14655
+ (error) => NO_RECORD_CODES.has(error?.code ?? "") ? [] : null
14656
+ ),
14657
+ timeout
14658
+ ]);
14659
+ } catch {
14660
+ return null;
14661
+ } finally {
14662
+ clearTimeout(timer);
14663
+ }
14664
+ }
14665
+ async function fetchBingSiteAuth(origin) {
14666
+ try {
14667
+ const response = await httpGet(`${origin}/BingSiteAuth.xml`, {
14668
+ timeout: FILE_TIMEOUT_MS,
14669
+ maxBytes: 64 * 1024
14670
+ });
14671
+ return response.status === 200 && typeof response.data === "string" && !looksLikeHtmlFallback(response.data, response.headers["content-type"]) && /<users[\s>]/i.test(response.data);
14672
+ } catch {
14673
+ return false;
14674
+ }
14675
+ }
14676
+
14677
+ // src/audit/checks/bing-optimization.ts
14568
14678
  async function analyzeBingOptimization(html, url) {
14569
14679
  const issues = [];
14570
14680
  const $ = cheerio28.load(html);
@@ -14573,12 +14683,9 @@ async function analyzeBingOptimization(html, url) {
14573
14683
  const detectedIssues = [];
14574
14684
  const bingMetaTag = $('meta[name="msvalidate.01"]').attr("content");
14575
14685
  const hasBingMetaTag = !!bingMetaTag && bingMetaTag.length > 0;
14576
- let hasBingSiteAuth = false;
14577
- try {
14578
- const authResponse = await httpGet(`${baseUrl}/BingSiteAuth.xml`, { timeout: 5e3 });
14579
- hasBingSiteAuth = authResponse.status === 200 && authResponse.data.includes("<?xml") && authResponse.data.toLowerCase().includes("users");
14580
- } catch {
14581
- }
14686
+ const verification = await lookupSiteVerification(url);
14687
+ const hasBingSiteAuth = verification.bingSiteAuthFile;
14688
+ const hasBingDnsRecord = verification.bingDns;
14582
14689
  let hasBingIndexNowKey = false;
14583
14690
  try {
14584
14691
  const keyPatterns = [
@@ -14626,22 +14733,27 @@ async function analyzeBingOptimization(html, url) {
14626
14733
  bingSitemapAccessible = sitemapResponse.status === 200 && sitemapResponse.data.includes("<?xml");
14627
14734
  } catch {
14628
14735
  }
14629
- if (!hasBingMetaTag && !hasBingSiteAuth) {
14736
+ if (!hasBingMetaTag && !hasBingSiteAuth && !hasBingDnsRecord) {
14630
14737
  detectedIssues.push("No Bing site verification");
14631
14738
  issues.push({
14632
14739
  code: "BING_NOT_VERIFIED",
14633
- severity: "warning",
14740
+ // A notice: verification done through Search Console import or a DNS record this run
14741
+ // could not see leaves no trace in the HTML, so this cannot be a warning.
14742
+ severity: "notice",
14634
14743
  category: "ai-readiness",
14635
- title: "Site not verified with Bing Webmaster Tools",
14636
- description: "ChatGPT Search uses Bing's index. Verifying your site with Bing Webmaster Tools ensures proper indexing and access to Bing-specific SEO tools.",
14744
+ title: verification.dnsChecked ? "Site not verified with Bing Webmaster Tools" : "Bing Webmaster Tools verification not visible in the HTML",
14745
+ description: verification.dnsChecked ? "No msvalidate.01 meta tag in the HTML, no MS= TXT record on the domain and no /BingSiteAuth.xml file. ChatGPT Search uses Bing's index. Verifying your site with Bing Webmaster Tools ensures proper indexing and access to Bing-specific SEO tools." : "No msvalidate.01 meta tag in the HTML and no /BingSiteAuth.xml file. The site may be verified through a DNS record instead, which this run could not check. ChatGPT Search uses Bing's index. Verifying your site with Bing Webmaster Tools ensures proper indexing and access to Bing-specific SEO tools.",
14637
14746
  impact: "May reduce visibility in ChatGPT Search results and miss Bing-specific optimization opportunities.",
14638
- howToFix: "Add Bing verification: 1) Go to Bing Webmaster Tools, 2) Add your site, 3) Choose verification method (meta tag or XML file), 4) Add the verification code to your site.",
14747
+ howToFix: "If the site is not yet in Bing Webmaster Tools: 1) Go to Bing Webmaster Tools, 2) Add your site (or import it from Google Search Console), 3) Choose a verification method (meta tag, DNS record or XML file), 4) Add the verification code to your site.",
14639
14748
  affectedUrls: [url],
14640
14749
  details: {
14641
14750
  hasBingMetaTag,
14642
14751
  hasBingSiteAuth,
14752
+ hasBingDnsRecord,
14753
+ dnsChecked: verification.dnsChecked,
14643
14754
  verificationMethods: [
14644
14755
  'Add <meta name="msvalidate.01" content="YOUR_CODE"> to <head>',
14756
+ "Add an MS=YOUR_CODE TXT record to the domain's DNS",
14645
14757
  "Upload BingSiteAuth.xml to root directory"
14646
14758
  ]
14647
14759
  }
@@ -14678,7 +14790,7 @@ async function analyzeBingOptimization(html, url) {
14678
14790
  });
14679
14791
  }
14680
14792
  const hasLocalBusinessSchema = bingPlacesDetected;
14681
- const hasAddressOnPage = $("address").length > 0 || html.toLowerCase().includes("street") || /\d{5}(-\d{4})?/.test(html);
14793
+ const hasAddressOnPage = $("address").text().trim().length > 0 || detectAddresses(html).length > 0;
14682
14794
  if (hasAddressOnPage && !hasLocalBusinessSchema) {
14683
14795
  detectedIssues.push("Local business may not be on Bing Places");
14684
14796
  issues.push({
@@ -14701,6 +14813,8 @@ async function analyzeBingOptimization(html, url) {
14701
14813
  data: {
14702
14814
  hasBingSiteAuth,
14703
14815
  hasBingMetaTag,
14816
+ hasBingDnsRecord,
14817
+ dnsChecked: verification.dnsChecked,
14704
14818
  hasBingIndexNowKey,
14705
14819
  bingPlacesDetected,
14706
14820
  bingSitemapAccessible,
@@ -16207,23 +16321,6 @@ function analyzeColorContrast(html, url) {
16207
16321
  });
16208
16322
  }
16209
16323
  }
16210
- if (cannotDetermine > elementsAnalyzed * 0.8) {
16211
- issues.push({
16212
- code: "A11Y_CONTRAST_UNDETERMINED",
16213
- severity: "notice",
16214
- category: "accessibility",
16215
- title: "Color contrast could not be fully analyzed",
16216
- description: "Most text colors are set via CSS stylesheets rather than inline styles. Full contrast checking requires browser rendering.",
16217
- impact: "Potential contrast issues may exist that were not detected.",
16218
- howToFix: "Use browser DevTools (Lighthouse accessibility audit) or dedicated tools like axe-core for comprehensive contrast checking.",
16219
- affectedUrls: [url],
16220
- details: {
16221
- elementsAnalyzed,
16222
- cannotDetermine,
16223
- recommendation: "Run Lighthouse accessibility audit for complete contrast analysis."
16224
- }
16225
- });
16226
- }
16227
16324
  return {
16228
16325
  issues,
16229
16326
  data: {
@@ -16686,6 +16783,10 @@ function analyzePageResources(html, url) {
16686
16783
  // src/audit/checks/responsive-css.ts
16687
16784
  init_http();
16688
16785
  var cheerio35 = __toESM(require("cheerio"));
16786
+ var MAX_STYLESHEETS = 3;
16787
+ var MAX_STYLESHEET_BYTES = 1024 * 1024;
16788
+ var STYLESHEET_TIMEOUT_MS = 3e3;
16789
+ var RESPONSIVE_SIGNAL = /@media\b|@container\b|\bclamp\(|(?<![\w-])(?:min|max)\(/i;
16689
16790
  function extractMediaQueries(css) {
16690
16791
  const mediaQueries = [];
16691
16792
  const regex = /@media\s*([^{]+)/g;
@@ -16695,36 +16796,31 @@ function extractMediaQueries(css) {
16695
16796
  }
16696
16797
  return mediaQueries;
16697
16798
  }
16799
+ var WIDTH_RULE = /(?:(?:min|max)-width\s*:|width\s*(?:<=|>=|<|>))\s*(\d+(?:\.\d+)?)\s*(px|em|rem)?|(\d+(?:\.\d+)?)\s*(px|em|rem)?\s*(?:<=|>=|<|>)\s*width/gi;
16698
16800
  function classifyBreakpoints(mediaQueries) {
16699
16801
  const breakpoints = /* @__PURE__ */ new Set();
16700
16802
  let hasMobile = false;
16701
16803
  let hasTablet = false;
16702
16804
  let hasDesktop = false;
16703
16805
  for (const query of mediaQueries) {
16704
- const widthMatches = query.match(/(?:min|max)-width\s*:\s*(\d+)(?:px|em|rem)?/gi);
16705
- if (widthMatches) {
16706
- for (const match of widthMatches) {
16707
- const valueMatch = match.match(/(\d+)/);
16708
- if (valueMatch) {
16709
- const value = parseInt(valueMatch[1], 10);
16710
- breakpoints.add(`${value}px`);
16711
- if (value <= 480) {
16712
- hasMobile = true;
16713
- } else if (value <= 768) {
16714
- hasMobile = true;
16715
- } else if (value <= 1024) {
16716
- hasTablet = true;
16717
- } else {
16718
- hasDesktop = true;
16719
- }
16720
- }
16721
- }
16722
- }
16723
- if (/screen\s+and/i.test(query)) {
16724
- }
16725
16806
  if (/print/i.test(query)) {
16726
16807
  continue;
16727
16808
  }
16809
+ for (const match of query.matchAll(WIDTH_RULE)) {
16810
+ const raw = match[1] ?? match[3];
16811
+ const unit = (match[2] ?? match[4] ?? "px").toLowerCase();
16812
+ const value = Math.round(parseFloat(raw) * (unit === "px" ? 1 : 16));
16813
+ breakpoints.add(`${value}px`);
16814
+ if (value <= 480) {
16815
+ hasMobile = true;
16816
+ } else if (value <= 768) {
16817
+ hasMobile = true;
16818
+ } else if (value <= 1024) {
16819
+ hasTablet = true;
16820
+ } else {
16821
+ hasDesktop = true;
16822
+ }
16823
+ }
16728
16824
  }
16729
16825
  return {
16730
16826
  hasMobile,
@@ -16742,6 +16838,7 @@ async function analyzeResponsiveCss(html, url) {
16742
16838
  const allMediaQueries = [];
16743
16839
  const externalSources = [];
16744
16840
  let inlineCount = 0;
16841
+ let hasResponsiveSignals = false;
16745
16842
  $("style").each((_, el) => {
16746
16843
  const css = $(el).text();
16747
16844
  const queries = extractMediaQueries(css);
@@ -16749,39 +16846,43 @@ async function analyzeResponsiveCss(html, url) {
16749
16846
  allMediaQueries.push(...queries);
16750
16847
  inlineCount += queries.length;
16751
16848
  }
16849
+ if (RESPONSIVE_SIGNAL.test(css)) hasResponsiveSignals = true;
16752
16850
  });
16753
- const cssLinks = $('link[rel="stylesheet"][href]').toArray().slice(0, 3).map((el) => $(el).attr("href")).filter((href) => {
16754
- if (!href || href.startsWith("data:")) return false;
16851
+ const sameOrigin = [];
16852
+ let crossOrigin = 0;
16853
+ $("link[rel][href]").each((_, el) => {
16854
+ if (!/\bstylesheet\b/i.test($(el).attr("rel") ?? "")) return;
16855
+ const href = $(el).attr("href");
16856
+ if (!href || href.startsWith("data:")) return;
16755
16857
  try {
16756
- const cssHostname = new URL(href, url).hostname;
16757
- return cssHostname === baseUrl.hostname;
16858
+ const resolved = new URL(href, url);
16859
+ if (resolved.hostname === baseUrl.hostname) sameOrigin.push(resolved.href);
16860
+ else crossOrigin++;
16758
16861
  } catch {
16759
- return false;
16862
+ crossOrigin++;
16760
16863
  }
16761
16864
  });
16762
- const cssPromises = cssLinks.map(async (cssUrl) => {
16763
- try {
16764
- const fullUrl = new URL(cssUrl, url).href;
16765
- const response = await httpGet(fullUrl, {
16766
- timeout: 3e3,
16767
- // Reduced timeout
16768
- validateStatus: () => true
16769
- });
16770
- if (response.status === 200 && typeof response.data === "string") {
16865
+ let fetchFailed = 0;
16866
+ await Promise.all(
16867
+ sameOrigin.slice(0, MAX_STYLESHEETS).map(async (cssUrl) => {
16868
+ try {
16869
+ const response = await httpGet(cssUrl, {
16870
+ timeout: STYLESHEET_TIMEOUT_MS,
16871
+ maxBytes: MAX_STYLESHEET_BYTES
16872
+ });
16873
+ if (typeof response.data !== "string") throw new Error("not text");
16771
16874
  const queries = extractMediaQueries(response.data);
16772
- if (queries.length > 0) {
16773
- allMediaQueries.push(...queries);
16875
+ if (queries.length > 0) allMediaQueries.push(...queries);
16876
+ if (RESPONSIVE_SIGNAL.test(response.data)) {
16877
+ hasResponsiveSignals = true;
16774
16878
  externalSources.push(cssUrl);
16775
16879
  }
16880
+ } catch {
16881
+ fetchFailed++;
16776
16882
  }
16777
- } catch {
16778
- }
16779
- });
16780
- await Promise.race([
16781
- Promise.all(cssPromises),
16782
- new Promise((resolve) => setTimeout(resolve, 6e3))
16783
- // 6s overall timeout
16784
- ]);
16883
+ })
16884
+ );
16885
+ const stylesheetsCovered = crossOrigin === 0 && fetchFailed === 0 && sameOrigin.length <= MAX_STYLESHEETS;
16785
16886
  const hasMediaQueries = allMediaQueries.length > 0;
16786
16887
  const { hasMobile, hasTablet, hasDesktop, breakpoints } = classifyBreakpoints(allMediaQueries);
16787
16888
  if (!hasViewport) {
@@ -16796,21 +16897,22 @@ async function analyzeResponsiveCss(html, url) {
16796
16897
  affectedUrls: [url]
16797
16898
  });
16798
16899
  }
16799
- if (!hasMediaQueries) {
16900
+ if (!hasResponsiveSignals && stylesheetsCovered) {
16800
16901
  issues.push({
16801
16902
  code: "RESPONSIVE_NO_MEDIA_QUERIES",
16802
16903
  severity: "warning",
16803
16904
  category: "mobile",
16804
16905
  title: "No CSS media queries detected",
16805
- description: "No @media queries found in inline styles or first-party stylesheets.",
16906
+ description: "No @media or @container queries and no fluid sizing (clamp, min, max) found in inline styles or first-party stylesheets.",
16806
16907
  impact: "Without media queries, the site likely uses fixed layouts that do not adapt to different screen sizes. This hurts mobile usability and SEO.",
16807
16908
  howToFix: "Add CSS media queries to adapt layout for different screen sizes. Consider using a mobile-first approach with min-width breakpoints.",
16808
16909
  affectedUrls: [url],
16809
16910
  details: {
16810
- suggestion: "Common breakpoints: 480px (mobile), 768px (tablet), 1024px (desktop)"
16911
+ suggestion: "Common breakpoints: 480px (mobile), 768px (tablet), 1024px (desktop)",
16912
+ stylesheetsScanned: sameOrigin.length
16811
16913
  }
16812
16914
  });
16813
- } else if (!hasMobile && !hasTablet) {
16915
+ } else if (hasMediaQueries && !hasMobile && !hasTablet) {
16814
16916
  issues.push({
16815
16917
  code: "RESPONSIVE_NO_MOBILE_BREAKPOINT",
16816
16918
  severity: "warning",
@@ -16836,6 +16938,8 @@ async function analyzeResponsiveCss(html, url) {
16836
16938
  inline: inlineCount,
16837
16939
  external: externalSources
16838
16940
  },
16941
+ hasResponsiveSignals,
16942
+ stylesheetsCovered,
16839
16943
  breakpoints,
16840
16944
  hasMobileBreakpoint: hasMobile,
16841
16945
  hasTabletBreakpoint: hasTablet,
@@ -17474,7 +17578,22 @@ var urlSafetyDatabase = {
17474
17578
 
17475
17579
  // src/audit/checks/tracking-verification.ts
17476
17580
  var cheerio36 = __toESM(require("cheerio"));
17477
- function analyzeTrackingVerification(html, url) {
17581
+ var OTHER_ANALYTICS = [
17582
+ { name: "Cloudflare Web Analytics", pattern: /cloudflareinsights\.com\/beacon(?:\.min)?\.js/i },
17583
+ { name: "Plausible", pattern: /plausible\.io\/js\/|<script[^>]*data-domain=[^>]*plausible|<script[^>]*plausible[^>]*data-domain=/i },
17584
+ { name: "Fathom", pattern: /cdn\.usefathom\.com/i },
17585
+ // data-website-id is Umami's attribute, on the hosted script and on self-hosted ones alike.
17586
+ { name: "Umami", pattern: /<script[^>]*\bdata-website-id=/i },
17587
+ { name: "PostHog", pattern: /posthog\.init\s*\(|\.posthog\.com\//i },
17588
+ { name: "Matomo", pattern: /matomo\.js|piwik\.js|_paq\s*(?:=|\.push)/i },
17589
+ { name: "Simple Analytics", pattern: /scripts\.simpleanalyticscdn\.com/i },
17590
+ { name: "Mixpanel", pattern: /cdn\.mxpnl\.com|mixpanel\.init\s*\(/i },
17591
+ { name: "Segment", pattern: /cdn\.segment\.com/i }
17592
+ ];
17593
+ function detectOtherAnalytics(html) {
17594
+ return OTHER_ANALYTICS.filter(({ pattern }) => pattern.test(html)).map(({ name }) => name);
17595
+ }
17596
+ async function analyzeTrackingVerification(html, url) {
17478
17597
  const issues = [];
17479
17598
  const $ = cheerio36.load(html);
17480
17599
  const ga4Match = html.match(/gtag\s*\(\s*['"]config['"]\s*,\s*['"](G-[A-Z0-9]+)['"]/i) || html.match(/googletagmanager\.com\/gtag\/js\?id=(G-[A-Z0-9]+)/i);
@@ -17484,10 +17603,16 @@ function analyzeTrackingVerification(html, url) {
17484
17603
  const hasGtm = !!gtmMatch;
17485
17604
  const gtmContainerId = gtmMatch ? gtmMatch[1] : void 0;
17486
17605
  const hasGtmNoscript = /googletagmanager\.com\/ns\.html\?id=GTM-[A-Z0-9]+/i.test(html);
17606
+ const otherAnalytics = detectOtherAnalytics(html);
17607
+ const verification = await lookupSiteVerification(url);
17487
17608
  const gscMeta = $('meta[name="google-site-verification"]').attr("content");
17488
- const hasGscVerification = !!(gscMeta && gscMeta.length > 10);
17609
+ const hasGscMeta = !!(gscMeta && gscMeta.length > 10);
17610
+ const hasGscVerification = hasGscMeta || verification.googleDns;
17611
+ const gscSource = hasGscMeta ? "meta" : verification.googleDns ? "dns" : void 0;
17489
17612
  const bingMeta = $('meta[name="msvalidate.01"]').attr("content");
17490
- const hasBingVerification = !!(bingMeta && bingMeta.length > 10);
17613
+ const hasBingMeta = !!(bingMeta && bingMeta.length > 10);
17614
+ const hasBingVerification = hasBingMeta || verification.bingDns || verification.bingSiteAuthFile;
17615
+ const bingSource = hasBingMeta ? "meta" : verification.bingDns ? "dns" : verification.bingSiteAuthFile ? "file" : void 0;
17491
17616
  const jsonLdScripts = $('script[type="application/ld+json"]');
17492
17617
  const hasMicrodata = $('[itemtype*="schema.org"]').length > 0;
17493
17618
  const hasJsonLd = jsonLdScripts.length > 0;
@@ -17509,16 +17634,19 @@ function analyzeTrackingVerification(html, url) {
17509
17634
  const data = {
17510
17635
  ga4: { detected: hasGa4, measurementId: ga4MeasurementId },
17511
17636
  gtm: { detected: hasGtm, containerId: gtmContainerId, hasNoscript: hasGtmNoscript },
17512
- gsc: { verified: hasGscVerification, verificationCode: gscMeta },
17513
- bing: { verified: hasBingVerification, verificationCode: bingMeta },
17637
+ otherAnalytics,
17638
+ gsc: { verified: hasGscVerification, verificationCode: gscMeta, source: gscSource },
17639
+ bing: { verified: hasBingVerification, verificationCode: bingMeta, source: bingSource },
17640
+ dnsChecked: verification.dnsChecked,
17514
17641
  schemaOrg: { detected: hasJsonLd || hasMicrodata, types: schemaTypes, hasValidJson },
17515
17642
  sitemap: { inRobotsTxt: false }
17516
17643
  // Set later if we check robots.txt
17517
17644
  };
17518
- if (!hasGa4 && !hasGtm) {
17645
+ if (!hasGa4 && !hasGtm && otherAnalytics.length === 0) {
17519
17646
  issues.push({
17520
17647
  code: "GA4_MISSING",
17521
- severity: "warning",
17648
+ // A notice: which analytics a site runs is a choice, not an SEO defect.
17649
+ severity: "notice",
17522
17650
  category: "on-page",
17523
17651
  title: "Google Analytics 4 not detected",
17524
17652
  description: "No GA4 tracking code found. GA4 is essential for measuring organic traffic and conversions.",
@@ -17528,40 +17656,47 @@ function analyzeTrackingVerification(html, url) {
17528
17656
  });
17529
17657
  }
17530
17658
  if (!hasGscVerification) {
17659
+ const dnsChecked = verification.dnsChecked;
17531
17660
  issues.push({
17532
17661
  code: "GSC_VERIFICATION_MISSING",
17533
- severity: "warning",
17662
+ // A notice: a site verified by DNS, Google Analytics or Tag Manager leaves no trace here.
17663
+ severity: "notice",
17534
17664
  category: "on-page",
17535
- title: "Google Search Console verification not found",
17536
- description: "No GSC verification meta tag detected. GSC provides essential data about your search performance.",
17665
+ title: dnsChecked ? "Google Search Console verification not found" : "Google Search Console verification not visible in the HTML",
17666
+ description: dnsChecked ? "No google-site-verification meta tag in the HTML and no google-site-verification TXT record on the domain. GSC provides essential data about your search performance." : "No google-site-verification meta tag in the HTML. The site may be verified through a DNS TXT record instead, which this run could not check. GSC provides essential data about your search performance.",
17537
17667
  impact: "Missing critical insights into Google search visibility, impressions, and clicks.",
17538
- howToFix: "Add your GSC verification meta tag to the <head> section. Get it from search.google.com/search-console.",
17539
- affectedUrls: [url]
17668
+ howToFix: dnsChecked ? "Verify the site in Google Search Console: add the meta tag to <head>, or a TXT record to the domain's DNS. Get it from search.google.com/search-console." : "If the site is not yet verified in Google Search Console, add the meta tag to <head> or a TXT record to the domain's DNS. Get it from search.google.com/search-console.",
17669
+ affectedUrls: [url],
17670
+ details: { dnsChecked }
17540
17671
  });
17541
17672
  }
17542
17673
  if (!hasBingVerification) {
17674
+ const dnsChecked = verification.dnsChecked;
17543
17675
  issues.push({
17544
17676
  code: "BING_VERIFICATION_MISSING",
17545
17677
  severity: "notice",
17546
17678
  category: "on-page",
17547
- title: "Bing Webmaster Tools verification not found",
17548
- description: "No Bing verification meta tag detected. Bing/Yahoo account for 5-8% of search traffic.",
17679
+ title: dnsChecked ? "Bing Webmaster Tools verification not found" : "Bing Webmaster Tools verification not visible in the HTML",
17680
+ description: dnsChecked ? "No msvalidate.01 meta tag in the HTML, no MS= TXT record on the domain and no /BingSiteAuth.xml file. Bing/Yahoo account for 5-8% of search traffic." : "No msvalidate.01 meta tag in the HTML and no /BingSiteAuth.xml file. The site may be verified through a DNS record instead, which this run could not check. Bing/Yahoo account for 5-8% of search traffic.",
17549
17681
  impact: "Missing insights on Bing/Yahoo search traffic.",
17550
- howToFix: "Add Bing Webmaster Tools verification for insights on Bing/Yahoo traffic. Get it from bing.com/webmasters.",
17551
- affectedUrls: [url]
17682
+ howToFix: "If the site is not yet in Bing Webmaster Tools, add it there: import it from Google Search Console, or verify with the meta tag, a DNS record or BingSiteAuth.xml. Get it from bing.com/webmasters.",
17683
+ affectedUrls: [url],
17684
+ details: { dnsChecked }
17552
17685
  });
17553
17686
  }
17554
17687
  if (!hasGtm) {
17555
- issues.push({
17556
- code: "GTM_MISSING",
17557
- severity: "notice",
17558
- category: "on-page",
17559
- title: "Google Tag Manager not detected",
17560
- description: "GTM not found. While not required, GTM makes it easier to manage tracking tags without code changes.",
17561
- impact: "Tag management requires developer involvement for each change.",
17562
- howToFix: "Consider using GTM for easier tag management. It allows marketing to add tags without developer help.",
17563
- affectedUrls: [url]
17564
- });
17688
+ if (otherAnalytics.length === 0) {
17689
+ issues.push({
17690
+ code: "GTM_MISSING",
17691
+ severity: "notice",
17692
+ category: "on-page",
17693
+ title: "Google Tag Manager not detected",
17694
+ description: "GTM not found. While not required, GTM makes it easier to manage tracking tags without code changes.",
17695
+ impact: "Tag management requires developer involvement for each change.",
17696
+ howToFix: "Consider using GTM for easier tag management. It allows marketing to add tags without developer help.",
17697
+ affectedUrls: [url]
17698
+ });
17699
+ }
17565
17700
  } else if (!hasGtmNoscript) {
17566
17701
  issues.push({
17567
17702
  code: "GTM_NOSCRIPT_MISSING",
@@ -17600,6 +17735,113 @@ function analyzeTrackingVerification(html, url) {
17600
17735
  return { issues, data };
17601
17736
  }
17602
17737
 
17738
+ // src/audit/meta-refresh.ts
17739
+ var cheerio37 = __toESM(require("cheerio"));
17740
+ function metaRefreshTarget(html, pageUrl) {
17741
+ if (!/http-equiv\s*=\s*["']?refresh/i.test(html)) return null;
17742
+ const $ = cheerio37.load(html);
17743
+ for (const el of $("meta[http-equiv]").toArray()) {
17744
+ if (($(el).attr("http-equiv") || "").trim().toLowerCase() !== "refresh") continue;
17745
+ const content = $(el).attr("content") || "";
17746
+ const match = content.match(/^\s*(\d+(?:\.\d+)?)\s*[;,]\s*(?:url\s*=\s*)?['"]?\s*([^'"\s>]+)/i);
17747
+ if (!match) continue;
17748
+ if (Number(match[1]) > 5) continue;
17749
+ try {
17750
+ const target = new URL(match[2], pageUrl);
17751
+ if (target.protocol !== "http:" && target.protocol !== "https:") continue;
17752
+ const strip = (u) => u.origin + (u.pathname.replace(/\/+$/, "") || "/") + u.search;
17753
+ if (strip(target) === strip(new URL(pageUrl))) continue;
17754
+ return target.toString();
17755
+ } catch {
17756
+ continue;
17757
+ }
17758
+ }
17759
+ return null;
17760
+ }
17761
+
17762
+ // src/audit/dedupe.ts
17763
+ var REDUNDANT_WHEN_PRESENT = [
17764
+ // The same tag, reported by three checks.
17765
+ { keep: "VIEWPORT_MISSING", drop: ["HTML_NO_VIEWPORT", "RESPONSIVE_NO_VIEWPORT"] },
17766
+ { keep: "LANG_ATTR_MISSING", drop: ["HTML_NO_LANG"] },
17767
+ { keep: "APPLE_TOUCH_ICON_MISSING", drop: ["HTML_NO_APPLE_TOUCH_ICON"] },
17768
+ { keep: "LLMS_TXT_MISSING", drop: ["AI_NO_LLMS_TXT"] },
17769
+ { keep: "INDEXNOW_NOT_IMPLEMENTED", drop: ["BING_NO_INDEXNOW"] },
17770
+ { keep: "BING_VERIFICATION_MISSING", drop: ["BING_NOT_VERIFIED"] },
17771
+ { keep: "SITEMAP_MISSING", drop: ["BING_SITEMAP_MISSING", "SITEMAP_NOT_IN_ROBOTS"] },
17772
+ { keep: "ROBOTS_TXT_MISSING", drop: ["SITEMAP_NOT_IN_ROBOTS"] },
17773
+ { keep: "IMAGES_MISSING_DIMENSIONS", drop: ["IMG_NO_DIMENSIONS"] },
17774
+ { keep: "NO_MODERN_IMAGE_FORMATS", drop: ["IMAGE_NOT_WEBP_AVIF"] },
17775
+ { keep: "SOCIAL_PROFILES_NOT_LINKED", drop: ["NO_SOCIAL_PROFILES"] },
17776
+ { keep: "NO_EXPERIENCE_SIGNALS", drop: ["AI_NO_EXPERIENCE_SIGNALS"] },
17777
+ // No structured data at all makes every "your schema lacks X" finding moot.
17778
+ {
17779
+ keep: "SCHEMA_MISSING",
17780
+ drop: [
17781
+ "SCHEMA_ORG_MISSING",
17782
+ "NO_ORGANIZATION_SCHEMA",
17783
+ "ORGANIZATION_SCHEMA_MISSING",
17784
+ "NO_ENTITY_SCHEMA",
17785
+ "NO_DATE_MODIFIED_SCHEMA",
17786
+ "NO_MAIN_ENTITY_OF_PAGE",
17787
+ "AUTHOR_NO_SCHEMA",
17788
+ "AI_NO_REVIEW_SCHEMA",
17789
+ "NO_SCHEMA_SAMEAS"
17790
+ ]
17791
+ },
17792
+ { keep: "NO_ORGANIZATION_SCHEMA", drop: ["ORGANIZATION_SCHEMA_MISSING"] },
17793
+ // No title, no H1: the findings about their wording are implied.
17794
+ { keep: "TITLE_MISSING", drop: ["LOW_CTR_POTENTIAL", "NO_KEYWORDS_IN_TITLE", "TITLE_TOO_SHORT"] },
17795
+ { keep: "META_DESC_MISSING", drop: ["LOW_CTR_POTENTIAL"] },
17796
+ { keep: "H1_MISSING", drop: ["NO_VISIBLE_HEADLINE", "NO_KEYWORDS_IN_H1", "H1_MISSING_KEYWORD"] },
17797
+ // Client-side rendering, diagnosed once. CLIENT_SIDE_RENDERING carries the per-framework fix.
17798
+ {
17799
+ keep: "CLIENT_SIDE_RENDERING",
17800
+ drop: [
17801
+ "REACT_CSR_DETECTED",
17802
+ "VUE_CSR_DETECTED",
17803
+ "ANGULAR_NO_UNIVERSAL",
17804
+ "SPA_WITHOUT_SSR",
17805
+ "EMPTY_BODY_CONTENT",
17806
+ "HIGH_JS_RENDERING_RATIO",
17807
+ "AI_LOW_HTML_READABILITY"
17808
+ ]
17809
+ },
17810
+ // An empty body has no vocabulary to be undiverse.
17811
+ { keep: "EMPTY_BODY_CONTENT", drop: ["THIN_CONTENT", "LOW_CONTENT_ENTROPY", "HIGH_CONTENT_REPETITION", "LOW_DWELL_TIME_CONTENT", "LOW_TEXT_HTML_RATIO"] },
17812
+ { keep: "THIN_CONTENT", drop: ["LOW_DWELL_TIME_CONTENT"] },
17813
+ { keep: "NO_INTERNAL_LINKS", drop: ["NO_CONTENT_INTERNAL_LINKS", "HIGH_ORPHAN_RISK", "LINKS_NO_INTERNAL"] },
17814
+ // No testimonials on the page, said once instead of six times.
17815
+ { keep: "NO_SOCIAL_PROOF", drop: ["BOFU_NO_SOCIAL_PROOF", "AI_NO_TESTIMONIALS", "AI_NO_REVIEW_INTEGRATION", "AI_NO_REVIEW_PLATFORMS", "AI_WEAK_REVIEW_ECOSYSTEM"] },
17816
+ { keep: "AI_NO_TESTIMONIALS", drop: ["AI_NO_REVIEW_INTEGRATION", "AI_NO_REVIEW_PLATFORMS", "AI_WEAK_REVIEW_ECOSYSTEM"] },
17817
+ // The brand-score composite restates the brand-definition finding it is built from.
17818
+ { keep: "AI_NO_BRAND_DEFINITION", drop: ["AI_LOW_BRAND_SCORE", "NO_ENTITY_DEFINITION"] }
17819
+ ];
17820
+ function dedupeIssues(issues) {
17821
+ const present = new Set(issues.map((i) => i.code));
17822
+ const redundant = /* @__PURE__ */ new Set();
17823
+ for (const rule of REDUNDANT_WHEN_PRESENT) {
17824
+ const fires = typeof rule.keep === "string" ? present.has(rule.keep) : [...present].some((code) => rule.keep.test(code));
17825
+ if (!fires) continue;
17826
+ for (const code of rule.drop) redundant.add(code);
17827
+ }
17828
+ const kept = /* @__PURE__ */ new Map();
17829
+ const out = [];
17830
+ for (const issue of issues) {
17831
+ if (redundant.has(issue.code)) continue;
17832
+ const earlier = kept.get(issue.code);
17833
+ if (earlier) {
17834
+ const urls = /* @__PURE__ */ new Set([...earlier.affectedUrls || [], ...issue.affectedUrls || []]);
17835
+ earlier.affectedUrls = [...urls];
17836
+ continue;
17837
+ }
17838
+ const copy = { ...issue };
17839
+ kept.set(issue.code, copy);
17840
+ out.push(copy);
17841
+ }
17842
+ return out;
17843
+ }
17844
+
17603
17845
  // src/audit/engine.ts
17604
17846
  function sameUrl(a, b) {
17605
17847
  try {
@@ -17612,15 +17854,15 @@ function sameUrl(a, b) {
17612
17854
  }
17613
17855
  }
17614
17856
  async function runFullAudit(options) {
17615
- const { url, checkBrokenLinks = false, checkHreflangUrls = false, checkCanonicalChain = false, cruxApiKey, checksLimit = 280, premiumChecks } = options;
17857
+ const { checkBrokenLinks = false, checkHreflangUrls = false, checkCanonicalChain = false, cruxApiKey, checksLimit = 280, premiumChecks } = options;
17858
+ let url = options.url;
17616
17859
  const auditRoot = isSiteRoot(url) || sameUrl(url, options.auditRootUrl ?? url);
17617
17860
  const allIssues = [];
17618
17861
  const pages = [];
17619
17862
  const tier = checksLimit <= 50 ? "free" : checksLimit <= 100 ? "registered" : "paid";
17620
17863
  const runPremiumChecks = tier === "paid" && !!premiumChecks;
17621
17864
  const runExtendedChecks = tier !== "free";
17622
- const parsedUrl = new URL(url);
17623
- const domain = parsedUrl.hostname;
17865
+ let domain = new URL(url).hostname;
17624
17866
  console.error(`
17625
17867
  \u{1F50D} Running comprehensive SEO audit on ${url}...
17626
17868
  `);
@@ -17647,7 +17889,9 @@ async function runFullAudit(options) {
17647
17889
  timeout: 3e4,
17648
17890
  validateStatus: () => true
17649
17891
  }).catch((err) => {
17650
- console.error("Main page fetch failed:", err instanceof Error ? { message: err.message, cause: err.cause } : err);
17892
+ const cause = err instanceof Error ? err.cause : void 0;
17893
+ const causeText = cause instanceof Error ? ` (${cause.code || cause.message})` : "";
17894
+ console.error(`Main page fetch failed: ${err instanceof Error ? err.message : String(err)}${causeText}`);
17651
17895
  return { error: err, data: "", headers: {} };
17652
17896
  })
17653
17897
  ]);
@@ -17670,7 +17914,7 @@ async function runFullAudit(options) {
17670
17914
  });
17671
17915
  return createReport(url, domain, allIssues, pages, { kind: "unreachable" });
17672
17916
  }
17673
- const statusCode = fetchResult.status;
17917
+ let statusCode = fetchResult.status;
17674
17918
  if (statusCode >= 400) {
17675
17919
  const blocked = statusCode === 401 || statusCode === 403 || statusCode === 429;
17676
17920
  allIssues.push({
@@ -17683,10 +17927,42 @@ async function runFullAudit(options) {
17683
17927
  pages.push({ url, statusCode, wordCount: 0, loadTime: 0, issues: allIssues.map((i) => i.code) });
17684
17928
  return createReport(url, domain, allIssues, pages, { kind: "http-error", statusCode });
17685
17929
  }
17686
- const body = typeof fetchResult.data === "string" ? fetchResult.data : JSON.stringify(fetchResult.data ?? "");
17930
+ const asText = (data) => typeof data === "string" ? data : JSON.stringify(data ?? "");
17931
+ let body = asText(fetchResult.data);
17932
+ let headers = fetchResult.headers;
17933
+ const requestedUrl = url;
17934
+ const refreshHops = [];
17935
+ for (let hop = 0; hop < 3; hop++) {
17936
+ const target = metaRefreshTarget(body, url);
17937
+ if (!target) break;
17938
+ try {
17939
+ const next = await httpGet(target, { timeout: 3e4, validateStatus: () => true });
17940
+ if (next.status >= 400) break;
17941
+ refreshHops.push(target);
17942
+ url = target;
17943
+ body = asText(next.data);
17944
+ headers = next.headers;
17945
+ statusCode = next.status;
17946
+ } catch {
17947
+ break;
17948
+ }
17949
+ }
17950
+ if (refreshHops.length > 0) {
17951
+ domain = new URL(url).hostname;
17952
+ allIssues.push({
17953
+ code: "META_REFRESH_REDIRECT",
17954
+ severity: "notice",
17955
+ category: "crawlability",
17956
+ title: "Page redirects with a meta refresh",
17957
+ description: `${requestedUrl} answers HTTP 200 with a <meta http-equiv="refresh"> to ${url}, so that page was audited. Search engines follow an instant meta refresh, but a server redirect is the documented way to move a URL and the only one every crawler and tool honours.`,
17958
+ impact: "Some crawlers and link-checking tools stop at the stub; link signals to the original URL may not all pass to the target.",
17959
+ howToFix: "Return a 301 (or 308) redirect from the server or CDN for this URL instead of a meta refresh.",
17960
+ affectedUrls: [requestedUrl],
17961
+ details: { from: requestedUrl, to: url, hops: refreshHops }
17962
+ });
17963
+ }
17687
17964
  const html = body.replace(/<\/?noscript[^>]*>/gi, "");
17688
- const headers = fetchResult.headers;
17689
- console.error(`\u{1F4DD} Phase 2: Running synchronous HTML checks (tier: ${tier}, limit: ${checksLimit})...`);
17965
+ console.error("\u{1F4DD} Phase 2: Running HTML checks...");
17690
17966
  const onPageResult = analyzeOnPage(html, url, headers, { auditRoot });
17691
17967
  const structuredDataResult = analyzeStructuredData(html, url);
17692
17968
  const mobileResult = analyzeMobile(html, url);
@@ -17701,7 +17977,6 @@ async function runFullAudit(options) {
17701
17977
  const urlSafetyResult = analyzeUrlSafety(url);
17702
17978
  const domSizeResult = analyzeDomSize(html, url);
17703
17979
  const imageDimensionsResult = analyzeImageDimensions(html, url);
17704
- const trackingResult = analyzeTrackingVerification(html, url);
17705
17980
  const anchorResult = runExtendedChecks ? analyzeAnchorText(html, url) : { issues: [], data: {} };
17706
17981
  const localSeoResult = runExtendedChecks ? analyzeLocalSEO(html, url) : { issues: [], data: {} };
17707
17982
  const modernImageResult = runExtendedChecks ? analyzeModernImages(html, url) : { issues: [], data: {} };
@@ -17776,8 +18051,7 @@ async function runFullAudit(options) {
17776
18051
  ...colorContrastResult.issues,
17777
18052
  ...pageResourcesResult.issues,
17778
18053
  ...urlSafetyResult.issues,
17779
- ...paginationResult.issues,
17780
- ...trackingResult.issues
18054
+ ...paginationResult.issues
17781
18055
  );
17782
18056
  if (!structuredDataResult.data.hasSchema) {
17783
18057
  const suggestions = suggestSchemaTypes(html, url);
@@ -17823,6 +18097,7 @@ async function runFullAudit(options) {
17823
18097
  asyncChecks.push(safeAsyncIssues("Certificate", async () => (await checkCertificate(url)).issues));
17824
18098
  asyncChecks.push(safeAsync("Redirects", () => analyzeRedirects(url)));
17825
18099
  asyncChecks.push(safeAsync("Robots Validation", () => validateRobotsTxt(url)));
18100
+ asyncChecks.push(safeAsync("Tracking Verification", () => analyzeTrackingVerification(html, url)));
17826
18101
  asyncChecks.push(safeAsyncIssues("Crawler Access", async () => (await analyzeCrawlerAccess(url)).issues));
17827
18102
  asyncChecks.push(safeAsync("Canonical Domain", () => analyzeCanonicalDomain(url)));
17828
18103
  asyncChecks.push(safeAsyncIssues("Framework Detection", async () => {
@@ -17854,10 +18129,10 @@ async function runFullAudit(options) {
17854
18129
  if (cruxApiKey) {
17855
18130
  asyncChecks.push(safeAsyncIssues("Core Web Vitals", async () => {
17856
18131
  const cwv = await fetchCoreWebVitals(url, cruxApiKey);
17857
- const issues = [];
18132
+ const issues2 = [];
17858
18133
  if (cwv) {
17859
18134
  if (cwv.lcp?.rating === "poor") {
17860
- issues.push({
18135
+ issues2.push({
17861
18136
  code: "LCP_POOR",
17862
18137
  severity: "error",
17863
18138
  category: "performance",
@@ -17870,7 +18145,7 @@ async function runFullAudit(options) {
17870
18145
  });
17871
18146
  }
17872
18147
  if (cwv.cls?.rating === "poor") {
17873
- issues.push({
18148
+ issues2.push({
17874
18149
  code: "CLS_POOR",
17875
18150
  severity: "error",
17876
18151
  category: "performance",
@@ -17883,14 +18158,15 @@ async function runFullAudit(options) {
17883
18158
  });
17884
18159
  }
17885
18160
  }
17886
- return issues;
18161
+ return issues2;
17887
18162
  }));
17888
18163
  }
17889
18164
  }
17890
18165
  const asyncResults = await Promise.all(asyncChecks);
17891
- for (const issues of asyncResults) {
17892
- allIssues.push(...issues);
18166
+ for (const issues2 of asyncResults) {
18167
+ allIssues.push(...issues2);
17893
18168
  }
18169
+ const issues = dedupeIssues(allIssues);
17894
18170
  pages.push({
17895
18171
  url,
17896
18172
  statusCode,
@@ -17900,10 +18176,10 @@ async function runFullAudit(options) {
17900
18176
  h1: onPageResult.data.h1s,
17901
18177
  wordCount: onPageResult.data.wordCount,
17902
18178
  loadTime: perfData.loadTime,
17903
- issues: allIssues.map((i) => i.code)
18179
+ issues: issues.map((i) => i.code)
17904
18180
  });
17905
18181
  console.error("\n\u2705 Audit complete!\n");
17906
- return createReport(url, domain, allIssues, pages);
18182
+ return createReport(url, domain, issues, pages);
17907
18183
  }
17908
18184
  function createReport(url, domain, issues, pages, pageFailure) {
17909
18185
  const healthScore = calculateHealthScore(issues);
@@ -18068,10 +18344,10 @@ function groupIssuesByCategory(issues) {
18068
18344
  }
18069
18345
 
18070
18346
  // src/audit/checks/duplicate-content.ts
18071
- var cheerio37 = __toESM(require("cheerio"));
18347
+ var cheerio38 = __toESM(require("cheerio"));
18072
18348
  var import_crypto = require("crypto");
18073
18349
  function extractContentHash(html, url) {
18074
- const $ = cheerio37.load(html);
18350
+ const $ = cheerio38.load(html);
18075
18351
  $("script, style, nav, header, footer, aside, .nav, .header, .footer, .sidebar").remove();
18076
18352
  const title = $("title").text().trim();
18077
18353
  const bodyText = $("body").text().replace(/\s+/g, " ").trim();
@@ -19299,7 +19575,7 @@ var PRIORITY_WEIGHTS = {
19299
19575
  };
19300
19576
 
19301
19577
  // src/keywords/engine.ts
19302
- var cheerio39 = __toESM(require("cheerio"));
19578
+ var cheerio40 = __toESM(require("cheerio"));
19303
19579
  init_http();
19304
19580
 
19305
19581
  // src/keywords/prioritizer.ts
@@ -19596,7 +19872,7 @@ function enrichKeywordsWithEstimates(keywords) {
19596
19872
 
19597
19873
  // src/keywords/sources/free-sources.ts
19598
19874
  init_http();
19599
- var cheerio38 = __toESM(require("cheerio"));
19875
+ var cheerio39 = __toESM(require("cheerio"));
19600
19876
  var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
19601
19877
  async function getPeopleAlsoAsk(query) {
19602
19878
  try {
@@ -19605,7 +19881,7 @@ async function getPeopleAlsoAsk(query) {
19605
19881
  headers: { "User-Agent": USER_AGENT },
19606
19882
  timeout: 1e4
19607
19883
  });
19608
- const $ = cheerio38.load(response.data);
19884
+ const $ = cheerio39.load(response.data);
19609
19885
  const questions = [];
19610
19886
  $("[data-sgrd]").each((_, el) => {
19611
19887
  const text = $(el).text().trim();
@@ -19631,7 +19907,7 @@ async function getRelatedSearches(query) {
19631
19907
  headers: { "User-Agent": USER_AGENT },
19632
19908
  timeout: 1e4
19633
19909
  });
19634
- const $ = cheerio38.load(response.data);
19910
+ const $ = cheerio39.load(response.data);
19635
19911
  const related = [];
19636
19912
  $("div[data-ved] a").each((_, el) => {
19637
19913
  const href = $(el).attr("href");
@@ -19721,7 +19997,7 @@ async function analyzeCompetitorTitles(query) {
19721
19997
  headers: { "User-Agent": USER_AGENT },
19722
19998
  timeout: 1e4
19723
19999
  });
19724
- const $ = cheerio38.load(response.data);
20000
+ const $ = cheerio39.load(response.data);
19725
20001
  const titles = [];
19726
20002
  const keywords = /* @__PURE__ */ new Set();
19727
20003
  $("h3").each((_, el) => {
@@ -20054,7 +20330,7 @@ async function fetchPageMeta(url) {
20054
20330
  const response = await httpGet(url, {
20055
20331
  timeout: 1e4
20056
20332
  });
20057
- const $ = cheerio39.load(response.data);
20333
+ const $ = cheerio40.load(response.data);
20058
20334
  return {
20059
20335
  url,
20060
20336
  title: $("title").text().trim() || void 0,
@@ -20185,20 +20461,17 @@ async function extractSeedKeywords(url) {
20185
20461
  const response = await httpGet(url, {
20186
20462
  timeout: 1e4
20187
20463
  });
20188
- const $ = cheerio39.load(response.data);
20464
+ const $ = cheerio40.load(response.data);
20189
20465
  const seeds = /* @__PURE__ */ new Set();
20190
- const title = $("title").text().toLowerCase();
20191
- const titleWords = title.split(/[\s\-|:]+/).filter((w) => w.length > 3);
20192
- titleWords.forEach((w) => seeds.add(w));
20193
- const h1 = $("h1").first().text().toLowerCase();
20194
- const h1Words = h1.split(/[\s\-|:]+/).filter((w) => w.length > 3);
20195
- h1Words.forEach((w) => seeds.add(w));
20466
+ const words = (text) => text.toLowerCase().split(/[\s\-|:]+/).map((w) => w.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "")).filter((w) => w.length > 3);
20467
+ words($("title").text()).forEach((w) => seeds.add(w));
20468
+ words($("h1").first().text()).forEach((w) => seeds.add(w));
20196
20469
  const metaKeywords = $('meta[name="keywords"]').attr("content");
20197
20470
  if (metaKeywords) {
20198
20471
  metaKeywords.split(",").forEach((k) => seeds.add(k.trim().toLowerCase()));
20199
20472
  }
20200
20473
  const desc = $('meta[name="description"]').attr("content")?.toLowerCase() || "";
20201
- const descWords = desc.split(/\s+/).filter((w) => w.length > 5);
20474
+ const descWords = words(desc).filter((w) => w.length > 5);
20202
20475
  descWords.slice(0, 5).forEach((w) => seeds.add(w));
20203
20476
  return Array.from(seeds).slice(0, 10);
20204
20477
  } catch {
@@ -20251,7 +20524,7 @@ function parseJsonReply(text) {
20251
20524
  }
20252
20525
 
20253
20526
  // src/keywords/site-crawler.ts
20254
- var cheerio40 = __toESM(require("cheerio"));
20527
+ var cheerio41 = __toESM(require("cheerio"));
20255
20528
  init_http();
20256
20529
  var EXCLUDED_PATHS = [
20257
20530
  "/cdn-cgi/",
@@ -20364,7 +20637,7 @@ async function crawlPage(url, timeout) {
20364
20637
  validateStatus: (status) => status === 200
20365
20638
  });
20366
20639
  const html = response.data;
20367
- const $ = cheerio40.load(html);
20640
+ const $ = cheerio41.load(html);
20368
20641
  $('script, style, noscript, iframe, nav, footer, header, aside, [role="navigation"]').remove();
20369
20642
  const title = $("title").text().trim();
20370
20643
  const description = $('meta[name="description"]').attr("content")?.trim() || "";
@@ -23000,7 +23273,7 @@ function getDateRange(days = 28) {
23000
23273
 
23001
23274
  // src/keywords/sources/competitor-analysis.ts
23002
23275
  init_http();
23003
- var cheerio41 = __toESM(require("cheerio"));
23276
+ var cheerio42 = __toESM(require("cheerio"));
23004
23277
  var USER_AGENT2 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
23005
23278
  async function discoverCompetitorKeywords(yourDomain, seedKeywords, competitors) {
23006
23279
  const yourKeywords = /* @__PURE__ */ new Set();
@@ -23097,7 +23370,7 @@ async function analyzeSERP(query) {
23097
23370
  headers: { "User-Agent": USER_AGENT2 },
23098
23371
  timeout: 1e4
23099
23372
  });
23100
- const $ = cheerio41.load(response.data);
23373
+ const $ = cheerio42.load(response.data);
23101
23374
  const results = [];
23102
23375
  const relatedSearches = [];
23103
23376
  const peopleAlsoAsk = [];
@@ -23550,7 +23823,7 @@ function formatTopicReport(result) {
23550
23823
 
23551
23824
  // src/content/readability.ts
23552
23825
  function analyzeReadability(text) {
23553
- const cleanText = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
23826
+ const cleanText = text.replace(/<[^>]*>/g, " ").replace(/[ \t\r\f\v]+/g, " ").replace(/ ?\n ?/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
23554
23827
  const words = getWords(cleanText);
23555
23828
  const sentences = getSentences(cleanText);
23556
23829
  const paragraphs = getParagraphs(cleanText);
@@ -23619,7 +23892,7 @@ function getSentences(text) {
23619
23892
  return text.split(/[.!?]+/).filter((s) => s.trim().length > 0);
23620
23893
  }
23621
23894
  function getParagraphs(text) {
23622
- return text.split(/\n\n+/).filter((p) => p.trim().length > 0);
23895
+ return text.split(/\n\n+/).filter((p) => p.trim().split(/\s+/).length >= 3);
23623
23896
  }
23624
23897
  function countSyllables(word) {
23625
23898
  word = word.toLowerCase().replace(/[^a-z]/g, "");
@@ -35904,6 +36177,7 @@ var SUPPORTED_FRAMEWORKS = [
35904
36177
  LOCATION_CODES,
35905
36178
  OG_IMAGE_SPECS,
35906
36179
  PRIORITY_WEIGHTS,
36180
+ REDUNDANT_WHEN_PRESENT,
35907
36181
  RankTracker,
35908
36182
  SEO_SCOPES,
35909
36183
  SITE_PROFILE_QUESTIONS,
@@ -35987,6 +36261,7 @@ var SUPPORTED_FRAMEWORKS = [
35987
36261
  createPullRequest,
35988
36262
  createSEOCommit,
35989
36263
  createSEOCommits,
36264
+ dedupeIssues,
35990
36265
  describeConcerns,
35991
36266
  detectDuplicates,
35992
36267
  detectFramework,
@@ -36123,6 +36398,7 @@ var SUPPORTED_FRAMEWORKS = [
36123
36398
  loadAgent,
36124
36399
  loadAgentByName,
36125
36400
  mergePages,
36401
+ metaRefreshTarget,
36126
36402
  openAIKeywordLLM,
36127
36403
  optimizeForAI,
36128
36404
  optimizeReadme,