@rankcli/agent-runtime 0.0.38 → 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
@@ -263,7 +263,7 @@ async function crawlUrl(params) {
263
263
  async function extractMeta(params) {
264
264
  const { html, url } = params;
265
265
  try {
266
- const $ = cheerio42.load(html);
266
+ const $ = cheerio43.load(html);
267
267
  const meta = {
268
268
  title: $("title").text().trim() || void 0,
269
269
  description: $('meta[name="description"]').attr("content")?.trim(),
@@ -308,7 +308,7 @@ async function extractMeta(params) {
308
308
  async function analyzeHeadings(params) {
309
309
  const { html } = params;
310
310
  try {
311
- const $ = cheerio42.load(html);
311
+ const $ = cheerio43.load(html);
312
312
  const headings = [];
313
313
  $("h1, h2, h3, h4, h5, h6").each((_, el) => {
314
314
  const tag = el.tagName.toLowerCase();
@@ -329,7 +329,7 @@ async function analyzeHeadings(params) {
329
329
  async function extractImages2(params) {
330
330
  const { html } = params;
331
331
  try {
332
- const $ = cheerio42.load(html);
332
+ const $ = cheerio43.load(html);
333
333
  const images = [];
334
334
  $("img").each((_, el) => {
335
335
  images.push({
@@ -351,7 +351,7 @@ async function extractImages2(params) {
351
351
  async function extractLinks2(params) {
352
352
  const { html, baseUrl } = params;
353
353
  try {
354
- const $ = cheerio42.load(html);
354
+ const $ = cheerio43.load(html);
355
355
  const links = [];
356
356
  const baseHostname = new URL(baseUrl).hostname;
357
357
  $("a[href]").each((_, el) => {
@@ -386,7 +386,7 @@ async function extractLinks2(params) {
386
386
  async function extractSchema2(params) {
387
387
  const { html } = params;
388
388
  try {
389
- const $ = cheerio42.load(html);
389
+ const $ = cheerio43.load(html);
390
390
  const schemas = [];
391
391
  $('script[type="application/ld+json"]').each((_, el) => {
392
392
  try {
@@ -455,7 +455,7 @@ async function checkSitemap2(params) {
455
455
  timeout: 1e4
456
456
  });
457
457
  if (response.status === 200 && response.data.includes("<?xml")) {
458
- const $ = cheerio42.load(response.data, { xmlMode: true });
458
+ const $ = cheerio43.load(response.data, { xmlMode: true });
459
459
  const urlCount = $("url").length || $("sitemap").length;
460
460
  return {
461
461
  success: true,
@@ -484,12 +484,12 @@ async function checkSitemap2(params) {
484
484
  };
485
485
  }
486
486
  }
487
- var cheerio42;
487
+ var cheerio43;
488
488
  var init_crawl = __esm({
489
489
  "src/tools/crawl.ts"() {
490
490
  "use strict";
491
491
  init_http();
492
- cheerio42 = __toESM(require("cheerio"));
492
+ cheerio43 = __toESM(require("cheerio"));
493
493
  }
494
494
  });
495
495
 
@@ -886,7 +886,7 @@ function analyzeRobotsTxtForAI(robotsTxt) {
886
886
  return { allowed, blocked, recommendations };
887
887
  }
888
888
  function detectRenderingMode(html) {
889
- const $ = cheerio43.load(html);
889
+ const $ = cheerio44.load(html);
890
890
  const signals = [];
891
891
  const hasNextData = html.includes("__NEXT_DATA__");
892
892
  const hasNuxtData = html.includes("__NUXT__");
@@ -927,7 +927,7 @@ function detectRenderingMode(html) {
927
927
  };
928
928
  }
929
929
  function analyzeContentStructure(html) {
930
- const $ = cheerio43.load(html);
930
+ const $ = cheerio44.load(html);
931
931
  const jsonLdScripts = $('script[type="application/ld+json"]');
932
932
  let hasStructuredData = jsonLdScripts.length > 0;
933
933
  let hasFAQSchema = false;
@@ -977,7 +977,7 @@ function analyzeContentStructure(html) {
977
977
  };
978
978
  }
979
979
  function analyzeCitationReadiness(html) {
980
- const $ = cheerio43.load(html);
980
+ const $ = cheerio44.load(html);
981
981
  const bodyText = $("body").text();
982
982
  const hasCitations = $('cite, blockquote, [class*="citation"], [class*="reference"]').length > 0 || /\[\d+\]|\(\d{4}\)/.test(bodyText);
983
983
  const externalLinks = $('a[href^="http"]').filter((_, el) => {
@@ -1010,7 +1010,7 @@ function analyzeCitationReadiness(html) {
1010
1010
  };
1011
1011
  }
1012
1012
  function analyzeEntityExtraction(html) {
1013
- const $ = cheerio43.load(html);
1013
+ const $ = cheerio44.load(html);
1014
1014
  const bodyText = $("body").text();
1015
1015
  const definedTerms = [];
1016
1016
  $("dfn, abbr[title]").each((_, el) => {
@@ -1046,7 +1046,7 @@ function analyzeEntityExtraction(html) {
1046
1046
  };
1047
1047
  }
1048
1048
  function calculateLLMSignals(structure, citation, entity, html) {
1049
- const $ = cheerio43.load(html);
1049
+ const $ = cheerio44.load(html);
1050
1050
  let contentClarity = 0;
1051
1051
  if (structure.headingHierarchy === "good") contentClarity += 30;
1052
1052
  else if (structure.headingHierarchy === "needs-work") contentClarity += 15;
@@ -1352,11 +1352,11 @@ Allow: /
1352
1352
  Sitemap: ${siteUrl}/sitemap.xml
1353
1353
  `;
1354
1354
  }
1355
- var cheerio43, AI_CRAWLERS, AI_CRAWLERS_INFO;
1355
+ var cheerio44, AI_CRAWLERS, AI_CRAWLERS_INFO;
1356
1356
  var init_geo_analyzer = __esm({
1357
1357
  "src/analyzers/geo-analyzer.ts"() {
1358
1358
  "use strict";
1359
- cheerio43 = __toESM(require("cheerio"));
1359
+ cheerio44 = __toESM(require("cheerio"));
1360
1360
  AI_CRAWLERS = {
1361
1361
  // OpenAI
1362
1362
  GPTBot: { userAgent: "GPTBot", company: "OpenAI", purpose: "ChatGPT training & browsing" },
@@ -1715,7 +1715,7 @@ function generateCWVIssues(lcp, fidInp, cls, ttfb, url) {
1715
1715
  return issues;
1716
1716
  }
1717
1717
  function analyzeCoreWebVitals(html, url, headers) {
1718
- const $ = cheerio44.load(html);
1718
+ const $ = cheerio45.load(html);
1719
1719
  const lcp = analyzeLCP(html, $);
1720
1720
  const fidInp = analyzeFID_INP(html, $);
1721
1721
  const cls = analyzeCLS(html, $);
@@ -1760,11 +1760,11 @@ function analyzeCoreWebVitals(html, url, headers) {
1760
1760
  issues
1761
1761
  };
1762
1762
  }
1763
- var cheerio44;
1763
+ var cheerio45;
1764
1764
  var init_core_web_vitals_analyzer = __esm({
1765
1765
  "src/analyzers/core-web-vitals-analyzer.ts"() {
1766
1766
  "use strict";
1767
- cheerio44 = __toESM(require("cheerio"));
1767
+ cheerio45 = __toESM(require("cheerio"));
1768
1768
  }
1769
1769
  });
1770
1770
 
@@ -2099,7 +2099,7 @@ function validateSchema(data, type) {
2099
2099
  };
2100
2100
  }
2101
2101
  function analyzeStructuredData2(html, url) {
2102
- const $ = cheerio45.load(html);
2102
+ const $ = cheerio46.load(html);
2103
2103
  const schemas = [];
2104
2104
  const issues = [];
2105
2105
  const recommendations = [];
@@ -2363,11 +2363,11 @@ function generateSchemaTemplate(pageType, options) {
2363
2363
  }
2364
2364
  return JSON.stringify(result, null, 2);
2365
2365
  }
2366
- var cheerio45, SCHEMA_REQUIREMENTS;
2366
+ var cheerio46, SCHEMA_REQUIREMENTS;
2367
2367
  var init_structured_data_analyzer = __esm({
2368
2368
  "src/analyzers/structured-data-analyzer.ts"() {
2369
2369
  "use strict";
2370
- cheerio45 = __toESM(require("cheerio"));
2370
+ cheerio46 = __toESM(require("cheerio"));
2371
2371
  SCHEMA_REQUIREMENTS = {
2372
2372
  "Organization": {
2373
2373
  required: ["name", "url"],
@@ -2483,7 +2483,7 @@ function getImageFormat2(src) {
2483
2483
  }
2484
2484
  }
2485
2485
  function analyzeImages3(html, url) {
2486
- const $ = cheerio46.load(html);
2486
+ const $ = cheerio47.load(html);
2487
2487
  const issues = [];
2488
2488
  const recommendations = [];
2489
2489
  let score = 100;
@@ -2701,11 +2701,11 @@ function generateResponsiveImage(options) {
2701
2701
  />
2702
2702
  </picture>`;
2703
2703
  }
2704
- var cheerio46, MODERN_FORMATS3, LEGACY_FORMATS2, MAX_RECOMMENDED_DIMENSION;
2704
+ var cheerio47, MODERN_FORMATS3, LEGACY_FORMATS2, MAX_RECOMMENDED_DIMENSION;
2705
2705
  var init_image_optimization_analyzer = __esm({
2706
2706
  "src/analyzers/image-optimization-analyzer.ts"() {
2707
2707
  "use strict";
2708
- cheerio46 = __toESM(require("cheerio"));
2708
+ cheerio47 = __toESM(require("cheerio"));
2709
2709
  MODERN_FORMATS3 = ["webp", "avif"];
2710
2710
  LEGACY_FORMATS2 = ["jpg", "jpeg", "png", "gif", "bmp"];
2711
2711
  MAX_RECOMMENDED_DIMENSION = 2e3;
@@ -2746,7 +2746,7 @@ function analyzeAnchorText2(text) {
2746
2746
  };
2747
2747
  }
2748
2748
  function analyzeInternalLinking(html, url) {
2749
- const $ = cheerio47.load(html);
2749
+ const $ = cheerio48.load(html);
2750
2750
  const issues = [];
2751
2751
  const recommendations = [];
2752
2752
  let score = 100;
@@ -2985,11 +2985,11 @@ function suggestInternalLinks(content, availablePages) {
2985
2985
  }
2986
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);
2987
2987
  }
2988
- var cheerio47, GENERIC_ANCHOR_TEXTS;
2988
+ var cheerio48, GENERIC_ANCHOR_TEXTS;
2989
2989
  var init_internal_linking_analyzer = __esm({
2990
2990
  "src/analyzers/internal-linking-analyzer.ts"() {
2991
2991
  "use strict";
2992
- cheerio47 = __toESM(require("cheerio"));
2992
+ cheerio48 = __toESM(require("cheerio"));
2993
2993
  GENERIC_ANCHOR_TEXTS = [
2994
2994
  "click here",
2995
2995
  "read more",
@@ -3183,7 +3183,7 @@ function analyzeMobileSpecific($) {
3183
3183
  };
3184
3184
  }
3185
3185
  function analyzeMobileSEO(html, url) {
3186
- const $ = cheerio48.load(html);
3186
+ const $ = cheerio49.load(html);
3187
3187
  const issues = [];
3188
3188
  const recommendations = [];
3189
3189
  let score = 100;
@@ -3314,11 +3314,11 @@ function analyzeMobileSEO(html, url) {
3314
3314
  recommendations
3315
3315
  };
3316
3316
  }
3317
- var cheerio48;
3317
+ var cheerio49;
3318
3318
  var init_mobile_seo_analyzer = __esm({
3319
3319
  "src/analyzers/mobile-seo-analyzer.ts"() {
3320
3320
  "use strict";
3321
- cheerio48 = __toESM(require("cheerio"));
3321
+ cheerio49 = __toESM(require("cheerio"));
3322
3322
  }
3323
3323
  });
3324
3324
 
@@ -3330,6 +3330,7 @@ __export(index_exports, {
3330
3330
  LOCATION_CODES: () => LOCATION_CODES,
3331
3331
  OG_IMAGE_SPECS: () => OG_IMAGE_SPECS,
3332
3332
  PRIORITY_WEIGHTS: () => PRIORITY_WEIGHTS,
3333
+ REDUNDANT_WHEN_PRESENT: () => REDUNDANT_WHEN_PRESENT,
3333
3334
  RankTracker: () => RankTracker,
3334
3335
  SEO_SCOPES: () => SEO_SCOPES,
3335
3336
  SITE_PROFILE_QUESTIONS: () => SITE_PROFILE_QUESTIONS,
@@ -3413,6 +3414,7 @@ __export(index_exports, {
3413
3414
  createPullRequest: () => createPullRequest,
3414
3415
  createSEOCommit: () => createSEOCommit,
3415
3416
  createSEOCommits: () => createSEOCommits,
3417
+ dedupeIssues: () => dedupeIssues,
3416
3418
  describeConcerns: () => describeConcerns,
3417
3419
  detectDuplicates: () => detectDuplicates,
3418
3420
  detectFramework: () => detectFramework4,
@@ -3549,6 +3551,7 @@ __export(index_exports, {
3549
3551
  loadAgent: () => loadAgent,
3550
3552
  loadAgentByName: () => loadAgentByName,
3551
3553
  mergePages: () => mergePages,
3554
+ metaRefreshTarget: () => metaRefreshTarget,
3552
3555
  openAIKeywordLLM: () => openAIKeywordLLM,
3553
3556
  optimizeForAI: () => optimizeForAI,
3554
3557
  optimizeReadme: () => optimizeReadme,
@@ -6371,7 +6374,7 @@ var ISSUE_DEFINITIONS = {
6371
6374
  severity: "warning",
6372
6375
  category: "performance",
6373
6376
  title: "HTML document too large",
6374
- description: "HTML size exceeds 100KB.",
6377
+ description: "HTML size exceeds 300KB.",
6375
6378
  impact: "Increases load time and memory usage.",
6376
6379
  howToFix: "Reduce inline CSS/JS, remove unnecessary HTML."
6377
6380
  },
@@ -9113,7 +9116,7 @@ async function analyzePerformance(url) {
9113
9116
  details: { ttfb: `${ttfbTime}ms` }
9114
9117
  });
9115
9118
  }
9116
- if (htmlSize > 100 * 1024) {
9119
+ if (htmlSize > 300 * 1024) {
9117
9120
  issues.push({
9118
9121
  ...ISSUE_DEFINITIONS.HTML_TOO_LARGE,
9119
9122
  affectedUrls: [url],
@@ -11277,7 +11280,7 @@ function analyzeLocalSEO(html, url) {
11277
11280
  const detectedPhones = detectPhones(html);
11278
11281
  const detectedAddresses = detectAddresses(html);
11279
11282
  const socialProfiles = detectSocialProfiles(html);
11280
- if (!hasLocalBusiness) {
11283
+ if (!hasLocalBusiness && (detectedPhones.length > 0 || detectedAddresses.length > 0)) {
11281
11284
  issues.push({
11282
11285
  ...ISSUE_DEFINITIONS.LOCAL_BUSINESS_SCHEMA_MISSING,
11283
11286
  affectedUrls: [url]
@@ -12340,7 +12343,17 @@ function analyzeKeywords(html, url) {
12340
12343
  const sortPhrases = (phrases, n) => {
12341
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 }));
12342
12345
  };
12343
- 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) : [];
12344
12357
  if (potentialStuffing.length > 0) {
12345
12358
  issues.push({
12346
12359
  ...ISSUE_DEFINITIONS.KEYWORD_STUFFING,
@@ -12567,13 +12580,6 @@ function checkAMP(html, url) {
12567
12580
  }
12568
12581
  };
12569
12582
  }
12570
- const isArticle = $("article").length > 0 || $('meta[property="og:type"][content="article"]').length > 0 || html.toLowerCase().includes("blog") || html.toLowerCase().includes("news");
12571
- if (isArticle) {
12572
- issues.push({
12573
- ...ISSUE_DEFINITIONS.AMP_MISSING,
12574
- affectedUrls: [url]
12575
- });
12576
- }
12577
12583
  return {
12578
12584
  issues,
12579
12585
  data: {
@@ -13285,13 +13291,13 @@ async function analyzeSiteMaturity(html, url) {
13285
13291
  contentToAuthorityRatio
13286
13292
  };
13287
13293
  const { strategy, recommendations } = determineStrategy(maturityLevel, authorityIndicators, contentMetrics);
13288
- if (maturityLevel === "new") {
13294
+ if (maturityLevel === "new" && ageSource === "wayback" && estimatedAgeDays !== null) {
13289
13295
  issues.push({
13290
13296
  code: "NEW_SITE_STRATEGY",
13291
13297
  severity: "notice",
13292
13298
  category: "content",
13293
13299
  title: "New website detected - Authority-first strategy recommended",
13294
- 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.`,
13295
13301
  impact: "Creating lots of content without authority is wasted effort. Content from new sites rarely ranks.",
13296
13302
  howToFix: "Focus on building 10+ quality backlinks to ONE great page before expanding content. See strategic recommendations.",
13297
13303
  affectedUrls: [url],
@@ -13302,24 +13308,7 @@ async function analyzeSiteMaturity(html, url) {
13302
13308
  }
13303
13309
  });
13304
13310
  }
13305
- if (contentToAuthorityRatio === "content-heavy") {
13306
- issues.push({
13307
- code: "CONTENT_AUTHORITY_IMBALANCE",
13308
- severity: "warning",
13309
- category: "content",
13310
- title: "Content-to-authority imbalance",
13311
- description: `Site has ~${indexedPages} pages but limited backlink signals. Content is unlikely to rank.`,
13312
- impact: "Most of your content may be invisible to search engines due to low domain authority.",
13313
- howToFix: "Stop publishing new content. Focus 100% on link building until you see ranking improvements.",
13314
- affectedUrls: [url],
13315
- details: {
13316
- indexedPages,
13317
- contentToAuthorityRatio,
13318
- maturityLevel
13319
- }
13320
- });
13321
- }
13322
- if (maturityLevel === "growing" && !htmlSignals.hasBlog) {
13311
+ if (maturityLevel === "growing" && ageSource === "wayback" && !htmlSignals.hasBlog) {
13323
13312
  issues.push({
13324
13313
  code: "GROWING_SITE_NO_BLOG",
13325
13314
  severity: "notice",
@@ -13813,8 +13802,8 @@ function analyzeRendering(html) {
13813
13802
  const hasSemanticStructure = hasH1 && (hasMain || hasArticle);
13814
13803
  const title = $("title").text().trim();
13815
13804
  const h1Text = $("h1").first().text().trim();
13816
- const firstParagraph = $("p").first().text().trim();
13817
- 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;
13818
13807
  const EXPECTED_MIN_CONTENT = 1e3;
13819
13808
  const htmlContentRatio = Math.min(100, mainContentCharCount / EXPECTED_MIN_CONTENT * 100);
13820
13809
  let aiReadableScore = 50;
@@ -14034,9 +14023,9 @@ function analyzeClientRendering(html, url) {
14034
14023
  howToFix: "Ensure the initial HTML contains: meaningful <title>, descriptive <h1>, and a substantial first paragraph with your key message.",
14035
14024
  affectedUrls: [url],
14036
14025
  details: {
14037
- hasTitle: $("title").text().trim().length > 10,
14038
- hasH1: $("h1").text().trim().length > 5,
14039
- 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)
14040
14029
  }
14041
14030
  });
14042
14031
  }
@@ -14748,7 +14737,9 @@ async function analyzeBingOptimization(html, url) {
14748
14737
  detectedIssues.push("No Bing site verification");
14749
14738
  issues.push({
14750
14739
  code: "BING_NOT_VERIFIED",
14751
- 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",
14752
14743
  category: "ai-readiness",
14753
14744
  title: verification.dnsChecked ? "Site not verified with Bing Webmaster Tools" : "Bing Webmaster Tools verification not visible in the HTML",
14754
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.",
@@ -14799,7 +14790,7 @@ async function analyzeBingOptimization(html, url) {
14799
14790
  });
14800
14791
  }
14801
14792
  const hasLocalBusinessSchema = bingPlacesDetected;
14802
- 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;
14803
14794
  if (hasAddressOnPage && !hasLocalBusinessSchema) {
14804
14795
  detectedIssues.push("Local business may not be on Bing Places");
14805
14796
  issues.push({
@@ -16330,23 +16321,6 @@ function analyzeColorContrast(html, url) {
16330
16321
  });
16331
16322
  }
16332
16323
  }
16333
- if (cannotDetermine > elementsAnalyzed * 0.8) {
16334
- issues.push({
16335
- code: "A11Y_CONTRAST_UNDETERMINED",
16336
- severity: "notice",
16337
- category: "accessibility",
16338
- title: "Color contrast could not be fully analyzed",
16339
- description: "Most text colors are set via CSS stylesheets rather than inline styles. Full contrast checking requires browser rendering.",
16340
- impact: "Potential contrast issues may exist that were not detected.",
16341
- howToFix: "Use browser DevTools (Lighthouse accessibility audit) or dedicated tools like axe-core for comprehensive contrast checking.",
16342
- affectedUrls: [url],
16343
- details: {
16344
- elementsAnalyzed,
16345
- cannotDetermine,
16346
- recommendation: "Run Lighthouse accessibility audit for complete contrast analysis."
16347
- }
16348
- });
16349
- }
16350
16324
  return {
16351
16325
  issues,
16352
16326
  data: {
@@ -17671,7 +17645,8 @@ async function analyzeTrackingVerification(html, url) {
17671
17645
  if (!hasGa4 && !hasGtm && otherAnalytics.length === 0) {
17672
17646
  issues.push({
17673
17647
  code: "GA4_MISSING",
17674
- severity: "warning",
17648
+ // A notice: which analytics a site runs is a choice, not an SEO defect.
17649
+ severity: "notice",
17675
17650
  category: "on-page",
17676
17651
  title: "Google Analytics 4 not detected",
17677
17652
  description: "No GA4 tracking code found. GA4 is essential for measuring organic traffic and conversions.",
@@ -17684,7 +17659,8 @@ async function analyzeTrackingVerification(html, url) {
17684
17659
  const dnsChecked = verification.dnsChecked;
17685
17660
  issues.push({
17686
17661
  code: "GSC_VERIFICATION_MISSING",
17687
- severity: "warning",
17662
+ // A notice: a site verified by DNS, Google Analytics or Tag Manager leaves no trace here.
17663
+ severity: "notice",
17688
17664
  category: "on-page",
17689
17665
  title: dnsChecked ? "Google Search Console verification not found" : "Google Search Console verification not visible in the HTML",
17690
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.",
@@ -17759,6 +17735,113 @@ async function analyzeTrackingVerification(html, url) {
17759
17735
  return { issues, data };
17760
17736
  }
17761
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
+
17762
17845
  // src/audit/engine.ts
17763
17846
  function sameUrl(a, b) {
17764
17847
  try {
@@ -17771,15 +17854,15 @@ function sameUrl(a, b) {
17771
17854
  }
17772
17855
  }
17773
17856
  async function runFullAudit(options) {
17774
- 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;
17775
17859
  const auditRoot = isSiteRoot(url) || sameUrl(url, options.auditRootUrl ?? url);
17776
17860
  const allIssues = [];
17777
17861
  const pages = [];
17778
17862
  const tier = checksLimit <= 50 ? "free" : checksLimit <= 100 ? "registered" : "paid";
17779
17863
  const runPremiumChecks = tier === "paid" && !!premiumChecks;
17780
17864
  const runExtendedChecks = tier !== "free";
17781
- const parsedUrl = new URL(url);
17782
- const domain = parsedUrl.hostname;
17865
+ let domain = new URL(url).hostname;
17783
17866
  console.error(`
17784
17867
  \u{1F50D} Running comprehensive SEO audit on ${url}...
17785
17868
  `);
@@ -17806,7 +17889,9 @@ async function runFullAudit(options) {
17806
17889
  timeout: 3e4,
17807
17890
  validateStatus: () => true
17808
17891
  }).catch((err) => {
17809
- 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}`);
17810
17895
  return { error: err, data: "", headers: {} };
17811
17896
  })
17812
17897
  ]);
@@ -17829,7 +17914,7 @@ async function runFullAudit(options) {
17829
17914
  });
17830
17915
  return createReport(url, domain, allIssues, pages, { kind: "unreachable" });
17831
17916
  }
17832
- const statusCode = fetchResult.status;
17917
+ let statusCode = fetchResult.status;
17833
17918
  if (statusCode >= 400) {
17834
17919
  const blocked = statusCode === 401 || statusCode === 403 || statusCode === 429;
17835
17920
  allIssues.push({
@@ -17842,10 +17927,42 @@ async function runFullAudit(options) {
17842
17927
  pages.push({ url, statusCode, wordCount: 0, loadTime: 0, issues: allIssues.map((i) => i.code) });
17843
17928
  return createReport(url, domain, allIssues, pages, { kind: "http-error", statusCode });
17844
17929
  }
17845
- 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
+ }
17846
17964
  const html = body.replace(/<\/?noscript[^>]*>/gi, "");
17847
- const headers = fetchResult.headers;
17848
- console.error(`\u{1F4DD} Phase 2: Running synchronous HTML checks (tier: ${tier}, limit: ${checksLimit})...`);
17965
+ console.error("\u{1F4DD} Phase 2: Running HTML checks...");
17849
17966
  const onPageResult = analyzeOnPage(html, url, headers, { auditRoot });
17850
17967
  const structuredDataResult = analyzeStructuredData(html, url);
17851
17968
  const mobileResult = analyzeMobile(html, url);
@@ -18012,10 +18129,10 @@ async function runFullAudit(options) {
18012
18129
  if (cruxApiKey) {
18013
18130
  asyncChecks.push(safeAsyncIssues("Core Web Vitals", async () => {
18014
18131
  const cwv = await fetchCoreWebVitals(url, cruxApiKey);
18015
- const issues = [];
18132
+ const issues2 = [];
18016
18133
  if (cwv) {
18017
18134
  if (cwv.lcp?.rating === "poor") {
18018
- issues.push({
18135
+ issues2.push({
18019
18136
  code: "LCP_POOR",
18020
18137
  severity: "error",
18021
18138
  category: "performance",
@@ -18028,7 +18145,7 @@ async function runFullAudit(options) {
18028
18145
  });
18029
18146
  }
18030
18147
  if (cwv.cls?.rating === "poor") {
18031
- issues.push({
18148
+ issues2.push({
18032
18149
  code: "CLS_POOR",
18033
18150
  severity: "error",
18034
18151
  category: "performance",
@@ -18041,14 +18158,15 @@ async function runFullAudit(options) {
18041
18158
  });
18042
18159
  }
18043
18160
  }
18044
- return issues;
18161
+ return issues2;
18045
18162
  }));
18046
18163
  }
18047
18164
  }
18048
18165
  const asyncResults = await Promise.all(asyncChecks);
18049
- for (const issues of asyncResults) {
18050
- allIssues.push(...issues);
18166
+ for (const issues2 of asyncResults) {
18167
+ allIssues.push(...issues2);
18051
18168
  }
18169
+ const issues = dedupeIssues(allIssues);
18052
18170
  pages.push({
18053
18171
  url,
18054
18172
  statusCode,
@@ -18058,10 +18176,10 @@ async function runFullAudit(options) {
18058
18176
  h1: onPageResult.data.h1s,
18059
18177
  wordCount: onPageResult.data.wordCount,
18060
18178
  loadTime: perfData.loadTime,
18061
- issues: allIssues.map((i) => i.code)
18179
+ issues: issues.map((i) => i.code)
18062
18180
  });
18063
18181
  console.error("\n\u2705 Audit complete!\n");
18064
- return createReport(url, domain, allIssues, pages);
18182
+ return createReport(url, domain, issues, pages);
18065
18183
  }
18066
18184
  function createReport(url, domain, issues, pages, pageFailure) {
18067
18185
  const healthScore = calculateHealthScore(issues);
@@ -18226,10 +18344,10 @@ function groupIssuesByCategory(issues) {
18226
18344
  }
18227
18345
 
18228
18346
  // src/audit/checks/duplicate-content.ts
18229
- var cheerio37 = __toESM(require("cheerio"));
18347
+ var cheerio38 = __toESM(require("cheerio"));
18230
18348
  var import_crypto = require("crypto");
18231
18349
  function extractContentHash(html, url) {
18232
- const $ = cheerio37.load(html);
18350
+ const $ = cheerio38.load(html);
18233
18351
  $("script, style, nav, header, footer, aside, .nav, .header, .footer, .sidebar").remove();
18234
18352
  const title = $("title").text().trim();
18235
18353
  const bodyText = $("body").text().replace(/\s+/g, " ").trim();
@@ -19457,7 +19575,7 @@ var PRIORITY_WEIGHTS = {
19457
19575
  };
19458
19576
 
19459
19577
  // src/keywords/engine.ts
19460
- var cheerio39 = __toESM(require("cheerio"));
19578
+ var cheerio40 = __toESM(require("cheerio"));
19461
19579
  init_http();
19462
19580
 
19463
19581
  // src/keywords/prioritizer.ts
@@ -19754,7 +19872,7 @@ function enrichKeywordsWithEstimates(keywords) {
19754
19872
 
19755
19873
  // src/keywords/sources/free-sources.ts
19756
19874
  init_http();
19757
- var cheerio38 = __toESM(require("cheerio"));
19875
+ var cheerio39 = __toESM(require("cheerio"));
19758
19876
  var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
19759
19877
  async function getPeopleAlsoAsk(query) {
19760
19878
  try {
@@ -19763,7 +19881,7 @@ async function getPeopleAlsoAsk(query) {
19763
19881
  headers: { "User-Agent": USER_AGENT },
19764
19882
  timeout: 1e4
19765
19883
  });
19766
- const $ = cheerio38.load(response.data);
19884
+ const $ = cheerio39.load(response.data);
19767
19885
  const questions = [];
19768
19886
  $("[data-sgrd]").each((_, el) => {
19769
19887
  const text = $(el).text().trim();
@@ -19789,7 +19907,7 @@ async function getRelatedSearches(query) {
19789
19907
  headers: { "User-Agent": USER_AGENT },
19790
19908
  timeout: 1e4
19791
19909
  });
19792
- const $ = cheerio38.load(response.data);
19910
+ const $ = cheerio39.load(response.data);
19793
19911
  const related = [];
19794
19912
  $("div[data-ved] a").each((_, el) => {
19795
19913
  const href = $(el).attr("href");
@@ -19879,7 +19997,7 @@ async function analyzeCompetitorTitles(query) {
19879
19997
  headers: { "User-Agent": USER_AGENT },
19880
19998
  timeout: 1e4
19881
19999
  });
19882
- const $ = cheerio38.load(response.data);
20000
+ const $ = cheerio39.load(response.data);
19883
20001
  const titles = [];
19884
20002
  const keywords = /* @__PURE__ */ new Set();
19885
20003
  $("h3").each((_, el) => {
@@ -20212,7 +20330,7 @@ async function fetchPageMeta(url) {
20212
20330
  const response = await httpGet(url, {
20213
20331
  timeout: 1e4
20214
20332
  });
20215
- const $ = cheerio39.load(response.data);
20333
+ const $ = cheerio40.load(response.data);
20216
20334
  return {
20217
20335
  url,
20218
20336
  title: $("title").text().trim() || void 0,
@@ -20343,20 +20461,17 @@ async function extractSeedKeywords(url) {
20343
20461
  const response = await httpGet(url, {
20344
20462
  timeout: 1e4
20345
20463
  });
20346
- const $ = cheerio39.load(response.data);
20464
+ const $ = cheerio40.load(response.data);
20347
20465
  const seeds = /* @__PURE__ */ new Set();
20348
- const title = $("title").text().toLowerCase();
20349
- const titleWords = title.split(/[\s\-|:]+/).filter((w) => w.length > 3);
20350
- titleWords.forEach((w) => seeds.add(w));
20351
- const h1 = $("h1").first().text().toLowerCase();
20352
- const h1Words = h1.split(/[\s\-|:]+/).filter((w) => w.length > 3);
20353
- 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));
20354
20469
  const metaKeywords = $('meta[name="keywords"]').attr("content");
20355
20470
  if (metaKeywords) {
20356
20471
  metaKeywords.split(",").forEach((k) => seeds.add(k.trim().toLowerCase()));
20357
20472
  }
20358
20473
  const desc = $('meta[name="description"]').attr("content")?.toLowerCase() || "";
20359
- const descWords = desc.split(/\s+/).filter((w) => w.length > 5);
20474
+ const descWords = words(desc).filter((w) => w.length > 5);
20360
20475
  descWords.slice(0, 5).forEach((w) => seeds.add(w));
20361
20476
  return Array.from(seeds).slice(0, 10);
20362
20477
  } catch {
@@ -20409,7 +20524,7 @@ function parseJsonReply(text) {
20409
20524
  }
20410
20525
 
20411
20526
  // src/keywords/site-crawler.ts
20412
- var cheerio40 = __toESM(require("cheerio"));
20527
+ var cheerio41 = __toESM(require("cheerio"));
20413
20528
  init_http();
20414
20529
  var EXCLUDED_PATHS = [
20415
20530
  "/cdn-cgi/",
@@ -20522,7 +20637,7 @@ async function crawlPage(url, timeout) {
20522
20637
  validateStatus: (status) => status === 200
20523
20638
  });
20524
20639
  const html = response.data;
20525
- const $ = cheerio40.load(html);
20640
+ const $ = cheerio41.load(html);
20526
20641
  $('script, style, noscript, iframe, nav, footer, header, aside, [role="navigation"]').remove();
20527
20642
  const title = $("title").text().trim();
20528
20643
  const description = $('meta[name="description"]').attr("content")?.trim() || "";
@@ -23158,7 +23273,7 @@ function getDateRange(days = 28) {
23158
23273
 
23159
23274
  // src/keywords/sources/competitor-analysis.ts
23160
23275
  init_http();
23161
- var cheerio41 = __toESM(require("cheerio"));
23276
+ var cheerio42 = __toESM(require("cheerio"));
23162
23277
  var USER_AGENT2 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
23163
23278
  async function discoverCompetitorKeywords(yourDomain, seedKeywords, competitors) {
23164
23279
  const yourKeywords = /* @__PURE__ */ new Set();
@@ -23255,7 +23370,7 @@ async function analyzeSERP(query) {
23255
23370
  headers: { "User-Agent": USER_AGENT2 },
23256
23371
  timeout: 1e4
23257
23372
  });
23258
- const $ = cheerio41.load(response.data);
23373
+ const $ = cheerio42.load(response.data);
23259
23374
  const results = [];
23260
23375
  const relatedSearches = [];
23261
23376
  const peopleAlsoAsk = [];
@@ -23708,7 +23823,7 @@ function formatTopicReport(result) {
23708
23823
 
23709
23824
  // src/content/readability.ts
23710
23825
  function analyzeReadability(text) {
23711
- 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();
23712
23827
  const words = getWords(cleanText);
23713
23828
  const sentences = getSentences(cleanText);
23714
23829
  const paragraphs = getParagraphs(cleanText);
@@ -23777,7 +23892,7 @@ function getSentences(text) {
23777
23892
  return text.split(/[.!?]+/).filter((s) => s.trim().length > 0);
23778
23893
  }
23779
23894
  function getParagraphs(text) {
23780
- 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);
23781
23896
  }
23782
23897
  function countSyllables(word) {
23783
23898
  word = word.toLowerCase().replace(/[^a-z]/g, "");
@@ -36062,6 +36177,7 @@ var SUPPORTED_FRAMEWORKS = [
36062
36177
  LOCATION_CODES,
36063
36178
  OG_IMAGE_SPECS,
36064
36179
  PRIORITY_WEIGHTS,
36180
+ REDUNDANT_WHEN_PRESENT,
36065
36181
  RankTracker,
36066
36182
  SEO_SCOPES,
36067
36183
  SITE_PROFILE_QUESTIONS,
@@ -36145,6 +36261,7 @@ var SUPPORTED_FRAMEWORKS = [
36145
36261
  createPullRequest,
36146
36262
  createSEOCommit,
36147
36263
  createSEOCommits,
36264
+ dedupeIssues,
36148
36265
  describeConcerns,
36149
36266
  detectDuplicates,
36150
36267
  detectFramework,
@@ -36281,6 +36398,7 @@ var SUPPORTED_FRAMEWORKS = [
36281
36398
  loadAgent,
36282
36399
  loadAgentByName,
36283
36400
  mergePages,
36401
+ metaRefreshTarget,
36284
36402
  openAIKeywordLLM,
36285
36403
  optimizeForAI,
36286
36404
  optimizeReadme,