@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 +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +65 -18
- package/dist/index.mjs +65 -18
- package/package.json +1 -1
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
|
|
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
|
|
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() :
|
|
16012
|
+
const background = bgMatch ? bgMatch[1].trim() : inheritedBackground($el);
|
|
15972
16013
|
if (!foreground) {
|
|
15973
16014
|
cannotDetermine++;
|
|
15974
16015
|
return;
|
|
15975
16016
|
}
|
|
15976
|
-
|
|
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"] *
|
|
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() :
|
|
12458
|
+
const background = bgMatch ? bgMatch[1].trim() : inheritedBackground($el);
|
|
12418
12459
|
if (!foreground) {
|
|
12419
12460
|
cannotDetermine++;
|
|
12420
12461
|
return;
|
|
12421
12462
|
}
|
|
12422
|
-
|
|
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"] *
|
|
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);
|