@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.d.mts +38 -1
- package/dist/index.d.ts +38 -1
- package/dist/index.js +243 -125
- package/dist/index.mjs +210 -95
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -2824,7 +2824,7 @@ var ISSUE_DEFINITIONS = {
|
|
|
2824
2824
|
severity: "warning",
|
|
2825
2825
|
category: "performance",
|
|
2826
2826
|
title: "HTML document too large",
|
|
2827
|
-
description: "HTML size exceeds
|
|
2827
|
+
description: "HTML size exceeds 300KB.",
|
|
2828
2828
|
impact: "Increases load time and memory usage.",
|
|
2829
2829
|
howToFix: "Reduce inline CSS/JS, remove unnecessary HTML."
|
|
2830
2830
|
},
|
|
@@ -5559,7 +5559,7 @@ async function analyzePerformance(url) {
|
|
|
5559
5559
|
details: { ttfb: `${ttfbTime}ms` }
|
|
5560
5560
|
});
|
|
5561
5561
|
}
|
|
5562
|
-
if (htmlSize >
|
|
5562
|
+
if (htmlSize > 300 * 1024) {
|
|
5563
5563
|
issues.push({
|
|
5564
5564
|
...ISSUE_DEFINITIONS.HTML_TOO_LARGE,
|
|
5565
5565
|
affectedUrls: [url],
|
|
@@ -7717,7 +7717,7 @@ function analyzeLocalSEO(html, url) {
|
|
|
7717
7717
|
const detectedPhones = detectPhones(html);
|
|
7718
7718
|
const detectedAddresses = detectAddresses(html);
|
|
7719
7719
|
const socialProfiles = detectSocialProfiles(html);
|
|
7720
|
-
if (!hasLocalBusiness) {
|
|
7720
|
+
if (!hasLocalBusiness && (detectedPhones.length > 0 || detectedAddresses.length > 0)) {
|
|
7721
7721
|
issues.push({
|
|
7722
7722
|
...ISSUE_DEFINITIONS.LOCAL_BUSINESS_SCHEMA_MISSING,
|
|
7723
7723
|
affectedUrls: [url]
|
|
@@ -8778,7 +8778,17 @@ function analyzeKeywords(html, url) {
|
|
|
8778
8778
|
const sortPhrases = (phrases, n) => {
|
|
8779
8779
|
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 }));
|
|
8780
8780
|
};
|
|
8781
|
-
const
|
|
8781
|
+
const brandLabels = /* @__PURE__ */ new Set();
|
|
8782
|
+
try {
|
|
8783
|
+
const host = new URL(url).hostname.toLowerCase().replace(/^www\./, "");
|
|
8784
|
+
for (const label of host.split(".").slice(0, -1)) brandLabels.add(label);
|
|
8785
|
+
} catch {
|
|
8786
|
+
}
|
|
8787
|
+
for (const name of [$('meta[property="og:site_name"]').attr("content"), $('meta[name="application-name"]').attr("content")]) {
|
|
8788
|
+
for (const token of tokenize(name || "")) brandLabels.add(token);
|
|
8789
|
+
}
|
|
8790
|
+
const isBrandWord = (word) => [...brandLabels].some((label) => label.includes(word) || word.includes(label));
|
|
8791
|
+
const potentialStuffing = totalWords >= 200 ? topKeywords.filter((k) => k.density > 4 && !isBrandWord(k.word)).map((k) => k.word) : [];
|
|
8782
8792
|
if (potentialStuffing.length > 0) {
|
|
8783
8793
|
issues.push({
|
|
8784
8794
|
...ISSUE_DEFINITIONS.KEYWORD_STUFFING,
|
|
@@ -9004,13 +9014,6 @@ function checkAMP(html, url) {
|
|
|
9004
9014
|
}
|
|
9005
9015
|
};
|
|
9006
9016
|
}
|
|
9007
|
-
const isArticle = $("article").length > 0 || $('meta[property="og:type"][content="article"]').length > 0 || html.toLowerCase().includes("blog") || html.toLowerCase().includes("news");
|
|
9008
|
-
if (isArticle) {
|
|
9009
|
-
issues.push({
|
|
9010
|
-
...ISSUE_DEFINITIONS.AMP_MISSING,
|
|
9011
|
-
affectedUrls: [url]
|
|
9012
|
-
});
|
|
9013
|
-
}
|
|
9014
9017
|
return {
|
|
9015
9018
|
issues,
|
|
9016
9019
|
data: {
|
|
@@ -9719,13 +9722,13 @@ async function analyzeSiteMaturity(html, url) {
|
|
|
9719
9722
|
contentToAuthorityRatio
|
|
9720
9723
|
};
|
|
9721
9724
|
const { strategy, recommendations } = determineStrategy(maturityLevel, authorityIndicators, contentMetrics);
|
|
9722
|
-
if (maturityLevel === "new") {
|
|
9725
|
+
if (maturityLevel === "new" && ageSource === "wayback" && estimatedAgeDays !== null) {
|
|
9723
9726
|
issues.push({
|
|
9724
9727
|
code: "NEW_SITE_STRATEGY",
|
|
9725
9728
|
severity: "notice",
|
|
9726
9729
|
category: "content",
|
|
9727
9730
|
title: "New website detected - Authority-first strategy recommended",
|
|
9728
|
-
description:
|
|
9731
|
+
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.`,
|
|
9729
9732
|
impact: "Creating lots of content without authority is wasted effort. Content from new sites rarely ranks.",
|
|
9730
9733
|
howToFix: "Focus on building 10+ quality backlinks to ONE great page before expanding content. See strategic recommendations.",
|
|
9731
9734
|
affectedUrls: [url],
|
|
@@ -9736,24 +9739,7 @@ async function analyzeSiteMaturity(html, url) {
|
|
|
9736
9739
|
}
|
|
9737
9740
|
});
|
|
9738
9741
|
}
|
|
9739
|
-
if (
|
|
9740
|
-
issues.push({
|
|
9741
|
-
code: "CONTENT_AUTHORITY_IMBALANCE",
|
|
9742
|
-
severity: "warning",
|
|
9743
|
-
category: "content",
|
|
9744
|
-
title: "Content-to-authority imbalance",
|
|
9745
|
-
description: `Site has ~${indexedPages} pages but limited backlink signals. Content is unlikely to rank.`,
|
|
9746
|
-
impact: "Most of your content may be invisible to search engines due to low domain authority.",
|
|
9747
|
-
howToFix: "Stop publishing new content. Focus 100% on link building until you see ranking improvements.",
|
|
9748
|
-
affectedUrls: [url],
|
|
9749
|
-
details: {
|
|
9750
|
-
indexedPages,
|
|
9751
|
-
contentToAuthorityRatio,
|
|
9752
|
-
maturityLevel
|
|
9753
|
-
}
|
|
9754
|
-
});
|
|
9755
|
-
}
|
|
9756
|
-
if (maturityLevel === "growing" && !htmlSignals.hasBlog) {
|
|
9742
|
+
if (maturityLevel === "growing" && ageSource === "wayback" && !htmlSignals.hasBlog) {
|
|
9757
9743
|
issues.push({
|
|
9758
9744
|
code: "GROWING_SITE_NO_BLOG",
|
|
9759
9745
|
severity: "notice",
|
|
@@ -10247,8 +10233,8 @@ function analyzeRendering(html) {
|
|
|
10247
10233
|
const hasSemanticStructure = hasH1 && (hasMain || hasArticle);
|
|
10248
10234
|
const title = $("title").text().trim();
|
|
10249
10235
|
const h1Text = $("h1").first().text().trim();
|
|
10250
|
-
const
|
|
10251
|
-
const keyContentInHTML = title.length >
|
|
10236
|
+
const hasSubstantialParagraph = $("p").toArray().some((p) => $(p).text().trim().length > 50);
|
|
10237
|
+
const keyContentInHTML = title.length > 0 && h1Text.length > 0 && hasSubstantialParagraph;
|
|
10252
10238
|
const EXPECTED_MIN_CONTENT = 1e3;
|
|
10253
10239
|
const htmlContentRatio = Math.min(100, mainContentCharCount / EXPECTED_MIN_CONTENT * 100);
|
|
10254
10240
|
let aiReadableScore = 50;
|
|
@@ -10468,9 +10454,9 @@ function analyzeClientRendering(html, url) {
|
|
|
10468
10454
|
howToFix: "Ensure the initial HTML contains: meaningful <title>, descriptive <h1>, and a substantial first paragraph with your key message.",
|
|
10469
10455
|
affectedUrls: [url],
|
|
10470
10456
|
details: {
|
|
10471
|
-
hasTitle: $("title").text().trim().length >
|
|
10472
|
-
hasH1: $("h1").text().trim().length >
|
|
10473
|
-
|
|
10457
|
+
hasTitle: $("title").text().trim().length > 0,
|
|
10458
|
+
hasH1: $("h1").text().trim().length > 0,
|
|
10459
|
+
hasSubstantialParagraph: $("p").toArray().some((p) => $(p).text().trim().length > 50)
|
|
10474
10460
|
}
|
|
10475
10461
|
});
|
|
10476
10462
|
}
|
|
@@ -11179,7 +11165,9 @@ async function analyzeBingOptimization(html, url) {
|
|
|
11179
11165
|
detectedIssues.push("No Bing site verification");
|
|
11180
11166
|
issues.push({
|
|
11181
11167
|
code: "BING_NOT_VERIFIED",
|
|
11182
|
-
|
|
11168
|
+
// A notice: verification done through Search Console import or a DNS record this run
|
|
11169
|
+
// could not see leaves no trace in the HTML, so this cannot be a warning.
|
|
11170
|
+
severity: "notice",
|
|
11183
11171
|
category: "ai-readiness",
|
|
11184
11172
|
title: verification.dnsChecked ? "Site not verified with Bing Webmaster Tools" : "Bing Webmaster Tools verification not visible in the HTML",
|
|
11185
11173
|
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.",
|
|
@@ -11230,7 +11218,7 @@ async function analyzeBingOptimization(html, url) {
|
|
|
11230
11218
|
});
|
|
11231
11219
|
}
|
|
11232
11220
|
const hasLocalBusinessSchema = bingPlacesDetected;
|
|
11233
|
-
const hasAddressOnPage = $("address").length > 0 || html
|
|
11221
|
+
const hasAddressOnPage = $("address").text().trim().length > 0 || detectAddresses(html).length > 0;
|
|
11234
11222
|
if (hasAddressOnPage && !hasLocalBusinessSchema) {
|
|
11235
11223
|
detectedIssues.push("Local business may not be on Bing Places");
|
|
11236
11224
|
issues.push({
|
|
@@ -12756,23 +12744,6 @@ function analyzeColorContrast(html, url) {
|
|
|
12756
12744
|
});
|
|
12757
12745
|
}
|
|
12758
12746
|
}
|
|
12759
|
-
if (cannotDetermine > elementsAnalyzed * 0.8) {
|
|
12760
|
-
issues.push({
|
|
12761
|
-
code: "A11Y_CONTRAST_UNDETERMINED",
|
|
12762
|
-
severity: "notice",
|
|
12763
|
-
category: "accessibility",
|
|
12764
|
-
title: "Color contrast could not be fully analyzed",
|
|
12765
|
-
description: "Most text colors are set via CSS stylesheets rather than inline styles. Full contrast checking requires browser rendering.",
|
|
12766
|
-
impact: "Potential contrast issues may exist that were not detected.",
|
|
12767
|
-
howToFix: "Use browser DevTools (Lighthouse accessibility audit) or dedicated tools like axe-core for comprehensive contrast checking.",
|
|
12768
|
-
affectedUrls: [url],
|
|
12769
|
-
details: {
|
|
12770
|
-
elementsAnalyzed,
|
|
12771
|
-
cannotDetermine,
|
|
12772
|
-
recommendation: "Run Lighthouse accessibility audit for complete contrast analysis."
|
|
12773
|
-
}
|
|
12774
|
-
});
|
|
12775
|
-
}
|
|
12776
12747
|
return {
|
|
12777
12748
|
issues,
|
|
12778
12749
|
data: {
|
|
@@ -14094,7 +14065,8 @@ async function analyzeTrackingVerification(html, url) {
|
|
|
14094
14065
|
if (!hasGa4 && !hasGtm && otherAnalytics.length === 0) {
|
|
14095
14066
|
issues.push({
|
|
14096
14067
|
code: "GA4_MISSING",
|
|
14097
|
-
|
|
14068
|
+
// A notice: which analytics a site runs is a choice, not an SEO defect.
|
|
14069
|
+
severity: "notice",
|
|
14098
14070
|
category: "on-page",
|
|
14099
14071
|
title: "Google Analytics 4 not detected",
|
|
14100
14072
|
description: "No GA4 tracking code found. GA4 is essential for measuring organic traffic and conversions.",
|
|
@@ -14107,7 +14079,8 @@ async function analyzeTrackingVerification(html, url) {
|
|
|
14107
14079
|
const dnsChecked = verification.dnsChecked;
|
|
14108
14080
|
issues.push({
|
|
14109
14081
|
code: "GSC_VERIFICATION_MISSING",
|
|
14110
|
-
|
|
14082
|
+
// A notice: a site verified by DNS, Google Analytics or Tag Manager leaves no trace here.
|
|
14083
|
+
severity: "notice",
|
|
14111
14084
|
category: "on-page",
|
|
14112
14085
|
title: dnsChecked ? "Google Search Console verification not found" : "Google Search Console verification not visible in the HTML",
|
|
14113
14086
|
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.",
|
|
@@ -14182,6 +14155,113 @@ async function analyzeTrackingVerification(html, url) {
|
|
|
14182
14155
|
return { issues, data };
|
|
14183
14156
|
}
|
|
14184
14157
|
|
|
14158
|
+
// src/audit/meta-refresh.ts
|
|
14159
|
+
import * as cheerio37 from "cheerio";
|
|
14160
|
+
function metaRefreshTarget(html, pageUrl) {
|
|
14161
|
+
if (!/http-equiv\s*=\s*["']?refresh/i.test(html)) return null;
|
|
14162
|
+
const $ = cheerio37.load(html);
|
|
14163
|
+
for (const el of $("meta[http-equiv]").toArray()) {
|
|
14164
|
+
if (($(el).attr("http-equiv") || "").trim().toLowerCase() !== "refresh") continue;
|
|
14165
|
+
const content = $(el).attr("content") || "";
|
|
14166
|
+
const match = content.match(/^\s*(\d+(?:\.\d+)?)\s*[;,]\s*(?:url\s*=\s*)?['"]?\s*([^'"\s>]+)/i);
|
|
14167
|
+
if (!match) continue;
|
|
14168
|
+
if (Number(match[1]) > 5) continue;
|
|
14169
|
+
try {
|
|
14170
|
+
const target = new URL(match[2], pageUrl);
|
|
14171
|
+
if (target.protocol !== "http:" && target.protocol !== "https:") continue;
|
|
14172
|
+
const strip = (u) => u.origin + (u.pathname.replace(/\/+$/, "") || "/") + u.search;
|
|
14173
|
+
if (strip(target) === strip(new URL(pageUrl))) continue;
|
|
14174
|
+
return target.toString();
|
|
14175
|
+
} catch {
|
|
14176
|
+
continue;
|
|
14177
|
+
}
|
|
14178
|
+
}
|
|
14179
|
+
return null;
|
|
14180
|
+
}
|
|
14181
|
+
|
|
14182
|
+
// src/audit/dedupe.ts
|
|
14183
|
+
var REDUNDANT_WHEN_PRESENT = [
|
|
14184
|
+
// The same tag, reported by three checks.
|
|
14185
|
+
{ keep: "VIEWPORT_MISSING", drop: ["HTML_NO_VIEWPORT", "RESPONSIVE_NO_VIEWPORT"] },
|
|
14186
|
+
{ keep: "LANG_ATTR_MISSING", drop: ["HTML_NO_LANG"] },
|
|
14187
|
+
{ keep: "APPLE_TOUCH_ICON_MISSING", drop: ["HTML_NO_APPLE_TOUCH_ICON"] },
|
|
14188
|
+
{ keep: "LLMS_TXT_MISSING", drop: ["AI_NO_LLMS_TXT"] },
|
|
14189
|
+
{ keep: "INDEXNOW_NOT_IMPLEMENTED", drop: ["BING_NO_INDEXNOW"] },
|
|
14190
|
+
{ keep: "BING_VERIFICATION_MISSING", drop: ["BING_NOT_VERIFIED"] },
|
|
14191
|
+
{ keep: "SITEMAP_MISSING", drop: ["BING_SITEMAP_MISSING", "SITEMAP_NOT_IN_ROBOTS"] },
|
|
14192
|
+
{ keep: "ROBOTS_TXT_MISSING", drop: ["SITEMAP_NOT_IN_ROBOTS"] },
|
|
14193
|
+
{ keep: "IMAGES_MISSING_DIMENSIONS", drop: ["IMG_NO_DIMENSIONS"] },
|
|
14194
|
+
{ keep: "NO_MODERN_IMAGE_FORMATS", drop: ["IMAGE_NOT_WEBP_AVIF"] },
|
|
14195
|
+
{ keep: "SOCIAL_PROFILES_NOT_LINKED", drop: ["NO_SOCIAL_PROFILES"] },
|
|
14196
|
+
{ keep: "NO_EXPERIENCE_SIGNALS", drop: ["AI_NO_EXPERIENCE_SIGNALS"] },
|
|
14197
|
+
// No structured data at all makes every "your schema lacks X" finding moot.
|
|
14198
|
+
{
|
|
14199
|
+
keep: "SCHEMA_MISSING",
|
|
14200
|
+
drop: [
|
|
14201
|
+
"SCHEMA_ORG_MISSING",
|
|
14202
|
+
"NO_ORGANIZATION_SCHEMA",
|
|
14203
|
+
"ORGANIZATION_SCHEMA_MISSING",
|
|
14204
|
+
"NO_ENTITY_SCHEMA",
|
|
14205
|
+
"NO_DATE_MODIFIED_SCHEMA",
|
|
14206
|
+
"NO_MAIN_ENTITY_OF_PAGE",
|
|
14207
|
+
"AUTHOR_NO_SCHEMA",
|
|
14208
|
+
"AI_NO_REVIEW_SCHEMA",
|
|
14209
|
+
"NO_SCHEMA_SAMEAS"
|
|
14210
|
+
]
|
|
14211
|
+
},
|
|
14212
|
+
{ keep: "NO_ORGANIZATION_SCHEMA", drop: ["ORGANIZATION_SCHEMA_MISSING"] },
|
|
14213
|
+
// No title, no H1: the findings about their wording are implied.
|
|
14214
|
+
{ keep: "TITLE_MISSING", drop: ["LOW_CTR_POTENTIAL", "NO_KEYWORDS_IN_TITLE", "TITLE_TOO_SHORT"] },
|
|
14215
|
+
{ keep: "META_DESC_MISSING", drop: ["LOW_CTR_POTENTIAL"] },
|
|
14216
|
+
{ keep: "H1_MISSING", drop: ["NO_VISIBLE_HEADLINE", "NO_KEYWORDS_IN_H1", "H1_MISSING_KEYWORD"] },
|
|
14217
|
+
// Client-side rendering, diagnosed once. CLIENT_SIDE_RENDERING carries the per-framework fix.
|
|
14218
|
+
{
|
|
14219
|
+
keep: "CLIENT_SIDE_RENDERING",
|
|
14220
|
+
drop: [
|
|
14221
|
+
"REACT_CSR_DETECTED",
|
|
14222
|
+
"VUE_CSR_DETECTED",
|
|
14223
|
+
"ANGULAR_NO_UNIVERSAL",
|
|
14224
|
+
"SPA_WITHOUT_SSR",
|
|
14225
|
+
"EMPTY_BODY_CONTENT",
|
|
14226
|
+
"HIGH_JS_RENDERING_RATIO",
|
|
14227
|
+
"AI_LOW_HTML_READABILITY"
|
|
14228
|
+
]
|
|
14229
|
+
},
|
|
14230
|
+
// An empty body has no vocabulary to be undiverse.
|
|
14231
|
+
{ keep: "EMPTY_BODY_CONTENT", drop: ["THIN_CONTENT", "LOW_CONTENT_ENTROPY", "HIGH_CONTENT_REPETITION", "LOW_DWELL_TIME_CONTENT", "LOW_TEXT_HTML_RATIO"] },
|
|
14232
|
+
{ keep: "THIN_CONTENT", drop: ["LOW_DWELL_TIME_CONTENT"] },
|
|
14233
|
+
{ keep: "NO_INTERNAL_LINKS", drop: ["NO_CONTENT_INTERNAL_LINKS", "HIGH_ORPHAN_RISK", "LINKS_NO_INTERNAL"] },
|
|
14234
|
+
// No testimonials on the page, said once instead of six times.
|
|
14235
|
+
{ keep: "NO_SOCIAL_PROOF", drop: ["BOFU_NO_SOCIAL_PROOF", "AI_NO_TESTIMONIALS", "AI_NO_REVIEW_INTEGRATION", "AI_NO_REVIEW_PLATFORMS", "AI_WEAK_REVIEW_ECOSYSTEM"] },
|
|
14236
|
+
{ keep: "AI_NO_TESTIMONIALS", drop: ["AI_NO_REVIEW_INTEGRATION", "AI_NO_REVIEW_PLATFORMS", "AI_WEAK_REVIEW_ECOSYSTEM"] },
|
|
14237
|
+
// The brand-score composite restates the brand-definition finding it is built from.
|
|
14238
|
+
{ keep: "AI_NO_BRAND_DEFINITION", drop: ["AI_LOW_BRAND_SCORE", "NO_ENTITY_DEFINITION"] }
|
|
14239
|
+
];
|
|
14240
|
+
function dedupeIssues(issues) {
|
|
14241
|
+
const present = new Set(issues.map((i) => i.code));
|
|
14242
|
+
const redundant = /* @__PURE__ */ new Set();
|
|
14243
|
+
for (const rule of REDUNDANT_WHEN_PRESENT) {
|
|
14244
|
+
const fires = typeof rule.keep === "string" ? present.has(rule.keep) : [...present].some((code) => rule.keep.test(code));
|
|
14245
|
+
if (!fires) continue;
|
|
14246
|
+
for (const code of rule.drop) redundant.add(code);
|
|
14247
|
+
}
|
|
14248
|
+
const kept = /* @__PURE__ */ new Map();
|
|
14249
|
+
const out = [];
|
|
14250
|
+
for (const issue of issues) {
|
|
14251
|
+
if (redundant.has(issue.code)) continue;
|
|
14252
|
+
const earlier = kept.get(issue.code);
|
|
14253
|
+
if (earlier) {
|
|
14254
|
+
const urls = /* @__PURE__ */ new Set([...earlier.affectedUrls || [], ...issue.affectedUrls || []]);
|
|
14255
|
+
earlier.affectedUrls = [...urls];
|
|
14256
|
+
continue;
|
|
14257
|
+
}
|
|
14258
|
+
const copy = { ...issue };
|
|
14259
|
+
kept.set(issue.code, copy);
|
|
14260
|
+
out.push(copy);
|
|
14261
|
+
}
|
|
14262
|
+
return out;
|
|
14263
|
+
}
|
|
14264
|
+
|
|
14185
14265
|
// src/audit/engine.ts
|
|
14186
14266
|
function sameUrl(a, b) {
|
|
14187
14267
|
try {
|
|
@@ -14194,15 +14274,15 @@ function sameUrl(a, b) {
|
|
|
14194
14274
|
}
|
|
14195
14275
|
}
|
|
14196
14276
|
async function runFullAudit(options) {
|
|
14197
|
-
const {
|
|
14277
|
+
const { checkBrokenLinks = false, checkHreflangUrls = false, checkCanonicalChain = false, cruxApiKey, checksLimit = 280, premiumChecks } = options;
|
|
14278
|
+
let url = options.url;
|
|
14198
14279
|
const auditRoot = isSiteRoot(url) || sameUrl(url, options.auditRootUrl ?? url);
|
|
14199
14280
|
const allIssues = [];
|
|
14200
14281
|
const pages = [];
|
|
14201
14282
|
const tier = checksLimit <= 50 ? "free" : checksLimit <= 100 ? "registered" : "paid";
|
|
14202
14283
|
const runPremiumChecks = tier === "paid" && !!premiumChecks;
|
|
14203
14284
|
const runExtendedChecks = tier !== "free";
|
|
14204
|
-
|
|
14205
|
-
const domain = parsedUrl.hostname;
|
|
14285
|
+
let domain = new URL(url).hostname;
|
|
14206
14286
|
console.error(`
|
|
14207
14287
|
\u{1F50D} Running comprehensive SEO audit on ${url}...
|
|
14208
14288
|
`);
|
|
@@ -14229,7 +14309,9 @@ async function runFullAudit(options) {
|
|
|
14229
14309
|
timeout: 3e4,
|
|
14230
14310
|
validateStatus: () => true
|
|
14231
14311
|
}).catch((err) => {
|
|
14232
|
-
|
|
14312
|
+
const cause = err instanceof Error ? err.cause : void 0;
|
|
14313
|
+
const causeText = cause instanceof Error ? ` (${cause.code || cause.message})` : "";
|
|
14314
|
+
console.error(`Main page fetch failed: ${err instanceof Error ? err.message : String(err)}${causeText}`);
|
|
14233
14315
|
return { error: err, data: "", headers: {} };
|
|
14234
14316
|
})
|
|
14235
14317
|
]);
|
|
@@ -14252,7 +14334,7 @@ async function runFullAudit(options) {
|
|
|
14252
14334
|
});
|
|
14253
14335
|
return createReport(url, domain, allIssues, pages, { kind: "unreachable" });
|
|
14254
14336
|
}
|
|
14255
|
-
|
|
14337
|
+
let statusCode = fetchResult.status;
|
|
14256
14338
|
if (statusCode >= 400) {
|
|
14257
14339
|
const blocked = statusCode === 401 || statusCode === 403 || statusCode === 429;
|
|
14258
14340
|
allIssues.push({
|
|
@@ -14265,10 +14347,42 @@ async function runFullAudit(options) {
|
|
|
14265
14347
|
pages.push({ url, statusCode, wordCount: 0, loadTime: 0, issues: allIssues.map((i) => i.code) });
|
|
14266
14348
|
return createReport(url, domain, allIssues, pages, { kind: "http-error", statusCode });
|
|
14267
14349
|
}
|
|
14268
|
-
const
|
|
14350
|
+
const asText = (data) => typeof data === "string" ? data : JSON.stringify(data ?? "");
|
|
14351
|
+
let body = asText(fetchResult.data);
|
|
14352
|
+
let headers = fetchResult.headers;
|
|
14353
|
+
const requestedUrl = url;
|
|
14354
|
+
const refreshHops = [];
|
|
14355
|
+
for (let hop = 0; hop < 3; hop++) {
|
|
14356
|
+
const target = metaRefreshTarget(body, url);
|
|
14357
|
+
if (!target) break;
|
|
14358
|
+
try {
|
|
14359
|
+
const next = await httpGet(target, { timeout: 3e4, validateStatus: () => true });
|
|
14360
|
+
if (next.status >= 400) break;
|
|
14361
|
+
refreshHops.push(target);
|
|
14362
|
+
url = target;
|
|
14363
|
+
body = asText(next.data);
|
|
14364
|
+
headers = next.headers;
|
|
14365
|
+
statusCode = next.status;
|
|
14366
|
+
} catch {
|
|
14367
|
+
break;
|
|
14368
|
+
}
|
|
14369
|
+
}
|
|
14370
|
+
if (refreshHops.length > 0) {
|
|
14371
|
+
domain = new URL(url).hostname;
|
|
14372
|
+
allIssues.push({
|
|
14373
|
+
code: "META_REFRESH_REDIRECT",
|
|
14374
|
+
severity: "notice",
|
|
14375
|
+
category: "crawlability",
|
|
14376
|
+
title: "Page redirects with a meta refresh",
|
|
14377
|
+
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.`,
|
|
14378
|
+
impact: "Some crawlers and link-checking tools stop at the stub; link signals to the original URL may not all pass to the target.",
|
|
14379
|
+
howToFix: "Return a 301 (or 308) redirect from the server or CDN for this URL instead of a meta refresh.",
|
|
14380
|
+
affectedUrls: [requestedUrl],
|
|
14381
|
+
details: { from: requestedUrl, to: url, hops: refreshHops }
|
|
14382
|
+
});
|
|
14383
|
+
}
|
|
14269
14384
|
const html = body.replace(/<\/?noscript[^>]*>/gi, "");
|
|
14270
|
-
|
|
14271
|
-
console.error(`\u{1F4DD} Phase 2: Running synchronous HTML checks (tier: ${tier}, limit: ${checksLimit})...`);
|
|
14385
|
+
console.error("\u{1F4DD} Phase 2: Running HTML checks...");
|
|
14272
14386
|
const onPageResult = analyzeOnPage(html, url, headers, { auditRoot });
|
|
14273
14387
|
const structuredDataResult = analyzeStructuredData2(html, url);
|
|
14274
14388
|
const mobileResult = analyzeMobile(html, url);
|
|
@@ -14435,10 +14549,10 @@ async function runFullAudit(options) {
|
|
|
14435
14549
|
if (cruxApiKey) {
|
|
14436
14550
|
asyncChecks.push(safeAsyncIssues("Core Web Vitals", async () => {
|
|
14437
14551
|
const cwv = await fetchCoreWebVitals(url, cruxApiKey);
|
|
14438
|
-
const
|
|
14552
|
+
const issues2 = [];
|
|
14439
14553
|
if (cwv) {
|
|
14440
14554
|
if (cwv.lcp?.rating === "poor") {
|
|
14441
|
-
|
|
14555
|
+
issues2.push({
|
|
14442
14556
|
code: "LCP_POOR",
|
|
14443
14557
|
severity: "error",
|
|
14444
14558
|
category: "performance",
|
|
@@ -14451,7 +14565,7 @@ async function runFullAudit(options) {
|
|
|
14451
14565
|
});
|
|
14452
14566
|
}
|
|
14453
14567
|
if (cwv.cls?.rating === "poor") {
|
|
14454
|
-
|
|
14568
|
+
issues2.push({
|
|
14455
14569
|
code: "CLS_POOR",
|
|
14456
14570
|
severity: "error",
|
|
14457
14571
|
category: "performance",
|
|
@@ -14464,14 +14578,15 @@ async function runFullAudit(options) {
|
|
|
14464
14578
|
});
|
|
14465
14579
|
}
|
|
14466
14580
|
}
|
|
14467
|
-
return
|
|
14581
|
+
return issues2;
|
|
14468
14582
|
}));
|
|
14469
14583
|
}
|
|
14470
14584
|
}
|
|
14471
14585
|
const asyncResults = await Promise.all(asyncChecks);
|
|
14472
|
-
for (const
|
|
14473
|
-
allIssues.push(...
|
|
14586
|
+
for (const issues2 of asyncResults) {
|
|
14587
|
+
allIssues.push(...issues2);
|
|
14474
14588
|
}
|
|
14589
|
+
const issues = dedupeIssues(allIssues);
|
|
14475
14590
|
pages.push({
|
|
14476
14591
|
url,
|
|
14477
14592
|
statusCode,
|
|
@@ -14481,10 +14596,10 @@ async function runFullAudit(options) {
|
|
|
14481
14596
|
h1: onPageResult.data.h1s,
|
|
14482
14597
|
wordCount: onPageResult.data.wordCount,
|
|
14483
14598
|
loadTime: perfData.loadTime,
|
|
14484
|
-
issues:
|
|
14599
|
+
issues: issues.map((i) => i.code)
|
|
14485
14600
|
});
|
|
14486
14601
|
console.error("\n\u2705 Audit complete!\n");
|
|
14487
|
-
return createReport(url, domain,
|
|
14602
|
+
return createReport(url, domain, issues, pages);
|
|
14488
14603
|
}
|
|
14489
14604
|
function createReport(url, domain, issues, pages, pageFailure) {
|
|
14490
14605
|
const healthScore = calculateHealthScore(issues);
|
|
@@ -14649,10 +14764,10 @@ function groupIssuesByCategory(issues) {
|
|
|
14649
14764
|
}
|
|
14650
14765
|
|
|
14651
14766
|
// src/audit/checks/duplicate-content.ts
|
|
14652
|
-
import * as
|
|
14767
|
+
import * as cheerio38 from "cheerio";
|
|
14653
14768
|
import { createHash } from "crypto";
|
|
14654
14769
|
function extractContentHash(html, url) {
|
|
14655
|
-
const $ =
|
|
14770
|
+
const $ = cheerio38.load(html);
|
|
14656
14771
|
$("script, style, nav, header, footer, aside, .nav, .header, .footer, .sidebar").remove();
|
|
14657
14772
|
const title = $("title").text().trim();
|
|
14658
14773
|
const bodyText = $("body").text().replace(/\s+/g, " ").trim();
|
|
@@ -15880,7 +15995,7 @@ var PRIORITY_WEIGHTS = {
|
|
|
15880
15995
|
};
|
|
15881
15996
|
|
|
15882
15997
|
// src/keywords/engine.ts
|
|
15883
|
-
import * as
|
|
15998
|
+
import * as cheerio40 from "cheerio";
|
|
15884
15999
|
|
|
15885
16000
|
// src/keywords/prioritizer.ts
|
|
15886
16001
|
function prioritizeKeywords(keywords, siteProfile, existingMeta) {
|
|
@@ -16174,7 +16289,7 @@ function enrichKeywordsWithEstimates(keywords) {
|
|
|
16174
16289
|
}
|
|
16175
16290
|
|
|
16176
16291
|
// src/keywords/sources/free-sources.ts
|
|
16177
|
-
import * as
|
|
16292
|
+
import * as cheerio39 from "cheerio";
|
|
16178
16293
|
var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
|
|
16179
16294
|
async function getPeopleAlsoAsk(query) {
|
|
16180
16295
|
try {
|
|
@@ -16183,7 +16298,7 @@ async function getPeopleAlsoAsk(query) {
|
|
|
16183
16298
|
headers: { "User-Agent": USER_AGENT },
|
|
16184
16299
|
timeout: 1e4
|
|
16185
16300
|
});
|
|
16186
|
-
const $ =
|
|
16301
|
+
const $ = cheerio39.load(response.data);
|
|
16187
16302
|
const questions = [];
|
|
16188
16303
|
$("[data-sgrd]").each((_, el) => {
|
|
16189
16304
|
const text = $(el).text().trim();
|
|
@@ -16209,7 +16324,7 @@ async function getRelatedSearches(query) {
|
|
|
16209
16324
|
headers: { "User-Agent": USER_AGENT },
|
|
16210
16325
|
timeout: 1e4
|
|
16211
16326
|
});
|
|
16212
|
-
const $ =
|
|
16327
|
+
const $ = cheerio39.load(response.data);
|
|
16213
16328
|
const related = [];
|
|
16214
16329
|
$("div[data-ved] a").each((_, el) => {
|
|
16215
16330
|
const href = $(el).attr("href");
|
|
@@ -16299,7 +16414,7 @@ async function analyzeCompetitorTitles(query) {
|
|
|
16299
16414
|
headers: { "User-Agent": USER_AGENT },
|
|
16300
16415
|
timeout: 1e4
|
|
16301
16416
|
});
|
|
16302
|
-
const $ =
|
|
16417
|
+
const $ = cheerio39.load(response.data);
|
|
16303
16418
|
const titles = [];
|
|
16304
16419
|
const keywords = /* @__PURE__ */ new Set();
|
|
16305
16420
|
$("h3").each((_, el) => {
|
|
@@ -16631,7 +16746,7 @@ async function fetchPageMeta(url) {
|
|
|
16631
16746
|
const response = await httpGet(url, {
|
|
16632
16747
|
timeout: 1e4
|
|
16633
16748
|
});
|
|
16634
|
-
const $ =
|
|
16749
|
+
const $ = cheerio40.load(response.data);
|
|
16635
16750
|
return {
|
|
16636
16751
|
url,
|
|
16637
16752
|
title: $("title").text().trim() || void 0,
|
|
@@ -16762,20 +16877,17 @@ async function extractSeedKeywords(url) {
|
|
|
16762
16877
|
const response = await httpGet(url, {
|
|
16763
16878
|
timeout: 1e4
|
|
16764
16879
|
});
|
|
16765
|
-
const $ =
|
|
16880
|
+
const $ = cheerio40.load(response.data);
|
|
16766
16881
|
const seeds = /* @__PURE__ */ new Set();
|
|
16767
|
-
const
|
|
16768
|
-
|
|
16769
|
-
|
|
16770
|
-
const h1 = $("h1").first().text().toLowerCase();
|
|
16771
|
-
const h1Words = h1.split(/[\s\-|:]+/).filter((w) => w.length > 3);
|
|
16772
|
-
h1Words.forEach((w) => seeds.add(w));
|
|
16882
|
+
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);
|
|
16883
|
+
words($("title").text()).forEach((w) => seeds.add(w));
|
|
16884
|
+
words($("h1").first().text()).forEach((w) => seeds.add(w));
|
|
16773
16885
|
const metaKeywords = $('meta[name="keywords"]').attr("content");
|
|
16774
16886
|
if (metaKeywords) {
|
|
16775
16887
|
metaKeywords.split(",").forEach((k) => seeds.add(k.trim().toLowerCase()));
|
|
16776
16888
|
}
|
|
16777
16889
|
const desc = $('meta[name="description"]').attr("content")?.toLowerCase() || "";
|
|
16778
|
-
const descWords = desc
|
|
16890
|
+
const descWords = words(desc).filter((w) => w.length > 5);
|
|
16779
16891
|
descWords.slice(0, 5).forEach((w) => seeds.add(w));
|
|
16780
16892
|
return Array.from(seeds).slice(0, 10);
|
|
16781
16893
|
} catch {
|
|
@@ -16828,7 +16940,7 @@ function parseJsonReply(text) {
|
|
|
16828
16940
|
}
|
|
16829
16941
|
|
|
16830
16942
|
// src/keywords/site-crawler.ts
|
|
16831
|
-
import * as
|
|
16943
|
+
import * as cheerio41 from "cheerio";
|
|
16832
16944
|
var EXCLUDED_PATHS = [
|
|
16833
16945
|
"/cdn-cgi/",
|
|
16834
16946
|
"/wp-admin/",
|
|
@@ -16940,7 +17052,7 @@ async function crawlPage(url, timeout) {
|
|
|
16940
17052
|
validateStatus: (status) => status === 200
|
|
16941
17053
|
});
|
|
16942
17054
|
const html = response.data;
|
|
16943
|
-
const $ =
|
|
17055
|
+
const $ = cheerio41.load(html);
|
|
16944
17056
|
$('script, style, noscript, iframe, nav, footer, header, aside, [role="navigation"]').remove();
|
|
16945
17057
|
const title = $("title").text().trim();
|
|
16946
17058
|
const description = $('meta[name="description"]').attr("content")?.trim() || "";
|
|
@@ -19575,7 +19687,7 @@ function getDateRange(days = 28) {
|
|
|
19575
19687
|
}
|
|
19576
19688
|
|
|
19577
19689
|
// src/keywords/sources/competitor-analysis.ts
|
|
19578
|
-
import * as
|
|
19690
|
+
import * as cheerio42 from "cheerio";
|
|
19579
19691
|
var USER_AGENT2 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
|
|
19580
19692
|
async function discoverCompetitorKeywords(yourDomain, seedKeywords, competitors) {
|
|
19581
19693
|
const yourKeywords = /* @__PURE__ */ new Set();
|
|
@@ -19672,7 +19784,7 @@ async function analyzeSERP(query) {
|
|
|
19672
19784
|
headers: { "User-Agent": USER_AGENT2 },
|
|
19673
19785
|
timeout: 1e4
|
|
19674
19786
|
});
|
|
19675
|
-
const $ =
|
|
19787
|
+
const $ = cheerio42.load(response.data);
|
|
19676
19788
|
const results = [];
|
|
19677
19789
|
const relatedSearches = [];
|
|
19678
19790
|
const peopleAlsoAsk = [];
|
|
@@ -20125,7 +20237,7 @@ function formatTopicReport(result) {
|
|
|
20125
20237
|
|
|
20126
20238
|
// src/content/readability.ts
|
|
20127
20239
|
function analyzeReadability(text) {
|
|
20128
|
-
const cleanText = text.replace(/<[^>]*>/g, " ").replace(
|
|
20240
|
+
const cleanText = text.replace(/<[^>]*>/g, " ").replace(/[ \t\r\f\v]+/g, " ").replace(/ ?\n ?/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
20129
20241
|
const words = getWords(cleanText);
|
|
20130
20242
|
const sentences = getSentences(cleanText);
|
|
20131
20243
|
const paragraphs = getParagraphs(cleanText);
|
|
@@ -20194,7 +20306,7 @@ function getSentences(text) {
|
|
|
20194
20306
|
return text.split(/[.!?]+/).filter((s) => s.trim().length > 0);
|
|
20195
20307
|
}
|
|
20196
20308
|
function getParagraphs(text) {
|
|
20197
|
-
return text.split(/\n\n+/).filter((p) => p.trim().length
|
|
20309
|
+
return text.split(/\n\n+/).filter((p) => p.trim().split(/\s+/).length >= 3);
|
|
20198
20310
|
}
|
|
20199
20311
|
function countSyllables(word) {
|
|
20200
20312
|
word = word.toLowerCase().replace(/[^a-z]/g, "");
|
|
@@ -32462,6 +32574,7 @@ export {
|
|
|
32462
32574
|
LOCATION_CODES,
|
|
32463
32575
|
OG_IMAGE_SPECS,
|
|
32464
32576
|
PRIORITY_WEIGHTS,
|
|
32577
|
+
REDUNDANT_WHEN_PRESENT,
|
|
32465
32578
|
RankTracker,
|
|
32466
32579
|
SEO_SCOPES,
|
|
32467
32580
|
SITE_PROFILE_QUESTIONS,
|
|
@@ -32545,6 +32658,7 @@ export {
|
|
|
32545
32658
|
createPullRequest,
|
|
32546
32659
|
createSEOCommit,
|
|
32547
32660
|
createSEOCommits,
|
|
32661
|
+
dedupeIssues,
|
|
32548
32662
|
describeConcerns,
|
|
32549
32663
|
detectDuplicates,
|
|
32550
32664
|
detectFramework4 as detectFramework,
|
|
@@ -32681,6 +32795,7 @@ export {
|
|
|
32681
32795
|
loadAgent,
|
|
32682
32796
|
loadAgentByName,
|
|
32683
32797
|
mergePages,
|
|
32798
|
+
metaRefreshTarget,
|
|
32684
32799
|
openAIKeywordLLM,
|
|
32685
32800
|
optimizeForAI,
|
|
32686
32801
|
optimizeReadme,
|