@rankcli/agent-runtime 0.0.32 → 0.0.34

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.d.mts CHANGED
@@ -3141,6 +3141,13 @@ interface FrameworkCheck {
3141
3141
  };
3142
3142
  app_router_only?: boolean;
3143
3143
  pages_router_only?: boolean;
3144
+ /**
3145
+ * Only meaningful on a page search engines are allowed to index. A check that
3146
+ * says "crawlers will not see your content" is not a finding on a page that
3147
+ * asks not to be crawled - every SaaS has a noindex /login, and reporting a
3148
+ * critical SEO error on it is noise that drags the score down.
3149
+ */
3150
+ indexable_only?: boolean;
3144
3151
  }
3145
3152
  interface FrameworkRecipe {
3146
3153
  framework: Framework;
@@ -3168,7 +3175,9 @@ declare function loadRecipe(framework: Framework): Promise<FrameworkRecipe | nul
3168
3175
  /**
3169
3176
  * Get framework-specific issues based on HTML analysis
3170
3177
  */
3171
- declare function getFrameworkIssues(frameworkInfo: FrameworkInfo, html: string, url: string): Promise<AuditIssue[]>;
3178
+ declare function getFrameworkIssues(frameworkInfo: FrameworkInfo, html: string, url: string, options?: {
3179
+ indexable?: boolean;
3180
+ }): Promise<AuditIssue[]>;
3172
3181
  /**
3173
3182
  * Get framework-specific suggestions for fixing issues
3174
3183
  */
package/dist/index.d.ts CHANGED
@@ -3141,6 +3141,13 @@ interface FrameworkCheck {
3141
3141
  };
3142
3142
  app_router_only?: boolean;
3143
3143
  pages_router_only?: boolean;
3144
+ /**
3145
+ * Only meaningful on a page search engines are allowed to index. A check that
3146
+ * says "crawlers will not see your content" is not a finding on a page that
3147
+ * asks not to be crawled - every SaaS has a noindex /login, and reporting a
3148
+ * critical SEO error on it is noise that drags the score down.
3149
+ */
3150
+ indexable_only?: boolean;
3144
3151
  }
3145
3152
  interface FrameworkRecipe {
3146
3153
  framework: Framework;
@@ -3168,7 +3175,9 @@ declare function loadRecipe(framework: Framework): Promise<FrameworkRecipe | nul
3168
3175
  /**
3169
3176
  * Get framework-specific issues based on HTML analysis
3170
3177
  */
3171
- declare function getFrameworkIssues(frameworkInfo: FrameworkInfo, html: string, url: string): Promise<AuditIssue[]>;
3178
+ declare function getFrameworkIssues(frameworkInfo: FrameworkInfo, html: string, url: string, options?: {
3179
+ indexable?: boolean;
3180
+ }): Promise<AuditIssue[]>;
3172
3181
  /**
3173
3182
  * Get framework-specific suggestions for fixing issues
3174
3183
  */
package/dist/index.js CHANGED
@@ -3660,12 +3660,13 @@ async function loadRecipe(framework) {
3660
3660
  }
3661
3661
  return null;
3662
3662
  }
3663
- async function getFrameworkIssues(frameworkInfo, html, url) {
3663
+ async function getFrameworkIssues(frameworkInfo, html, url, options = {}) {
3664
3664
  const recipe = await loadRecipe(frameworkInfo.framework);
3665
3665
  if (!recipe) return [];
3666
3666
  const issues = [];
3667
+ const indexable = options.indexable ?? true;
3667
3668
  for (const check of recipe.checks) {
3668
- const matched = evaluateCheck(check, html, frameworkInfo);
3669
+ const matched = evaluateCheck(check, html, frameworkInfo, indexable);
3669
3670
  if (matched) {
3670
3671
  issues.push({
3671
3672
  code: check.code,
@@ -3686,7 +3687,10 @@ async function getFrameworkIssues(frameworkInfo, html, url) {
3686
3687
  }
3687
3688
  return issues;
3688
3689
  }
3689
- function evaluateCheck(check, html, frameworkInfo) {
3690
+ function evaluateCheck(check, html, frameworkInfo, indexable = true) {
3691
+ if (check.indexable_only && !indexable) {
3692
+ return false;
3693
+ }
3690
3694
  if (check.app_router_only && html.includes("__next")) {
3691
3695
  return false;
3692
3696
  }
@@ -3800,6 +3804,7 @@ seo_challenges:
3800
3804
 
3801
3805
  checks:
3802
3806
  - code: ANGULAR_NO_UNIVERSAL
3807
+ indexable_only: true
3803
3808
  severity: error
3804
3809
  title: "Angular app without Universal (SSR)"
3805
3810
  description: "Client-side Angular apps send empty HTML to crawlers."
@@ -3818,6 +3823,7 @@ checks:
3818
3823
  missing_content: true
3819
3824
 
3820
3825
  - code: ANGULAR_NO_TITLE_SERVICE
3826
+ indexable_only: true
3821
3827
  severity: error
3822
3828
  title: "Not using Title/Meta services"
3823
3829
  description: "Angular has built-in services for managing title and meta tags."
@@ -3866,6 +3872,7 @@ checks:
3866
3872
  \`\`\`
3867
3873
 
3868
3874
  - code: ANGULAR_PRERENDER_MISSING
3875
+ indexable_only: true
3869
3876
  severity: warning
3870
3877
  title: "Static routes not prerendered"
3871
3878
  description: "Static pages should be prerendered at build time."
@@ -4224,6 +4231,7 @@ checks:
4224
4231
  \`\`\`
4225
4232
 
4226
4233
  - code: DJANGO_NO_META_FRAMEWORK
4234
+ indexable_only: true
4227
4235
  severity: warning
4228
4236
  title: "No SEO meta framework detected"
4229
4237
  description: "Use django-meta for consistent meta tag management."
@@ -4371,6 +4379,7 @@ seo_strengths:
4371
4379
 
4372
4380
  checks:
4373
4381
  - code: LARAVEL_NO_META_PACKAGE
4382
+ indexable_only: true
4374
4383
  severity: warning
4375
4384
  title: "No SEO package detected"
4376
4385
  description: "Laravel benefits from an SEO package like artesaos/seotools."
@@ -4422,6 +4431,7 @@ checks:
4422
4431
  \`\`\`
4423
4432
 
4424
4433
  - code: LARAVEL_INERTIA_NO_SSR
4434
+ indexable_only: true
4425
4435
  severity: error
4426
4436
  title: "Inertia.js without SSR"
4427
4437
  description: "Inertia apps are client-rendered by default, losing SEO benefits."
@@ -4516,6 +4526,7 @@ seo_strengths:
4516
4526
  checks:
4517
4527
  # App Router specific
4518
4528
  - code: NEXTJS_NO_METADATA_API
4529
+ indexable_only: true
4519
4530
  severity: warning
4520
4531
  title: "Not using Next.js Metadata API"
4521
4532
  description: "Next.js 13+ has a built-in Metadata API that's better than react-helmet."
@@ -4788,6 +4799,7 @@ seo_strengths:
4788
4799
 
4789
4800
  checks:
4790
4801
  - code: NUXT_NO_USESEOMETA
4802
+ indexable_only: true
4791
4803
  severity: warning
4792
4804
  title: "Not using useSeoMeta composable"
4793
4805
  description: "Nuxt 3 has built-in SEO meta composables that are cleaner than useHead."
@@ -4897,6 +4909,7 @@ checks:
4897
4909
  \`\`\`
4898
4910
 
4899
4911
  - code: NUXT_SPA_MODE
4912
+ indexable_only: true
4900
4913
  severity: error
4901
4914
  title: "Nuxt running in SPA mode"
4902
4915
  description: "SPA mode disables SSR, losing SEO benefits."
@@ -4922,6 +4935,7 @@ checks:
4922
4935
  missing_content: true
4923
4936
 
4924
4937
  - code: NUXT_NO_PRERENDER
4938
+ indexable_only: true
4925
4939
  severity: notice
4926
4940
  title: "Static pages not pre-rendered"
4927
4941
  description: "Static pages could be pre-rendered for better performance."
@@ -4965,6 +4979,7 @@ seo_strengths:
4965
4979
 
4966
4980
  checks:
4967
4981
  - code: RAILS_NO_META_TAGS_GEM
4982
+ indexable_only: true
4968
4983
  severity: warning
4969
4984
  title: "Not using meta-tags gem"
4970
4985
  description: "The meta-tags gem provides a clean API for SEO tags."
@@ -5037,6 +5052,7 @@ checks:
5037
5052
  \`\`\`
5038
5053
 
5039
5054
  - code: RAILS_TURBO_BREAKS_CRAWLERS
5055
+ indexable_only: true
5040
5056
  severity: notice
5041
5057
  title: "Turbo may affect JavaScript-dependent crawlers"
5042
5058
  description: "Turbo Drive updates pages via JavaScript, which some crawlers may miss."
@@ -5159,6 +5175,7 @@ seo_challenges:
5159
5175
  checks:
5160
5176
  # Critical - these will severely hurt SEO
5161
5177
  - code: REACT_CSR_DETECTED
5178
+ indexable_only: true
5162
5179
  severity: error
5163
5180
  title: "Client-side rendered React app detected"
5164
5181
  description: "React apps without SSR send empty HTML to crawlers. Search engines may not index your content."
@@ -5174,6 +5191,7 @@ checks:
5174
5191
  missing_content: true # Body has minimal text content
5175
5192
 
5176
5193
  - code: REACT_NO_SSR_META
5194
+ indexable_only: true
5177
5195
  severity: error
5178
5196
  title: "Meta tags not server-rendered"
5179
5197
  description: "Title and meta description are set client-side and won't be seen by crawlers."
@@ -5240,6 +5258,7 @@ checks:
5240
5258
  \`\`\`
5241
5259
 
5242
5260
  - code: REACT_ROUTER_NO_SSR
5261
+ indexable_only: true
5243
5262
  severity: warning
5244
5263
  title: "React Router without server-side support"
5245
5264
  description: "Client-side routing breaks when crawlers access URLs directly."
@@ -5258,6 +5277,7 @@ checks:
5258
5277
  \`\`\`
5259
5278
 
5260
5279
  - code: REACT_NO_PRERENDER
5280
+ indexable_only: true
5261
5281
  severity: warning
5262
5282
  title: "No prerendering configured"
5263
5283
  description: "Static pages could be prerendered for better SEO and performance."
@@ -5414,6 +5434,7 @@ checks:
5414
5434
  - 'name="description"'
5415
5435
 
5416
5436
  - code: SVELTEKIT_NO_PRERENDER
5437
+ indexable_only: true
5417
5438
  severity: notice
5418
5439
  title: "Static pages not prerendered"
5419
5440
  description: "Static pages should be prerendered for best performance."
@@ -5519,6 +5540,7 @@ seo_challenges:
5519
5540
 
5520
5541
  checks:
5521
5542
  - code: VUE_CSR_DETECTED
5543
+ indexable_only: true
5522
5544
  severity: error
5523
5545
  title: "Client-side rendered Vue app detected"
5524
5546
  description: "Vue apps without SSR send empty HTML to crawlers."
@@ -5535,6 +5557,7 @@ checks:
5535
5557
  missing_content: true
5536
5558
 
5537
5559
  - code: VUE_NO_META_MANAGEMENT
5560
+ indexable_only: true
5538
5561
  severity: error
5539
5562
  title: "No meta tag management library"
5540
5563
  description: "Title and meta description are not dynamically managed."
@@ -8502,6 +8525,23 @@ function metaHasNoindex(content) {
8502
8525
  if (!content) return false;
8503
8526
  return content.toLowerCase().split(",").some((d) => ["noindex", "none"].includes(d.trim()));
8504
8527
  }
8528
+ function readRobotsDirectives(html, headers) {
8529
+ const $ = cheerio.load(html);
8530
+ const metaValues = [];
8531
+ $("meta[name]").each((_, el) => {
8532
+ const name = ($(el).attr("name") || "").toLowerCase();
8533
+ if (name === "robots" || name === "googlebot") {
8534
+ metaValues.push($(el).attr("content") || "");
8535
+ }
8536
+ });
8537
+ const metaNoindex = metaValues.some(metaHasNoindex);
8538
+ const xRobotsTag = headers ? Object.entries(headers).find(([k]) => k.toLowerCase() === "x-robots-tag")?.[1] : void 0;
8539
+ const headerNoindex = xRobotsTagHasNoindex(xRobotsTag);
8540
+ return { metaValues, metaNoindex, xRobotsTag, headerNoindex, hasNoindex: metaNoindex || headerNoindex };
8541
+ }
8542
+ function pageHasNoindex(html, headers) {
8543
+ return readRobotsDirectives(html, headers).hasNoindex;
8544
+ }
8505
8545
  function isSiteRoot(url) {
8506
8546
  try {
8507
8547
  const u = new URL(url);
@@ -8517,17 +8557,7 @@ function analyzeOnPage(html, url, headers, options = {}) {
8517
8557
  const description = $('meta[name="description"]').attr("content")?.trim();
8518
8558
  const canonical = $('link[rel="canonical"]').attr("href");
8519
8559
  const metaRobots = $('meta[name="robots"]').attr("content");
8520
- const robotsMetaValues = [];
8521
- $("meta[name]").each((_, el) => {
8522
- const name = ($(el).attr("name") || "").toLowerCase();
8523
- if (name === "robots" || name === "googlebot") {
8524
- robotsMetaValues.push($(el).attr("content") || "");
8525
- }
8526
- });
8527
- const metaNoindex = robotsMetaValues.some(metaHasNoindex);
8528
- const headerLookup = headers ? Object.entries(headers).find(([k]) => k.toLowerCase() === "x-robots-tag")?.[1] : void 0;
8529
- const headerNoindex = xRobotsTagHasNoindex(headerLookup);
8530
- const hasNoindex = metaNoindex || headerNoindex;
8560
+ const { metaValues: robotsMetaValues, metaNoindex, xRobotsTag: headerLookup, headerNoindex, hasNoindex } = readRobotsDirectives(html, headers);
8531
8561
  const h1s = [];
8532
8562
  $("h1").each((_, el) => {
8533
8563
  h1s.push($(el).text().trim());
@@ -15948,6 +15978,17 @@ var NAMED_COLORS = {
15948
15978
  transparent: [255, 255, 255]
15949
15979
  // Treat as white for calculation
15950
15980
  };
15981
+ function inheritedBackground($el) {
15982
+ for (const ancestor of $el.parents().toArray()) {
15983
+ const style = ancestor.attribs?.style;
15984
+ if (!style) continue;
15985
+ const match = style.match(/(?:^|;)\s*background(?:-color)?\s*:\s*([^;]+)/i);
15986
+ if (!match) continue;
15987
+ const value = match[1].trim();
15988
+ if (!/^(transparent|none)$/i.test(value)) return value;
15989
+ }
15990
+ return null;
15991
+ }
15951
15992
  function analyzeColorContrast(html, url) {
15952
15993
  const issues = [];
15953
15994
  const $ = cheerio32.load(html);
@@ -15968,12 +16009,16 @@ function analyzeColorContrast(html, url) {
15968
16009
  const colorMatch = style.match(/(?:^|;)\s*color\s*:\s*([^;]+)/i);
15969
16010
  const bgMatch = style.match(/(?:^|;)\s*background(?:-color)?\s*:\s*([^;]+)/i);
15970
16011
  const foreground = colorMatch ? colorMatch[1].trim() : null;
15971
- const background = bgMatch ? bgMatch[1].trim() : null;
16012
+ const background = bgMatch ? bgMatch[1].trim() : inheritedBackground($el);
15972
16013
  if (!foreground) {
15973
16014
  cannotDetermine++;
15974
16015
  return;
15975
16016
  }
15976
- const bgColor = background || "white";
16017
+ if (!background) {
16018
+ cannotDetermine++;
16019
+ return;
16020
+ }
16021
+ const bgColor = background;
15977
16022
  const fgRgb = parseColor(foreground);
15978
16023
  const bgRgb = parseColor(bgColor);
15979
16024
  if (!fgRgb || !bgRgb) {
@@ -17669,7 +17714,9 @@ async function runFullAudit(options) {
17669
17714
  asyncChecks.push(safeAsyncIssues("Framework Detection", async () => {
17670
17715
  const frameworkInfo = detectFramework({ html, headers });
17671
17716
  if (frameworkInfo.framework === "unknown") return [];
17672
- return getFrameworkIssues(frameworkInfo, html, url);
17717
+ return getFrameworkIssues(frameworkInfo, html, url, {
17718
+ indexable: !pageHasNoindex(html, headers)
17719
+ });
17673
17720
  }));
17674
17721
  if (runExtendedChecks) {
17675
17722
  asyncChecks.push(safeAsync("Hreflang", () => analyzeHreflang(html, url, { validateUrls: checkHreflangUrls })));
@@ -17808,7 +17855,7 @@ function calculateHealthScore(issues) {
17808
17855
  const links = Math.max(0, 100 - categoryDeductions.links * 3);
17809
17856
  const performance = Math.max(0, 100 - categoryDeductions.performance * 4);
17810
17857
  const security = Math.max(0, 100 - categoryDeductions.security * 5);
17811
- const aiReadiness = Math.max(0, 100 - categoryDeductions["ai-readiness"] * 3);
17858
+ const aiReadiness = Math.max(0, 100 - categoryDeductions["ai-readiness"] * 1);
17812
17859
  const social = Math.max(0, 100 - categoryDeductions.social * 2);
17813
17860
  const localSeo = Math.max(0, 100 - categoryDeductions["local-seo"] * 2);
17814
17861
  const accessibility = Math.max(0, 100 - categoryDeductions.accessibility * 4);
package/dist/index.mjs CHANGED
@@ -131,12 +131,13 @@ async function loadRecipe(framework) {
131
131
  }
132
132
  return null;
133
133
  }
134
- async function getFrameworkIssues(frameworkInfo, html, url) {
134
+ async function getFrameworkIssues(frameworkInfo, html, url, options = {}) {
135
135
  const recipe = await loadRecipe(frameworkInfo.framework);
136
136
  if (!recipe) return [];
137
137
  const issues = [];
138
+ const indexable = options.indexable ?? true;
138
139
  for (const check of recipe.checks) {
139
- const matched = evaluateCheck(check, html, frameworkInfo);
140
+ const matched = evaluateCheck(check, html, frameworkInfo, indexable);
140
141
  if (matched) {
141
142
  issues.push({
142
143
  code: check.code,
@@ -157,7 +158,10 @@ async function getFrameworkIssues(frameworkInfo, html, url) {
157
158
  }
158
159
  return issues;
159
160
  }
160
- function evaluateCheck(check, html, frameworkInfo) {
161
+ function evaluateCheck(check, html, frameworkInfo, indexable = true) {
162
+ if (check.indexable_only && !indexable) {
163
+ return false;
164
+ }
161
165
  if (check.app_router_only && html.includes("__next")) {
162
166
  return false;
163
167
  }
@@ -271,6 +275,7 @@ seo_challenges:
271
275
 
272
276
  checks:
273
277
  - code: ANGULAR_NO_UNIVERSAL
278
+ indexable_only: true
274
279
  severity: error
275
280
  title: "Angular app without Universal (SSR)"
276
281
  description: "Client-side Angular apps send empty HTML to crawlers."
@@ -289,6 +294,7 @@ checks:
289
294
  missing_content: true
290
295
 
291
296
  - code: ANGULAR_NO_TITLE_SERVICE
297
+ indexable_only: true
292
298
  severity: error
293
299
  title: "Not using Title/Meta services"
294
300
  description: "Angular has built-in services for managing title and meta tags."
@@ -337,6 +343,7 @@ checks:
337
343
  \`\`\`
338
344
 
339
345
  - code: ANGULAR_PRERENDER_MISSING
346
+ indexable_only: true
340
347
  severity: warning
341
348
  title: "Static routes not prerendered"
342
349
  description: "Static pages should be prerendered at build time."
@@ -695,6 +702,7 @@ checks:
695
702
  \`\`\`
696
703
 
697
704
  - code: DJANGO_NO_META_FRAMEWORK
705
+ indexable_only: true
698
706
  severity: warning
699
707
  title: "No SEO meta framework detected"
700
708
  description: "Use django-meta for consistent meta tag management."
@@ -842,6 +850,7 @@ seo_strengths:
842
850
 
843
851
  checks:
844
852
  - code: LARAVEL_NO_META_PACKAGE
853
+ indexable_only: true
845
854
  severity: warning
846
855
  title: "No SEO package detected"
847
856
  description: "Laravel benefits from an SEO package like artesaos/seotools."
@@ -893,6 +902,7 @@ checks:
893
902
  \`\`\`
894
903
 
895
904
  - code: LARAVEL_INERTIA_NO_SSR
905
+ indexable_only: true
896
906
  severity: error
897
907
  title: "Inertia.js without SSR"
898
908
  description: "Inertia apps are client-rendered by default, losing SEO benefits."
@@ -987,6 +997,7 @@ seo_strengths:
987
997
  checks:
988
998
  # App Router specific
989
999
  - code: NEXTJS_NO_METADATA_API
1000
+ indexable_only: true
990
1001
  severity: warning
991
1002
  title: "Not using Next.js Metadata API"
992
1003
  description: "Next.js 13+ has a built-in Metadata API that's better than react-helmet."
@@ -1259,6 +1270,7 @@ seo_strengths:
1259
1270
 
1260
1271
  checks:
1261
1272
  - code: NUXT_NO_USESEOMETA
1273
+ indexable_only: true
1262
1274
  severity: warning
1263
1275
  title: "Not using useSeoMeta composable"
1264
1276
  description: "Nuxt 3 has built-in SEO meta composables that are cleaner than useHead."
@@ -1368,6 +1380,7 @@ checks:
1368
1380
  \`\`\`
1369
1381
 
1370
1382
  - code: NUXT_SPA_MODE
1383
+ indexable_only: true
1371
1384
  severity: error
1372
1385
  title: "Nuxt running in SPA mode"
1373
1386
  description: "SPA mode disables SSR, losing SEO benefits."
@@ -1393,6 +1406,7 @@ checks:
1393
1406
  missing_content: true
1394
1407
 
1395
1408
  - code: NUXT_NO_PRERENDER
1409
+ indexable_only: true
1396
1410
  severity: notice
1397
1411
  title: "Static pages not pre-rendered"
1398
1412
  description: "Static pages could be pre-rendered for better performance."
@@ -1436,6 +1450,7 @@ seo_strengths:
1436
1450
 
1437
1451
  checks:
1438
1452
  - code: RAILS_NO_META_TAGS_GEM
1453
+ indexable_only: true
1439
1454
  severity: warning
1440
1455
  title: "Not using meta-tags gem"
1441
1456
  description: "The meta-tags gem provides a clean API for SEO tags."
@@ -1508,6 +1523,7 @@ checks:
1508
1523
  \`\`\`
1509
1524
 
1510
1525
  - code: RAILS_TURBO_BREAKS_CRAWLERS
1526
+ indexable_only: true
1511
1527
  severity: notice
1512
1528
  title: "Turbo may affect JavaScript-dependent crawlers"
1513
1529
  description: "Turbo Drive updates pages via JavaScript, which some crawlers may miss."
@@ -1630,6 +1646,7 @@ seo_challenges:
1630
1646
  checks:
1631
1647
  # Critical - these will severely hurt SEO
1632
1648
  - code: REACT_CSR_DETECTED
1649
+ indexable_only: true
1633
1650
  severity: error
1634
1651
  title: "Client-side rendered React app detected"
1635
1652
  description: "React apps without SSR send empty HTML to crawlers. Search engines may not index your content."
@@ -1645,6 +1662,7 @@ checks:
1645
1662
  missing_content: true # Body has minimal text content
1646
1663
 
1647
1664
  - code: REACT_NO_SSR_META
1665
+ indexable_only: true
1648
1666
  severity: error
1649
1667
  title: "Meta tags not server-rendered"
1650
1668
  description: "Title and meta description are set client-side and won't be seen by crawlers."
@@ -1711,6 +1729,7 @@ checks:
1711
1729
  \`\`\`
1712
1730
 
1713
1731
  - code: REACT_ROUTER_NO_SSR
1732
+ indexable_only: true
1714
1733
  severity: warning
1715
1734
  title: "React Router without server-side support"
1716
1735
  description: "Client-side routing breaks when crawlers access URLs directly."
@@ -1729,6 +1748,7 @@ checks:
1729
1748
  \`\`\`
1730
1749
 
1731
1750
  - code: REACT_NO_PRERENDER
1751
+ indexable_only: true
1732
1752
  severity: warning
1733
1753
  title: "No prerendering configured"
1734
1754
  description: "Static pages could be prerendered for better SEO and performance."
@@ -1885,6 +1905,7 @@ checks:
1885
1905
  - 'name="description"'
1886
1906
 
1887
1907
  - code: SVELTEKIT_NO_PRERENDER
1908
+ indexable_only: true
1888
1909
  severity: notice
1889
1910
  title: "Static pages not prerendered"
1890
1911
  description: "Static pages should be prerendered for best performance."
@@ -1990,6 +2011,7 @@ seo_challenges:
1990
2011
 
1991
2012
  checks:
1992
2013
  - code: VUE_CSR_DETECTED
2014
+ indexable_only: true
1993
2015
  severity: error
1994
2016
  title: "Client-side rendered Vue app detected"
1995
2017
  description: "Vue apps without SSR send empty HTML to crawlers."
@@ -2006,6 +2028,7 @@ checks:
2006
2028
  missing_content: true
2007
2029
 
2008
2030
  - code: VUE_NO_META_MANAGEMENT
2031
+ indexable_only: true
2009
2032
  severity: error
2010
2033
  title: "No meta tag management library"
2011
2034
  description: "Title and meta description are not dynamically managed."
@@ -4969,6 +4992,23 @@ function metaHasNoindex(content) {
4969
4992
  if (!content) return false;
4970
4993
  return content.toLowerCase().split(",").some((d) => ["noindex", "none"].includes(d.trim()));
4971
4994
  }
4995
+ function readRobotsDirectives(html, headers) {
4996
+ const $ = cheerio.load(html);
4997
+ const metaValues = [];
4998
+ $("meta[name]").each((_, el) => {
4999
+ const name = ($(el).attr("name") || "").toLowerCase();
5000
+ if (name === "robots" || name === "googlebot") {
5001
+ metaValues.push($(el).attr("content") || "");
5002
+ }
5003
+ });
5004
+ const metaNoindex = metaValues.some(metaHasNoindex);
5005
+ const xRobotsTag = headers ? Object.entries(headers).find(([k]) => k.toLowerCase() === "x-robots-tag")?.[1] : void 0;
5006
+ const headerNoindex = xRobotsTagHasNoindex(xRobotsTag);
5007
+ return { metaValues, metaNoindex, xRobotsTag, headerNoindex, hasNoindex: metaNoindex || headerNoindex };
5008
+ }
5009
+ function pageHasNoindex(html, headers) {
5010
+ return readRobotsDirectives(html, headers).hasNoindex;
5011
+ }
4972
5012
  function isSiteRoot(url) {
4973
5013
  try {
4974
5014
  const u = new URL(url);
@@ -4984,17 +5024,7 @@ function analyzeOnPage(html, url, headers, options = {}) {
4984
5024
  const description = $('meta[name="description"]').attr("content")?.trim();
4985
5025
  const canonical = $('link[rel="canonical"]').attr("href");
4986
5026
  const metaRobots = $('meta[name="robots"]').attr("content");
4987
- const robotsMetaValues = [];
4988
- $("meta[name]").each((_, el) => {
4989
- const name = ($(el).attr("name") || "").toLowerCase();
4990
- if (name === "robots" || name === "googlebot") {
4991
- robotsMetaValues.push($(el).attr("content") || "");
4992
- }
4993
- });
4994
- const metaNoindex = robotsMetaValues.some(metaHasNoindex);
4995
- const headerLookup = headers ? Object.entries(headers).find(([k]) => k.toLowerCase() === "x-robots-tag")?.[1] : void 0;
4996
- const headerNoindex = xRobotsTagHasNoindex(headerLookup);
4997
- const hasNoindex = metaNoindex || headerNoindex;
5027
+ const { metaValues: robotsMetaValues, metaNoindex, xRobotsTag: headerLookup, headerNoindex, hasNoindex } = readRobotsDirectives(html, headers);
4998
5028
  const h1s = [];
4999
5029
  $("h1").each((_, el) => {
5000
5030
  h1s.push($(el).text().trim());
@@ -12394,6 +12424,17 @@ var NAMED_COLORS = {
12394
12424
  transparent: [255, 255, 255]
12395
12425
  // Treat as white for calculation
12396
12426
  };
12427
+ function inheritedBackground($el) {
12428
+ for (const ancestor of $el.parents().toArray()) {
12429
+ const style = ancestor.attribs?.style;
12430
+ if (!style) continue;
12431
+ const match = style.match(/(?:^|;)\s*background(?:-color)?\s*:\s*([^;]+)/i);
12432
+ if (!match) continue;
12433
+ const value = match[1].trim();
12434
+ if (!/^(transparent|none)$/i.test(value)) return value;
12435
+ }
12436
+ return null;
12437
+ }
12397
12438
  function analyzeColorContrast(html, url) {
12398
12439
  const issues = [];
12399
12440
  const $ = cheerio32.load(html);
@@ -12414,12 +12455,16 @@ function analyzeColorContrast(html, url) {
12414
12455
  const colorMatch = style.match(/(?:^|;)\s*color\s*:\s*([^;]+)/i);
12415
12456
  const bgMatch = style.match(/(?:^|;)\s*background(?:-color)?\s*:\s*([^;]+)/i);
12416
12457
  const foreground = colorMatch ? colorMatch[1].trim() : null;
12417
- const background = bgMatch ? bgMatch[1].trim() : null;
12458
+ const background = bgMatch ? bgMatch[1].trim() : inheritedBackground($el);
12418
12459
  if (!foreground) {
12419
12460
  cannotDetermine++;
12420
12461
  return;
12421
12462
  }
12422
- const bgColor = background || "white";
12463
+ if (!background) {
12464
+ cannotDetermine++;
12465
+ return;
12466
+ }
12467
+ const bgColor = background;
12423
12468
  const fgRgb = parseColor(foreground);
12424
12469
  const bgRgb = parseColor(bgColor);
12425
12470
  if (!fgRgb || !bgRgb) {
@@ -14112,7 +14157,9 @@ async function runFullAudit(options) {
14112
14157
  asyncChecks.push(safeAsyncIssues("Framework Detection", async () => {
14113
14158
  const frameworkInfo = detectFramework({ html, headers });
14114
14159
  if (frameworkInfo.framework === "unknown") return [];
14115
- return getFrameworkIssues(frameworkInfo, html, url);
14160
+ return getFrameworkIssues(frameworkInfo, html, url, {
14161
+ indexable: !pageHasNoindex(html, headers)
14162
+ });
14116
14163
  }));
14117
14164
  if (runExtendedChecks) {
14118
14165
  asyncChecks.push(safeAsync("Hreflang", () => analyzeHreflang(html, url, { validateUrls: checkHreflangUrls })));
@@ -14251,7 +14298,7 @@ function calculateHealthScore(issues) {
14251
14298
  const links = Math.max(0, 100 - categoryDeductions.links * 3);
14252
14299
  const performance = Math.max(0, 100 - categoryDeductions.performance * 4);
14253
14300
  const security = Math.max(0, 100 - categoryDeductions.security * 5);
14254
- const aiReadiness = Math.max(0, 100 - categoryDeductions["ai-readiness"] * 3);
14301
+ const aiReadiness = Math.max(0, 100 - categoryDeductions["ai-readiness"] * 1);
14255
14302
  const social = Math.max(0, 100 - categoryDeductions.social * 2);
14256
14303
  const localSeo = Math.max(0, 100 - categoryDeductions["local-seo"] * 2);
14257
14304
  const accessibility = Math.max(0, 100 - categoryDeductions.accessibility * 4);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rankcli/agent-runtime",
3
- "version": "0.0.32",
3
+ "version": "0.0.34",
4
4
  "description": "RankCLI agent runtime - executes SEO audits and fixes with AI",
5
5
  "homepage": "https://rankcli.dev",
6
6
  "main": "dist/index.js",