@rankcli/agent-runtime 0.0.37 → 0.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{analyzer-BYAFIO33.mjs → analyzer-HHRJODCF.mjs} +1 -1
- package/dist/{chunk-O7B2LBKG.mjs → chunk-ZLQQXIZ3.mjs} +20 -2
- package/dist/index.d.mts +38 -1
- package/dist/index.d.ts +38 -1
- package/dist/index.js +490 -214
- package/dist/index.mjs +438 -184
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
httpHead,
|
|
16
16
|
httpPost,
|
|
17
17
|
looksLikeHtmlFallback
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-ZLQQXIZ3.mjs";
|
|
19
19
|
import {
|
|
20
20
|
AI_CRAWLERS_INFO,
|
|
21
21
|
analyzeCitationReadiness,
|
|
@@ -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],
|
|
@@ -6459,7 +6459,9 @@ function analyzeAnchorText(html, url) {
|
|
|
6459
6459
|
} catch {
|
|
6460
6460
|
internalLinks++;
|
|
6461
6461
|
}
|
|
6462
|
-
const
|
|
6462
|
+
const imgAlt = $(el).find("img[alt]").map((_2, img) => ($(img).attr("alt") || "").trim()).get().join(" ").trim();
|
|
6463
|
+
const svgTitle = $(el).find("svg title").text().trim();
|
|
6464
|
+
const hasAccessibleText = text.length > 0 || ariaLabel || title || imgAlt.length > 0 || svgTitle.length > 0;
|
|
6463
6465
|
if (!hasAccessibleText) {
|
|
6464
6466
|
emptyAnchors++;
|
|
6465
6467
|
if (emptyAnchorUrls.length < 5) {
|
|
@@ -7715,7 +7717,7 @@ function analyzeLocalSEO(html, url) {
|
|
|
7715
7717
|
const detectedPhones = detectPhones(html);
|
|
7716
7718
|
const detectedAddresses = detectAddresses(html);
|
|
7717
7719
|
const socialProfiles = detectSocialProfiles(html);
|
|
7718
|
-
if (!hasLocalBusiness) {
|
|
7720
|
+
if (!hasLocalBusiness && (detectedPhones.length > 0 || detectedAddresses.length > 0)) {
|
|
7719
7721
|
issues.push({
|
|
7720
7722
|
...ISSUE_DEFINITIONS.LOCAL_BUSINESS_SCHEMA_MISSING,
|
|
7721
7723
|
affectedUrls: [url]
|
|
@@ -8776,7 +8778,17 @@ function analyzeKeywords(html, url) {
|
|
|
8776
8778
|
const sortPhrases = (phrases, n) => {
|
|
8777
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 }));
|
|
8778
8780
|
};
|
|
8779
|
-
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) : [];
|
|
8780
8792
|
if (potentialStuffing.length > 0) {
|
|
8781
8793
|
issues.push({
|
|
8782
8794
|
...ISSUE_DEFINITIONS.KEYWORD_STUFFING,
|
|
@@ -9002,13 +9014,6 @@ function checkAMP(html, url) {
|
|
|
9002
9014
|
}
|
|
9003
9015
|
};
|
|
9004
9016
|
}
|
|
9005
|
-
const isArticle = $("article").length > 0 || $('meta[property="og:type"][content="article"]').length > 0 || html.toLowerCase().includes("blog") || html.toLowerCase().includes("news");
|
|
9006
|
-
if (isArticle) {
|
|
9007
|
-
issues.push({
|
|
9008
|
-
...ISSUE_DEFINITIONS.AMP_MISSING,
|
|
9009
|
-
affectedUrls: [url]
|
|
9010
|
-
});
|
|
9011
|
-
}
|
|
9012
9017
|
return {
|
|
9013
9018
|
issues,
|
|
9014
9019
|
data: {
|
|
@@ -9717,13 +9722,13 @@ async function analyzeSiteMaturity(html, url) {
|
|
|
9717
9722
|
contentToAuthorityRatio
|
|
9718
9723
|
};
|
|
9719
9724
|
const { strategy, recommendations } = determineStrategy(maturityLevel, authorityIndicators, contentMetrics);
|
|
9720
|
-
if (maturityLevel === "new") {
|
|
9725
|
+
if (maturityLevel === "new" && ageSource === "wayback" && estimatedAgeDays !== null) {
|
|
9721
9726
|
issues.push({
|
|
9722
9727
|
code: "NEW_SITE_STRATEGY",
|
|
9723
9728
|
severity: "notice",
|
|
9724
9729
|
category: "content",
|
|
9725
9730
|
title: "New website detected - Authority-first strategy recommended",
|
|
9726
|
-
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.`,
|
|
9727
9732
|
impact: "Creating lots of content without authority is wasted effort. Content from new sites rarely ranks.",
|
|
9728
9733
|
howToFix: "Focus on building 10+ quality backlinks to ONE great page before expanding content. See strategic recommendations.",
|
|
9729
9734
|
affectedUrls: [url],
|
|
@@ -9734,24 +9739,7 @@ async function analyzeSiteMaturity(html, url) {
|
|
|
9734
9739
|
}
|
|
9735
9740
|
});
|
|
9736
9741
|
}
|
|
9737
|
-
if (
|
|
9738
|
-
issues.push({
|
|
9739
|
-
code: "CONTENT_AUTHORITY_IMBALANCE",
|
|
9740
|
-
severity: "warning",
|
|
9741
|
-
category: "content",
|
|
9742
|
-
title: "Content-to-authority imbalance",
|
|
9743
|
-
description: `Site has ~${indexedPages} pages but limited backlink signals. Content is unlikely to rank.`,
|
|
9744
|
-
impact: "Most of your content may be invisible to search engines due to low domain authority.",
|
|
9745
|
-
howToFix: "Stop publishing new content. Focus 100% on link building until you see ranking improvements.",
|
|
9746
|
-
affectedUrls: [url],
|
|
9747
|
-
details: {
|
|
9748
|
-
indexedPages,
|
|
9749
|
-
contentToAuthorityRatio,
|
|
9750
|
-
maturityLevel
|
|
9751
|
-
}
|
|
9752
|
-
});
|
|
9753
|
-
}
|
|
9754
|
-
if (maturityLevel === "growing" && !htmlSignals.hasBlog) {
|
|
9742
|
+
if (maturityLevel === "growing" && ageSource === "wayback" && !htmlSignals.hasBlog) {
|
|
9755
9743
|
issues.push({
|
|
9756
9744
|
code: "GROWING_SITE_NO_BLOG",
|
|
9757
9745
|
severity: "notice",
|
|
@@ -10245,8 +10233,8 @@ function analyzeRendering(html) {
|
|
|
10245
10233
|
const hasSemanticStructure = hasH1 && (hasMain || hasArticle);
|
|
10246
10234
|
const title = $("title").text().trim();
|
|
10247
10235
|
const h1Text = $("h1").first().text().trim();
|
|
10248
|
-
const
|
|
10249
|
-
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;
|
|
10250
10238
|
const EXPECTED_MIN_CONTENT = 1e3;
|
|
10251
10239
|
const htmlContentRatio = Math.min(100, mainContentCharCount / EXPECTED_MIN_CONTENT * 100);
|
|
10252
10240
|
let aiReadableScore = 50;
|
|
@@ -10466,9 +10454,9 @@ function analyzeClientRendering(html, url) {
|
|
|
10466
10454
|
howToFix: "Ensure the initial HTML contains: meaningful <title>, descriptive <h1>, and a substantial first paragraph with your key message.",
|
|
10467
10455
|
affectedUrls: [url],
|
|
10468
10456
|
details: {
|
|
10469
|
-
hasTitle: $("title").text().trim().length >
|
|
10470
|
-
hasH1: $("h1").text().trim().length >
|
|
10471
|
-
|
|
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)
|
|
10472
10460
|
}
|
|
10473
10461
|
});
|
|
10474
10462
|
}
|
|
@@ -11015,6 +11003,106 @@ function analyzeResponsiveImages(html, url) {
|
|
|
11015
11003
|
|
|
11016
11004
|
// src/audit/checks/bing-optimization.ts
|
|
11017
11005
|
import * as cheerio28 from "cheerio";
|
|
11006
|
+
|
|
11007
|
+
// src/audit/checks/site-verification.ts
|
|
11008
|
+
var UNCHECKED = {
|
|
11009
|
+
dnsChecked: false,
|
|
11010
|
+
googleDns: false,
|
|
11011
|
+
bingDns: false,
|
|
11012
|
+
bingSiteAuthFile: false
|
|
11013
|
+
};
|
|
11014
|
+
var DNS_TIMEOUT_MS = 2e3;
|
|
11015
|
+
var FILE_TIMEOUT_MS = 5e3;
|
|
11016
|
+
var CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
11017
|
+
var cache = /* @__PURE__ */ new Map();
|
|
11018
|
+
function lookupSiteVerification(url) {
|
|
11019
|
+
let parsed;
|
|
11020
|
+
try {
|
|
11021
|
+
parsed = new URL(url);
|
|
11022
|
+
} catch {
|
|
11023
|
+
return Promise.resolve(UNCHECKED);
|
|
11024
|
+
}
|
|
11025
|
+
const host = parsed.hostname.toLowerCase();
|
|
11026
|
+
const now = Date.now();
|
|
11027
|
+
const cached = cache.get(host);
|
|
11028
|
+
if (cached && cached.expires > now) return cached.result;
|
|
11029
|
+
const result = Promise.all([readTxtRecords(host), fetchBingSiteAuth(parsed.origin)]).then(
|
|
11030
|
+
([txt, bingSiteAuthFile]) => ({
|
|
11031
|
+
dnsChecked: txt.checked,
|
|
11032
|
+
googleDns: txt.records.some((record) => /^google-site-verification=/i.test(record)),
|
|
11033
|
+
bingDns: txt.records.some((record) => /^MS=/i.test(record)),
|
|
11034
|
+
bingSiteAuthFile
|
|
11035
|
+
})
|
|
11036
|
+
);
|
|
11037
|
+
cache.set(host, { expires: now + CACHE_TTL_MS, result });
|
|
11038
|
+
return result;
|
|
11039
|
+
}
|
|
11040
|
+
var resolveTxtLoader;
|
|
11041
|
+
function loadResolveTxt() {
|
|
11042
|
+
resolveTxtLoader ??= (async () => {
|
|
11043
|
+
if (typeof process === "undefined" || !process.versions?.node) return null;
|
|
11044
|
+
try {
|
|
11045
|
+
const specifier = "node:dns";
|
|
11046
|
+
const dns = await import(specifier);
|
|
11047
|
+
const resolveTxt = dns?.promises?.resolveTxt ?? dns?.default?.promises?.resolveTxt;
|
|
11048
|
+
return typeof resolveTxt === "function" ? resolveTxt : null;
|
|
11049
|
+
} catch {
|
|
11050
|
+
return null;
|
|
11051
|
+
}
|
|
11052
|
+
})();
|
|
11053
|
+
return resolveTxtLoader;
|
|
11054
|
+
}
|
|
11055
|
+
function domainCandidates(host) {
|
|
11056
|
+
if (/^[\d.]+$/.test(host) || host.includes(":")) return [];
|
|
11057
|
+
const labels = host.split(".").filter(Boolean);
|
|
11058
|
+
const names = [];
|
|
11059
|
+
for (let n = 2; n <= labels.length && names.length < 4; n++) {
|
|
11060
|
+
names.push(labels.slice(-n).join("."));
|
|
11061
|
+
}
|
|
11062
|
+
return names.length > 0 ? names : [host];
|
|
11063
|
+
}
|
|
11064
|
+
async function readTxtRecords(host) {
|
|
11065
|
+
const resolveTxt = await loadResolveTxt();
|
|
11066
|
+
if (!resolveTxt) return { checked: false, records: [] };
|
|
11067
|
+
const lookups = await Promise.all(domainCandidates(host).map((name) => resolveTxtWithin(resolveTxt, name)));
|
|
11068
|
+
return {
|
|
11069
|
+
checked: lookups.every((records) => records !== null),
|
|
11070
|
+
records: lookups.flatMap((records) => records ?? [])
|
|
11071
|
+
};
|
|
11072
|
+
}
|
|
11073
|
+
var NO_RECORD_CODES = /* @__PURE__ */ new Set(["ENODATA", "ENOTFOUND"]);
|
|
11074
|
+
async function resolveTxtWithin(resolveTxt, name) {
|
|
11075
|
+
let timer;
|
|
11076
|
+
const timeout = new Promise((resolve) => {
|
|
11077
|
+
timer = setTimeout(() => resolve(null), DNS_TIMEOUT_MS);
|
|
11078
|
+
});
|
|
11079
|
+
try {
|
|
11080
|
+
return await Promise.race([
|
|
11081
|
+
resolveTxt(name).then(
|
|
11082
|
+
(records) => records.map((chunks) => chunks.join("").trim()),
|
|
11083
|
+
(error) => NO_RECORD_CODES.has(error?.code ?? "") ? [] : null
|
|
11084
|
+
),
|
|
11085
|
+
timeout
|
|
11086
|
+
]);
|
|
11087
|
+
} catch {
|
|
11088
|
+
return null;
|
|
11089
|
+
} finally {
|
|
11090
|
+
clearTimeout(timer);
|
|
11091
|
+
}
|
|
11092
|
+
}
|
|
11093
|
+
async function fetchBingSiteAuth(origin) {
|
|
11094
|
+
try {
|
|
11095
|
+
const response = await httpGet(`${origin}/BingSiteAuth.xml`, {
|
|
11096
|
+
timeout: FILE_TIMEOUT_MS,
|
|
11097
|
+
maxBytes: 64 * 1024
|
|
11098
|
+
});
|
|
11099
|
+
return response.status === 200 && typeof response.data === "string" && !looksLikeHtmlFallback(response.data, response.headers["content-type"]) && /<users[\s>]/i.test(response.data);
|
|
11100
|
+
} catch {
|
|
11101
|
+
return false;
|
|
11102
|
+
}
|
|
11103
|
+
}
|
|
11104
|
+
|
|
11105
|
+
// src/audit/checks/bing-optimization.ts
|
|
11018
11106
|
async function analyzeBingOptimization(html, url) {
|
|
11019
11107
|
const issues = [];
|
|
11020
11108
|
const $ = cheerio28.load(html);
|
|
@@ -11023,12 +11111,9 @@ async function analyzeBingOptimization(html, url) {
|
|
|
11023
11111
|
const detectedIssues = [];
|
|
11024
11112
|
const bingMetaTag = $('meta[name="msvalidate.01"]').attr("content");
|
|
11025
11113
|
const hasBingMetaTag = !!bingMetaTag && bingMetaTag.length > 0;
|
|
11026
|
-
|
|
11027
|
-
|
|
11028
|
-
|
|
11029
|
-
hasBingSiteAuth = authResponse.status === 200 && authResponse.data.includes("<?xml") && authResponse.data.toLowerCase().includes("users");
|
|
11030
|
-
} catch {
|
|
11031
|
-
}
|
|
11114
|
+
const verification = await lookupSiteVerification(url);
|
|
11115
|
+
const hasBingSiteAuth = verification.bingSiteAuthFile;
|
|
11116
|
+
const hasBingDnsRecord = verification.bingDns;
|
|
11032
11117
|
let hasBingIndexNowKey = false;
|
|
11033
11118
|
try {
|
|
11034
11119
|
const keyPatterns = [
|
|
@@ -11076,22 +11161,27 @@ async function analyzeBingOptimization(html, url) {
|
|
|
11076
11161
|
bingSitemapAccessible = sitemapResponse.status === 200 && sitemapResponse.data.includes("<?xml");
|
|
11077
11162
|
} catch {
|
|
11078
11163
|
}
|
|
11079
|
-
if (!hasBingMetaTag && !hasBingSiteAuth) {
|
|
11164
|
+
if (!hasBingMetaTag && !hasBingSiteAuth && !hasBingDnsRecord) {
|
|
11080
11165
|
detectedIssues.push("No Bing site verification");
|
|
11081
11166
|
issues.push({
|
|
11082
11167
|
code: "BING_NOT_VERIFIED",
|
|
11083
|
-
|
|
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",
|
|
11084
11171
|
category: "ai-readiness",
|
|
11085
|
-
title: "Site not verified with Bing Webmaster Tools",
|
|
11086
|
-
description: "ChatGPT Search uses Bing's index. Verifying your site with Bing Webmaster Tools ensures proper indexing and access to Bing-specific SEO tools.",
|
|
11172
|
+
title: verification.dnsChecked ? "Site not verified with Bing Webmaster Tools" : "Bing Webmaster Tools verification not visible in the HTML",
|
|
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.",
|
|
11087
11174
|
impact: "May reduce visibility in ChatGPT Search results and miss Bing-specific optimization opportunities.",
|
|
11088
|
-
howToFix: "
|
|
11175
|
+
howToFix: "If the site is not yet in Bing Webmaster Tools: 1) Go to Bing Webmaster Tools, 2) Add your site (or import it from Google Search Console), 3) Choose a verification method (meta tag, DNS record or XML file), 4) Add the verification code to your site.",
|
|
11089
11176
|
affectedUrls: [url],
|
|
11090
11177
|
details: {
|
|
11091
11178
|
hasBingMetaTag,
|
|
11092
11179
|
hasBingSiteAuth,
|
|
11180
|
+
hasBingDnsRecord,
|
|
11181
|
+
dnsChecked: verification.dnsChecked,
|
|
11093
11182
|
verificationMethods: [
|
|
11094
11183
|
'Add <meta name="msvalidate.01" content="YOUR_CODE"> to <head>',
|
|
11184
|
+
"Add an MS=YOUR_CODE TXT record to the domain's DNS",
|
|
11095
11185
|
"Upload BingSiteAuth.xml to root directory"
|
|
11096
11186
|
]
|
|
11097
11187
|
}
|
|
@@ -11128,7 +11218,7 @@ async function analyzeBingOptimization(html, url) {
|
|
|
11128
11218
|
});
|
|
11129
11219
|
}
|
|
11130
11220
|
const hasLocalBusinessSchema = bingPlacesDetected;
|
|
11131
|
-
const hasAddressOnPage = $("address").length > 0 || html
|
|
11221
|
+
const hasAddressOnPage = $("address").text().trim().length > 0 || detectAddresses(html).length > 0;
|
|
11132
11222
|
if (hasAddressOnPage && !hasLocalBusinessSchema) {
|
|
11133
11223
|
detectedIssues.push("Local business may not be on Bing Places");
|
|
11134
11224
|
issues.push({
|
|
@@ -11151,6 +11241,8 @@ async function analyzeBingOptimization(html, url) {
|
|
|
11151
11241
|
data: {
|
|
11152
11242
|
hasBingSiteAuth,
|
|
11153
11243
|
hasBingMetaTag,
|
|
11244
|
+
hasBingDnsRecord,
|
|
11245
|
+
dnsChecked: verification.dnsChecked,
|
|
11154
11246
|
hasBingIndexNowKey,
|
|
11155
11247
|
bingPlacesDetected,
|
|
11156
11248
|
bingSitemapAccessible,
|
|
@@ -12652,23 +12744,6 @@ function analyzeColorContrast(html, url) {
|
|
|
12652
12744
|
});
|
|
12653
12745
|
}
|
|
12654
12746
|
}
|
|
12655
|
-
if (cannotDetermine > elementsAnalyzed * 0.8) {
|
|
12656
|
-
issues.push({
|
|
12657
|
-
code: "A11Y_CONTRAST_UNDETERMINED",
|
|
12658
|
-
severity: "notice",
|
|
12659
|
-
category: "accessibility",
|
|
12660
|
-
title: "Color contrast could not be fully analyzed",
|
|
12661
|
-
description: "Most text colors are set via CSS stylesheets rather than inline styles. Full contrast checking requires browser rendering.",
|
|
12662
|
-
impact: "Potential contrast issues may exist that were not detected.",
|
|
12663
|
-
howToFix: "Use browser DevTools (Lighthouse accessibility audit) or dedicated tools like axe-core for comprehensive contrast checking.",
|
|
12664
|
-
affectedUrls: [url],
|
|
12665
|
-
details: {
|
|
12666
|
-
elementsAnalyzed,
|
|
12667
|
-
cannotDetermine,
|
|
12668
|
-
recommendation: "Run Lighthouse accessibility audit for complete contrast analysis."
|
|
12669
|
-
}
|
|
12670
|
-
});
|
|
12671
|
-
}
|
|
12672
12747
|
return {
|
|
12673
12748
|
issues,
|
|
12674
12749
|
data: {
|
|
@@ -13129,6 +13204,10 @@ function analyzePageResources(html, url) {
|
|
|
13129
13204
|
|
|
13130
13205
|
// src/audit/checks/responsive-css.ts
|
|
13131
13206
|
import * as cheerio35 from "cheerio";
|
|
13207
|
+
var MAX_STYLESHEETS = 3;
|
|
13208
|
+
var MAX_STYLESHEET_BYTES = 1024 * 1024;
|
|
13209
|
+
var STYLESHEET_TIMEOUT_MS = 3e3;
|
|
13210
|
+
var RESPONSIVE_SIGNAL = /@media\b|@container\b|\bclamp\(|(?<![\w-])(?:min|max)\(/i;
|
|
13132
13211
|
function extractMediaQueries(css) {
|
|
13133
13212
|
const mediaQueries = [];
|
|
13134
13213
|
const regex = /@media\s*([^{]+)/g;
|
|
@@ -13138,36 +13217,31 @@ function extractMediaQueries(css) {
|
|
|
13138
13217
|
}
|
|
13139
13218
|
return mediaQueries;
|
|
13140
13219
|
}
|
|
13220
|
+
var WIDTH_RULE = /(?:(?:min|max)-width\s*:|width\s*(?:<=|>=|<|>))\s*(\d+(?:\.\d+)?)\s*(px|em|rem)?|(\d+(?:\.\d+)?)\s*(px|em|rem)?\s*(?:<=|>=|<|>)\s*width/gi;
|
|
13141
13221
|
function classifyBreakpoints(mediaQueries) {
|
|
13142
13222
|
const breakpoints = /* @__PURE__ */ new Set();
|
|
13143
13223
|
let hasMobile = false;
|
|
13144
13224
|
let hasTablet = false;
|
|
13145
13225
|
let hasDesktop = false;
|
|
13146
13226
|
for (const query of mediaQueries) {
|
|
13147
|
-
const widthMatches = query.match(/(?:min|max)-width\s*:\s*(\d+)(?:px|em|rem)?/gi);
|
|
13148
|
-
if (widthMatches) {
|
|
13149
|
-
for (const match of widthMatches) {
|
|
13150
|
-
const valueMatch = match.match(/(\d+)/);
|
|
13151
|
-
if (valueMatch) {
|
|
13152
|
-
const value = parseInt(valueMatch[1], 10);
|
|
13153
|
-
breakpoints.add(`${value}px`);
|
|
13154
|
-
if (value <= 480) {
|
|
13155
|
-
hasMobile = true;
|
|
13156
|
-
} else if (value <= 768) {
|
|
13157
|
-
hasMobile = true;
|
|
13158
|
-
} else if (value <= 1024) {
|
|
13159
|
-
hasTablet = true;
|
|
13160
|
-
} else {
|
|
13161
|
-
hasDesktop = true;
|
|
13162
|
-
}
|
|
13163
|
-
}
|
|
13164
|
-
}
|
|
13165
|
-
}
|
|
13166
|
-
if (/screen\s+and/i.test(query)) {
|
|
13167
|
-
}
|
|
13168
13227
|
if (/print/i.test(query)) {
|
|
13169
13228
|
continue;
|
|
13170
13229
|
}
|
|
13230
|
+
for (const match of query.matchAll(WIDTH_RULE)) {
|
|
13231
|
+
const raw = match[1] ?? match[3];
|
|
13232
|
+
const unit = (match[2] ?? match[4] ?? "px").toLowerCase();
|
|
13233
|
+
const value = Math.round(parseFloat(raw) * (unit === "px" ? 1 : 16));
|
|
13234
|
+
breakpoints.add(`${value}px`);
|
|
13235
|
+
if (value <= 480) {
|
|
13236
|
+
hasMobile = true;
|
|
13237
|
+
} else if (value <= 768) {
|
|
13238
|
+
hasMobile = true;
|
|
13239
|
+
} else if (value <= 1024) {
|
|
13240
|
+
hasTablet = true;
|
|
13241
|
+
} else {
|
|
13242
|
+
hasDesktop = true;
|
|
13243
|
+
}
|
|
13244
|
+
}
|
|
13171
13245
|
}
|
|
13172
13246
|
return {
|
|
13173
13247
|
hasMobile,
|
|
@@ -13185,6 +13259,7 @@ async function analyzeResponsiveCss(html, url) {
|
|
|
13185
13259
|
const allMediaQueries = [];
|
|
13186
13260
|
const externalSources = [];
|
|
13187
13261
|
let inlineCount = 0;
|
|
13262
|
+
let hasResponsiveSignals = false;
|
|
13188
13263
|
$("style").each((_, el) => {
|
|
13189
13264
|
const css = $(el).text();
|
|
13190
13265
|
const queries = extractMediaQueries(css);
|
|
@@ -13192,39 +13267,43 @@ async function analyzeResponsiveCss(html, url) {
|
|
|
13192
13267
|
allMediaQueries.push(...queries);
|
|
13193
13268
|
inlineCount += queries.length;
|
|
13194
13269
|
}
|
|
13270
|
+
if (RESPONSIVE_SIGNAL.test(css)) hasResponsiveSignals = true;
|
|
13195
13271
|
});
|
|
13196
|
-
const
|
|
13197
|
-
|
|
13272
|
+
const sameOrigin = [];
|
|
13273
|
+
let crossOrigin = 0;
|
|
13274
|
+
$("link[rel][href]").each((_, el) => {
|
|
13275
|
+
if (!/\bstylesheet\b/i.test($(el).attr("rel") ?? "")) return;
|
|
13276
|
+
const href = $(el).attr("href");
|
|
13277
|
+
if (!href || href.startsWith("data:")) return;
|
|
13198
13278
|
try {
|
|
13199
|
-
const
|
|
13200
|
-
|
|
13279
|
+
const resolved = new URL(href, url);
|
|
13280
|
+
if (resolved.hostname === baseUrl.hostname) sameOrigin.push(resolved.href);
|
|
13281
|
+
else crossOrigin++;
|
|
13201
13282
|
} catch {
|
|
13202
|
-
|
|
13283
|
+
crossOrigin++;
|
|
13203
13284
|
}
|
|
13204
13285
|
});
|
|
13205
|
-
|
|
13206
|
-
|
|
13207
|
-
|
|
13208
|
-
|
|
13209
|
-
|
|
13210
|
-
|
|
13211
|
-
|
|
13212
|
-
|
|
13213
|
-
|
|
13286
|
+
let fetchFailed = 0;
|
|
13287
|
+
await Promise.all(
|
|
13288
|
+
sameOrigin.slice(0, MAX_STYLESHEETS).map(async (cssUrl) => {
|
|
13289
|
+
try {
|
|
13290
|
+
const response = await httpGet(cssUrl, {
|
|
13291
|
+
timeout: STYLESHEET_TIMEOUT_MS,
|
|
13292
|
+
maxBytes: MAX_STYLESHEET_BYTES
|
|
13293
|
+
});
|
|
13294
|
+
if (typeof response.data !== "string") throw new Error("not text");
|
|
13214
13295
|
const queries = extractMediaQueries(response.data);
|
|
13215
|
-
if (queries.length > 0)
|
|
13216
|
-
|
|
13296
|
+
if (queries.length > 0) allMediaQueries.push(...queries);
|
|
13297
|
+
if (RESPONSIVE_SIGNAL.test(response.data)) {
|
|
13298
|
+
hasResponsiveSignals = true;
|
|
13217
13299
|
externalSources.push(cssUrl);
|
|
13218
13300
|
}
|
|
13301
|
+
} catch {
|
|
13302
|
+
fetchFailed++;
|
|
13219
13303
|
}
|
|
13220
|
-
}
|
|
13221
|
-
|
|
13222
|
-
|
|
13223
|
-
await Promise.race([
|
|
13224
|
-
Promise.all(cssPromises),
|
|
13225
|
-
new Promise((resolve) => setTimeout(resolve, 6e3))
|
|
13226
|
-
// 6s overall timeout
|
|
13227
|
-
]);
|
|
13304
|
+
})
|
|
13305
|
+
);
|
|
13306
|
+
const stylesheetsCovered = crossOrigin === 0 && fetchFailed === 0 && sameOrigin.length <= MAX_STYLESHEETS;
|
|
13228
13307
|
const hasMediaQueries = allMediaQueries.length > 0;
|
|
13229
13308
|
const { hasMobile, hasTablet, hasDesktop, breakpoints } = classifyBreakpoints(allMediaQueries);
|
|
13230
13309
|
if (!hasViewport) {
|
|
@@ -13239,21 +13318,22 @@ async function analyzeResponsiveCss(html, url) {
|
|
|
13239
13318
|
affectedUrls: [url]
|
|
13240
13319
|
});
|
|
13241
13320
|
}
|
|
13242
|
-
if (!
|
|
13321
|
+
if (!hasResponsiveSignals && stylesheetsCovered) {
|
|
13243
13322
|
issues.push({
|
|
13244
13323
|
code: "RESPONSIVE_NO_MEDIA_QUERIES",
|
|
13245
13324
|
severity: "warning",
|
|
13246
13325
|
category: "mobile",
|
|
13247
13326
|
title: "No CSS media queries detected",
|
|
13248
|
-
description: "No @media queries found in inline styles or first-party stylesheets.",
|
|
13327
|
+
description: "No @media or @container queries and no fluid sizing (clamp, min, max) found in inline styles or first-party stylesheets.",
|
|
13249
13328
|
impact: "Without media queries, the site likely uses fixed layouts that do not adapt to different screen sizes. This hurts mobile usability and SEO.",
|
|
13250
13329
|
howToFix: "Add CSS media queries to adapt layout for different screen sizes. Consider using a mobile-first approach with min-width breakpoints.",
|
|
13251
13330
|
affectedUrls: [url],
|
|
13252
13331
|
details: {
|
|
13253
|
-
suggestion: "Common breakpoints: 480px (mobile), 768px (tablet), 1024px (desktop)"
|
|
13332
|
+
suggestion: "Common breakpoints: 480px (mobile), 768px (tablet), 1024px (desktop)",
|
|
13333
|
+
stylesheetsScanned: sameOrigin.length
|
|
13254
13334
|
}
|
|
13255
13335
|
});
|
|
13256
|
-
} else if (!hasMobile && !hasTablet) {
|
|
13336
|
+
} else if (hasMediaQueries && !hasMobile && !hasTablet) {
|
|
13257
13337
|
issues.push({
|
|
13258
13338
|
code: "RESPONSIVE_NO_MOBILE_BREAKPOINT",
|
|
13259
13339
|
severity: "warning",
|
|
@@ -13279,6 +13359,8 @@ async function analyzeResponsiveCss(html, url) {
|
|
|
13279
13359
|
inline: inlineCount,
|
|
13280
13360
|
external: externalSources
|
|
13281
13361
|
},
|
|
13362
|
+
hasResponsiveSignals,
|
|
13363
|
+
stylesheetsCovered,
|
|
13282
13364
|
breakpoints,
|
|
13283
13365
|
hasMobileBreakpoint: hasMobile,
|
|
13284
13366
|
hasTabletBreakpoint: hasTablet,
|
|
@@ -13916,7 +13998,22 @@ var urlSafetyDatabase = {
|
|
|
13916
13998
|
|
|
13917
13999
|
// src/audit/checks/tracking-verification.ts
|
|
13918
14000
|
import * as cheerio36 from "cheerio";
|
|
13919
|
-
|
|
14001
|
+
var OTHER_ANALYTICS = [
|
|
14002
|
+
{ name: "Cloudflare Web Analytics", pattern: /cloudflareinsights\.com\/beacon(?:\.min)?\.js/i },
|
|
14003
|
+
{ name: "Plausible", pattern: /plausible\.io\/js\/|<script[^>]*data-domain=[^>]*plausible|<script[^>]*plausible[^>]*data-domain=/i },
|
|
14004
|
+
{ name: "Fathom", pattern: /cdn\.usefathom\.com/i },
|
|
14005
|
+
// data-website-id is Umami's attribute, on the hosted script and on self-hosted ones alike.
|
|
14006
|
+
{ name: "Umami", pattern: /<script[^>]*\bdata-website-id=/i },
|
|
14007
|
+
{ name: "PostHog", pattern: /posthog\.init\s*\(|\.posthog\.com\//i },
|
|
14008
|
+
{ name: "Matomo", pattern: /matomo\.js|piwik\.js|_paq\s*(?:=|\.push)/i },
|
|
14009
|
+
{ name: "Simple Analytics", pattern: /scripts\.simpleanalyticscdn\.com/i },
|
|
14010
|
+
{ name: "Mixpanel", pattern: /cdn\.mxpnl\.com|mixpanel\.init\s*\(/i },
|
|
14011
|
+
{ name: "Segment", pattern: /cdn\.segment\.com/i }
|
|
14012
|
+
];
|
|
14013
|
+
function detectOtherAnalytics(html) {
|
|
14014
|
+
return OTHER_ANALYTICS.filter(({ pattern }) => pattern.test(html)).map(({ name }) => name);
|
|
14015
|
+
}
|
|
14016
|
+
async function analyzeTrackingVerification(html, url) {
|
|
13920
14017
|
const issues = [];
|
|
13921
14018
|
const $ = cheerio36.load(html);
|
|
13922
14019
|
const ga4Match = html.match(/gtag\s*\(\s*['"]config['"]\s*,\s*['"](G-[A-Z0-9]+)['"]/i) || html.match(/googletagmanager\.com\/gtag\/js\?id=(G-[A-Z0-9]+)/i);
|
|
@@ -13926,10 +14023,16 @@ function analyzeTrackingVerification(html, url) {
|
|
|
13926
14023
|
const hasGtm = !!gtmMatch;
|
|
13927
14024
|
const gtmContainerId = gtmMatch ? gtmMatch[1] : void 0;
|
|
13928
14025
|
const hasGtmNoscript = /googletagmanager\.com\/ns\.html\?id=GTM-[A-Z0-9]+/i.test(html);
|
|
14026
|
+
const otherAnalytics = detectOtherAnalytics(html);
|
|
14027
|
+
const verification = await lookupSiteVerification(url);
|
|
13929
14028
|
const gscMeta = $('meta[name="google-site-verification"]').attr("content");
|
|
13930
|
-
const
|
|
14029
|
+
const hasGscMeta = !!(gscMeta && gscMeta.length > 10);
|
|
14030
|
+
const hasGscVerification = hasGscMeta || verification.googleDns;
|
|
14031
|
+
const gscSource = hasGscMeta ? "meta" : verification.googleDns ? "dns" : void 0;
|
|
13931
14032
|
const bingMeta = $('meta[name="msvalidate.01"]').attr("content");
|
|
13932
|
-
const
|
|
14033
|
+
const hasBingMeta = !!(bingMeta && bingMeta.length > 10);
|
|
14034
|
+
const hasBingVerification = hasBingMeta || verification.bingDns || verification.bingSiteAuthFile;
|
|
14035
|
+
const bingSource = hasBingMeta ? "meta" : verification.bingDns ? "dns" : verification.bingSiteAuthFile ? "file" : void 0;
|
|
13933
14036
|
const jsonLdScripts = $('script[type="application/ld+json"]');
|
|
13934
14037
|
const hasMicrodata = $('[itemtype*="schema.org"]').length > 0;
|
|
13935
14038
|
const hasJsonLd = jsonLdScripts.length > 0;
|
|
@@ -13951,16 +14054,19 @@ function analyzeTrackingVerification(html, url) {
|
|
|
13951
14054
|
const data = {
|
|
13952
14055
|
ga4: { detected: hasGa4, measurementId: ga4MeasurementId },
|
|
13953
14056
|
gtm: { detected: hasGtm, containerId: gtmContainerId, hasNoscript: hasGtmNoscript },
|
|
13954
|
-
|
|
13955
|
-
|
|
14057
|
+
otherAnalytics,
|
|
14058
|
+
gsc: { verified: hasGscVerification, verificationCode: gscMeta, source: gscSource },
|
|
14059
|
+
bing: { verified: hasBingVerification, verificationCode: bingMeta, source: bingSource },
|
|
14060
|
+
dnsChecked: verification.dnsChecked,
|
|
13956
14061
|
schemaOrg: { detected: hasJsonLd || hasMicrodata, types: schemaTypes, hasValidJson },
|
|
13957
14062
|
sitemap: { inRobotsTxt: false }
|
|
13958
14063
|
// Set later if we check robots.txt
|
|
13959
14064
|
};
|
|
13960
|
-
if (!hasGa4 && !hasGtm) {
|
|
14065
|
+
if (!hasGa4 && !hasGtm && otherAnalytics.length === 0) {
|
|
13961
14066
|
issues.push({
|
|
13962
14067
|
code: "GA4_MISSING",
|
|
13963
|
-
|
|
14068
|
+
// A notice: which analytics a site runs is a choice, not an SEO defect.
|
|
14069
|
+
severity: "notice",
|
|
13964
14070
|
category: "on-page",
|
|
13965
14071
|
title: "Google Analytics 4 not detected",
|
|
13966
14072
|
description: "No GA4 tracking code found. GA4 is essential for measuring organic traffic and conversions.",
|
|
@@ -13970,40 +14076,47 @@ function analyzeTrackingVerification(html, url) {
|
|
|
13970
14076
|
});
|
|
13971
14077
|
}
|
|
13972
14078
|
if (!hasGscVerification) {
|
|
14079
|
+
const dnsChecked = verification.dnsChecked;
|
|
13973
14080
|
issues.push({
|
|
13974
14081
|
code: "GSC_VERIFICATION_MISSING",
|
|
13975
|
-
|
|
14082
|
+
// A notice: a site verified by DNS, Google Analytics or Tag Manager leaves no trace here.
|
|
14083
|
+
severity: "notice",
|
|
13976
14084
|
category: "on-page",
|
|
13977
|
-
title: "Google Search Console verification not found",
|
|
13978
|
-
description: "No GSC verification meta tag
|
|
14085
|
+
title: dnsChecked ? "Google Search Console verification not found" : "Google Search Console verification not visible in the HTML",
|
|
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.",
|
|
13979
14087
|
impact: "Missing critical insights into Google search visibility, impressions, and clicks.",
|
|
13980
|
-
howToFix: "
|
|
13981
|
-
affectedUrls: [url]
|
|
14088
|
+
howToFix: dnsChecked ? "Verify the site in Google Search Console: add the meta tag to <head>, or a TXT record to the domain's DNS. Get it from search.google.com/search-console." : "If the site is not yet verified in Google Search Console, add the meta tag to <head> or a TXT record to the domain's DNS. Get it from search.google.com/search-console.",
|
|
14089
|
+
affectedUrls: [url],
|
|
14090
|
+
details: { dnsChecked }
|
|
13982
14091
|
});
|
|
13983
14092
|
}
|
|
13984
14093
|
if (!hasBingVerification) {
|
|
14094
|
+
const dnsChecked = verification.dnsChecked;
|
|
13985
14095
|
issues.push({
|
|
13986
14096
|
code: "BING_VERIFICATION_MISSING",
|
|
13987
14097
|
severity: "notice",
|
|
13988
14098
|
category: "on-page",
|
|
13989
|
-
title: "Bing Webmaster Tools verification not found",
|
|
13990
|
-
description: "No Bing
|
|
14099
|
+
title: dnsChecked ? "Bing Webmaster Tools verification not found" : "Bing Webmaster Tools verification not visible in the HTML",
|
|
14100
|
+
description: dnsChecked ? "No msvalidate.01 meta tag in the HTML, no MS= TXT record on the domain and no /BingSiteAuth.xml file. Bing/Yahoo account for 5-8% of search traffic." : "No msvalidate.01 meta tag in the HTML and no /BingSiteAuth.xml file. The site may be verified through a DNS record instead, which this run could not check. Bing/Yahoo account for 5-8% of search traffic.",
|
|
13991
14101
|
impact: "Missing insights on Bing/Yahoo search traffic.",
|
|
13992
|
-
howToFix: "
|
|
13993
|
-
affectedUrls: [url]
|
|
14102
|
+
howToFix: "If the site is not yet in Bing Webmaster Tools, add it there: import it from Google Search Console, or verify with the meta tag, a DNS record or BingSiteAuth.xml. Get it from bing.com/webmasters.",
|
|
14103
|
+
affectedUrls: [url],
|
|
14104
|
+
details: { dnsChecked }
|
|
13994
14105
|
});
|
|
13995
14106
|
}
|
|
13996
14107
|
if (!hasGtm) {
|
|
13997
|
-
|
|
13998
|
-
|
|
13999
|
-
|
|
14000
|
-
|
|
14001
|
-
|
|
14002
|
-
|
|
14003
|
-
|
|
14004
|
-
|
|
14005
|
-
|
|
14006
|
-
|
|
14108
|
+
if (otherAnalytics.length === 0) {
|
|
14109
|
+
issues.push({
|
|
14110
|
+
code: "GTM_MISSING",
|
|
14111
|
+
severity: "notice",
|
|
14112
|
+
category: "on-page",
|
|
14113
|
+
title: "Google Tag Manager not detected",
|
|
14114
|
+
description: "GTM not found. While not required, GTM makes it easier to manage tracking tags without code changes.",
|
|
14115
|
+
impact: "Tag management requires developer involvement for each change.",
|
|
14116
|
+
howToFix: "Consider using GTM for easier tag management. It allows marketing to add tags without developer help.",
|
|
14117
|
+
affectedUrls: [url]
|
|
14118
|
+
});
|
|
14119
|
+
}
|
|
14007
14120
|
} else if (!hasGtmNoscript) {
|
|
14008
14121
|
issues.push({
|
|
14009
14122
|
code: "GTM_NOSCRIPT_MISSING",
|
|
@@ -14042,6 +14155,113 @@ function analyzeTrackingVerification(html, url) {
|
|
|
14042
14155
|
return { issues, data };
|
|
14043
14156
|
}
|
|
14044
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
|
+
|
|
14045
14265
|
// src/audit/engine.ts
|
|
14046
14266
|
function sameUrl(a, b) {
|
|
14047
14267
|
try {
|
|
@@ -14054,15 +14274,15 @@ function sameUrl(a, b) {
|
|
|
14054
14274
|
}
|
|
14055
14275
|
}
|
|
14056
14276
|
async function runFullAudit(options) {
|
|
14057
|
-
const {
|
|
14277
|
+
const { checkBrokenLinks = false, checkHreflangUrls = false, checkCanonicalChain = false, cruxApiKey, checksLimit = 280, premiumChecks } = options;
|
|
14278
|
+
let url = options.url;
|
|
14058
14279
|
const auditRoot = isSiteRoot(url) || sameUrl(url, options.auditRootUrl ?? url);
|
|
14059
14280
|
const allIssues = [];
|
|
14060
14281
|
const pages = [];
|
|
14061
14282
|
const tier = checksLimit <= 50 ? "free" : checksLimit <= 100 ? "registered" : "paid";
|
|
14062
14283
|
const runPremiumChecks = tier === "paid" && !!premiumChecks;
|
|
14063
14284
|
const runExtendedChecks = tier !== "free";
|
|
14064
|
-
|
|
14065
|
-
const domain = parsedUrl.hostname;
|
|
14285
|
+
let domain = new URL(url).hostname;
|
|
14066
14286
|
console.error(`
|
|
14067
14287
|
\u{1F50D} Running comprehensive SEO audit on ${url}...
|
|
14068
14288
|
`);
|
|
@@ -14089,7 +14309,9 @@ async function runFullAudit(options) {
|
|
|
14089
14309
|
timeout: 3e4,
|
|
14090
14310
|
validateStatus: () => true
|
|
14091
14311
|
}).catch((err) => {
|
|
14092
|
-
|
|
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}`);
|
|
14093
14315
|
return { error: err, data: "", headers: {} };
|
|
14094
14316
|
})
|
|
14095
14317
|
]);
|
|
@@ -14112,7 +14334,7 @@ async function runFullAudit(options) {
|
|
|
14112
14334
|
});
|
|
14113
14335
|
return createReport(url, domain, allIssues, pages, { kind: "unreachable" });
|
|
14114
14336
|
}
|
|
14115
|
-
|
|
14337
|
+
let statusCode = fetchResult.status;
|
|
14116
14338
|
if (statusCode >= 400) {
|
|
14117
14339
|
const blocked = statusCode === 401 || statusCode === 403 || statusCode === 429;
|
|
14118
14340
|
allIssues.push({
|
|
@@ -14125,10 +14347,42 @@ async function runFullAudit(options) {
|
|
|
14125
14347
|
pages.push({ url, statusCode, wordCount: 0, loadTime: 0, issues: allIssues.map((i) => i.code) });
|
|
14126
14348
|
return createReport(url, domain, allIssues, pages, { kind: "http-error", statusCode });
|
|
14127
14349
|
}
|
|
14128
|
-
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
|
+
}
|
|
14129
14384
|
const html = body.replace(/<\/?noscript[^>]*>/gi, "");
|
|
14130
|
-
|
|
14131
|
-
console.error(`\u{1F4DD} Phase 2: Running synchronous HTML checks (tier: ${tier}, limit: ${checksLimit})...`);
|
|
14385
|
+
console.error("\u{1F4DD} Phase 2: Running HTML checks...");
|
|
14132
14386
|
const onPageResult = analyzeOnPage(html, url, headers, { auditRoot });
|
|
14133
14387
|
const structuredDataResult = analyzeStructuredData2(html, url);
|
|
14134
14388
|
const mobileResult = analyzeMobile(html, url);
|
|
@@ -14143,7 +14397,6 @@ async function runFullAudit(options) {
|
|
|
14143
14397
|
const urlSafetyResult = analyzeUrlSafety(url);
|
|
14144
14398
|
const domSizeResult = analyzeDomSize(html, url);
|
|
14145
14399
|
const imageDimensionsResult = analyzeImageDimensions(html, url);
|
|
14146
|
-
const trackingResult = analyzeTrackingVerification(html, url);
|
|
14147
14400
|
const anchorResult = runExtendedChecks ? analyzeAnchorText(html, url) : { issues: [], data: {} };
|
|
14148
14401
|
const localSeoResult = runExtendedChecks ? analyzeLocalSEO(html, url) : { issues: [], data: {} };
|
|
14149
14402
|
const modernImageResult = runExtendedChecks ? analyzeModernImages(html, url) : { issues: [], data: {} };
|
|
@@ -14218,8 +14471,7 @@ async function runFullAudit(options) {
|
|
|
14218
14471
|
...colorContrastResult.issues,
|
|
14219
14472
|
...pageResourcesResult.issues,
|
|
14220
14473
|
...urlSafetyResult.issues,
|
|
14221
|
-
...paginationResult.issues
|
|
14222
|
-
...trackingResult.issues
|
|
14474
|
+
...paginationResult.issues
|
|
14223
14475
|
);
|
|
14224
14476
|
if (!structuredDataResult.data.hasSchema) {
|
|
14225
14477
|
const suggestions = suggestSchemaTypes(html, url);
|
|
@@ -14265,6 +14517,7 @@ async function runFullAudit(options) {
|
|
|
14265
14517
|
asyncChecks.push(safeAsyncIssues("Certificate", async () => (await checkCertificate(url)).issues));
|
|
14266
14518
|
asyncChecks.push(safeAsync("Redirects", () => analyzeRedirects(url)));
|
|
14267
14519
|
asyncChecks.push(safeAsync("Robots Validation", () => validateRobotsTxt(url)));
|
|
14520
|
+
asyncChecks.push(safeAsync("Tracking Verification", () => analyzeTrackingVerification(html, url)));
|
|
14268
14521
|
asyncChecks.push(safeAsyncIssues("Crawler Access", async () => (await analyzeCrawlerAccess(url)).issues));
|
|
14269
14522
|
asyncChecks.push(safeAsync("Canonical Domain", () => analyzeCanonicalDomain(url)));
|
|
14270
14523
|
asyncChecks.push(safeAsyncIssues("Framework Detection", async () => {
|
|
@@ -14296,10 +14549,10 @@ async function runFullAudit(options) {
|
|
|
14296
14549
|
if (cruxApiKey) {
|
|
14297
14550
|
asyncChecks.push(safeAsyncIssues("Core Web Vitals", async () => {
|
|
14298
14551
|
const cwv = await fetchCoreWebVitals(url, cruxApiKey);
|
|
14299
|
-
const
|
|
14552
|
+
const issues2 = [];
|
|
14300
14553
|
if (cwv) {
|
|
14301
14554
|
if (cwv.lcp?.rating === "poor") {
|
|
14302
|
-
|
|
14555
|
+
issues2.push({
|
|
14303
14556
|
code: "LCP_POOR",
|
|
14304
14557
|
severity: "error",
|
|
14305
14558
|
category: "performance",
|
|
@@ -14312,7 +14565,7 @@ async function runFullAudit(options) {
|
|
|
14312
14565
|
});
|
|
14313
14566
|
}
|
|
14314
14567
|
if (cwv.cls?.rating === "poor") {
|
|
14315
|
-
|
|
14568
|
+
issues2.push({
|
|
14316
14569
|
code: "CLS_POOR",
|
|
14317
14570
|
severity: "error",
|
|
14318
14571
|
category: "performance",
|
|
@@ -14325,14 +14578,15 @@ async function runFullAudit(options) {
|
|
|
14325
14578
|
});
|
|
14326
14579
|
}
|
|
14327
14580
|
}
|
|
14328
|
-
return
|
|
14581
|
+
return issues2;
|
|
14329
14582
|
}));
|
|
14330
14583
|
}
|
|
14331
14584
|
}
|
|
14332
14585
|
const asyncResults = await Promise.all(asyncChecks);
|
|
14333
|
-
for (const
|
|
14334
|
-
allIssues.push(...
|
|
14586
|
+
for (const issues2 of asyncResults) {
|
|
14587
|
+
allIssues.push(...issues2);
|
|
14335
14588
|
}
|
|
14589
|
+
const issues = dedupeIssues(allIssues);
|
|
14336
14590
|
pages.push({
|
|
14337
14591
|
url,
|
|
14338
14592
|
statusCode,
|
|
@@ -14342,10 +14596,10 @@ async function runFullAudit(options) {
|
|
|
14342
14596
|
h1: onPageResult.data.h1s,
|
|
14343
14597
|
wordCount: onPageResult.data.wordCount,
|
|
14344
14598
|
loadTime: perfData.loadTime,
|
|
14345
|
-
issues:
|
|
14599
|
+
issues: issues.map((i) => i.code)
|
|
14346
14600
|
});
|
|
14347
14601
|
console.error("\n\u2705 Audit complete!\n");
|
|
14348
|
-
return createReport(url, domain,
|
|
14602
|
+
return createReport(url, domain, issues, pages);
|
|
14349
14603
|
}
|
|
14350
14604
|
function createReport(url, domain, issues, pages, pageFailure) {
|
|
14351
14605
|
const healthScore = calculateHealthScore(issues);
|
|
@@ -14510,10 +14764,10 @@ function groupIssuesByCategory(issues) {
|
|
|
14510
14764
|
}
|
|
14511
14765
|
|
|
14512
14766
|
// src/audit/checks/duplicate-content.ts
|
|
14513
|
-
import * as
|
|
14767
|
+
import * as cheerio38 from "cheerio";
|
|
14514
14768
|
import { createHash } from "crypto";
|
|
14515
14769
|
function extractContentHash(html, url) {
|
|
14516
|
-
const $ =
|
|
14770
|
+
const $ = cheerio38.load(html);
|
|
14517
14771
|
$("script, style, nav, header, footer, aside, .nav, .header, .footer, .sidebar").remove();
|
|
14518
14772
|
const title = $("title").text().trim();
|
|
14519
14773
|
const bodyText = $("body").text().replace(/\s+/g, " ").trim();
|
|
@@ -15741,7 +15995,7 @@ var PRIORITY_WEIGHTS = {
|
|
|
15741
15995
|
};
|
|
15742
15996
|
|
|
15743
15997
|
// src/keywords/engine.ts
|
|
15744
|
-
import * as
|
|
15998
|
+
import * as cheerio40 from "cheerio";
|
|
15745
15999
|
|
|
15746
16000
|
// src/keywords/prioritizer.ts
|
|
15747
16001
|
function prioritizeKeywords(keywords, siteProfile, existingMeta) {
|
|
@@ -16035,7 +16289,7 @@ function enrichKeywordsWithEstimates(keywords) {
|
|
|
16035
16289
|
}
|
|
16036
16290
|
|
|
16037
16291
|
// src/keywords/sources/free-sources.ts
|
|
16038
|
-
import * as
|
|
16292
|
+
import * as cheerio39 from "cheerio";
|
|
16039
16293
|
var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
|
|
16040
16294
|
async function getPeopleAlsoAsk(query) {
|
|
16041
16295
|
try {
|
|
@@ -16044,7 +16298,7 @@ async function getPeopleAlsoAsk(query) {
|
|
|
16044
16298
|
headers: { "User-Agent": USER_AGENT },
|
|
16045
16299
|
timeout: 1e4
|
|
16046
16300
|
});
|
|
16047
|
-
const $ =
|
|
16301
|
+
const $ = cheerio39.load(response.data);
|
|
16048
16302
|
const questions = [];
|
|
16049
16303
|
$("[data-sgrd]").each((_, el) => {
|
|
16050
16304
|
const text = $(el).text().trim();
|
|
@@ -16070,7 +16324,7 @@ async function getRelatedSearches(query) {
|
|
|
16070
16324
|
headers: { "User-Agent": USER_AGENT },
|
|
16071
16325
|
timeout: 1e4
|
|
16072
16326
|
});
|
|
16073
|
-
const $ =
|
|
16327
|
+
const $ = cheerio39.load(response.data);
|
|
16074
16328
|
const related = [];
|
|
16075
16329
|
$("div[data-ved] a").each((_, el) => {
|
|
16076
16330
|
const href = $(el).attr("href");
|
|
@@ -16160,7 +16414,7 @@ async function analyzeCompetitorTitles(query) {
|
|
|
16160
16414
|
headers: { "User-Agent": USER_AGENT },
|
|
16161
16415
|
timeout: 1e4
|
|
16162
16416
|
});
|
|
16163
|
-
const $ =
|
|
16417
|
+
const $ = cheerio39.load(response.data);
|
|
16164
16418
|
const titles = [];
|
|
16165
16419
|
const keywords = /* @__PURE__ */ new Set();
|
|
16166
16420
|
$("h3").each((_, el) => {
|
|
@@ -16492,7 +16746,7 @@ async function fetchPageMeta(url) {
|
|
|
16492
16746
|
const response = await httpGet(url, {
|
|
16493
16747
|
timeout: 1e4
|
|
16494
16748
|
});
|
|
16495
|
-
const $ =
|
|
16749
|
+
const $ = cheerio40.load(response.data);
|
|
16496
16750
|
return {
|
|
16497
16751
|
url,
|
|
16498
16752
|
title: $("title").text().trim() || void 0,
|
|
@@ -16623,20 +16877,17 @@ async function extractSeedKeywords(url) {
|
|
|
16623
16877
|
const response = await httpGet(url, {
|
|
16624
16878
|
timeout: 1e4
|
|
16625
16879
|
});
|
|
16626
|
-
const $ =
|
|
16880
|
+
const $ = cheerio40.load(response.data);
|
|
16627
16881
|
const seeds = /* @__PURE__ */ new Set();
|
|
16628
|
-
const
|
|
16629
|
-
|
|
16630
|
-
|
|
16631
|
-
const h1 = $("h1").first().text().toLowerCase();
|
|
16632
|
-
const h1Words = h1.split(/[\s\-|:]+/).filter((w) => w.length > 3);
|
|
16633
|
-
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));
|
|
16634
16885
|
const metaKeywords = $('meta[name="keywords"]').attr("content");
|
|
16635
16886
|
if (metaKeywords) {
|
|
16636
16887
|
metaKeywords.split(",").forEach((k) => seeds.add(k.trim().toLowerCase()));
|
|
16637
16888
|
}
|
|
16638
16889
|
const desc = $('meta[name="description"]').attr("content")?.toLowerCase() || "";
|
|
16639
|
-
const descWords = desc
|
|
16890
|
+
const descWords = words(desc).filter((w) => w.length > 5);
|
|
16640
16891
|
descWords.slice(0, 5).forEach((w) => seeds.add(w));
|
|
16641
16892
|
return Array.from(seeds).slice(0, 10);
|
|
16642
16893
|
} catch {
|
|
@@ -16689,7 +16940,7 @@ function parseJsonReply(text) {
|
|
|
16689
16940
|
}
|
|
16690
16941
|
|
|
16691
16942
|
// src/keywords/site-crawler.ts
|
|
16692
|
-
import * as
|
|
16943
|
+
import * as cheerio41 from "cheerio";
|
|
16693
16944
|
var EXCLUDED_PATHS = [
|
|
16694
16945
|
"/cdn-cgi/",
|
|
16695
16946
|
"/wp-admin/",
|
|
@@ -16801,7 +17052,7 @@ async function crawlPage(url, timeout) {
|
|
|
16801
17052
|
validateStatus: (status) => status === 200
|
|
16802
17053
|
});
|
|
16803
17054
|
const html = response.data;
|
|
16804
|
-
const $ =
|
|
17055
|
+
const $ = cheerio41.load(html);
|
|
16805
17056
|
$('script, style, noscript, iframe, nav, footer, header, aside, [role="navigation"]').remove();
|
|
16806
17057
|
const title = $("title").text().trim();
|
|
16807
17058
|
const description = $('meta[name="description"]').attr("content")?.trim() || "";
|
|
@@ -19436,7 +19687,7 @@ function getDateRange(days = 28) {
|
|
|
19436
19687
|
}
|
|
19437
19688
|
|
|
19438
19689
|
// src/keywords/sources/competitor-analysis.ts
|
|
19439
|
-
import * as
|
|
19690
|
+
import * as cheerio42 from "cheerio";
|
|
19440
19691
|
var USER_AGENT2 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
|
|
19441
19692
|
async function discoverCompetitorKeywords(yourDomain, seedKeywords, competitors) {
|
|
19442
19693
|
const yourKeywords = /* @__PURE__ */ new Set();
|
|
@@ -19533,7 +19784,7 @@ async function analyzeSERP(query) {
|
|
|
19533
19784
|
headers: { "User-Agent": USER_AGENT2 },
|
|
19534
19785
|
timeout: 1e4
|
|
19535
19786
|
});
|
|
19536
|
-
const $ =
|
|
19787
|
+
const $ = cheerio42.load(response.data);
|
|
19537
19788
|
const results = [];
|
|
19538
19789
|
const relatedSearches = [];
|
|
19539
19790
|
const peopleAlsoAsk = [];
|
|
@@ -19986,7 +20237,7 @@ function formatTopicReport(result) {
|
|
|
19986
20237
|
|
|
19987
20238
|
// src/content/readability.ts
|
|
19988
20239
|
function analyzeReadability(text) {
|
|
19989
|
-
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();
|
|
19990
20241
|
const words = getWords(cleanText);
|
|
19991
20242
|
const sentences = getSentences(cleanText);
|
|
19992
20243
|
const paragraphs = getParagraphs(cleanText);
|
|
@@ -20055,7 +20306,7 @@ function getSentences(text) {
|
|
|
20055
20306
|
return text.split(/[.!?]+/).filter((s) => s.trim().length > 0);
|
|
20056
20307
|
}
|
|
20057
20308
|
function getParagraphs(text) {
|
|
20058
|
-
return text.split(/\n\n+/).filter((p) => p.trim().length
|
|
20309
|
+
return text.split(/\n\n+/).filter((p) => p.trim().split(/\s+/).length >= 3);
|
|
20059
20310
|
}
|
|
20060
20311
|
function countSyllables(word) {
|
|
20061
20312
|
word = word.toLowerCase().replace(/[^a-z]/g, "");
|
|
@@ -23143,7 +23394,7 @@ async function executeAgent(agent, options = {}) {
|
|
|
23143
23394
|
}
|
|
23144
23395
|
}
|
|
23145
23396
|
async function runDirectAnalysis(url) {
|
|
23146
|
-
const { analyzeUrl: analyzeUrl2 } = await import("./analyzer-
|
|
23397
|
+
const { analyzeUrl: analyzeUrl2 } = await import("./analyzer-HHRJODCF.mjs");
|
|
23147
23398
|
const result = await analyzeUrl2({ url });
|
|
23148
23399
|
if (!result.success || !result.data) {
|
|
23149
23400
|
throw new Error(result.error || "Analysis failed");
|
|
@@ -32323,6 +32574,7 @@ export {
|
|
|
32323
32574
|
LOCATION_CODES,
|
|
32324
32575
|
OG_IMAGE_SPECS,
|
|
32325
32576
|
PRIORITY_WEIGHTS,
|
|
32577
|
+
REDUNDANT_WHEN_PRESENT,
|
|
32326
32578
|
RankTracker,
|
|
32327
32579
|
SEO_SCOPES,
|
|
32328
32580
|
SITE_PROFILE_QUESTIONS,
|
|
@@ -32406,6 +32658,7 @@ export {
|
|
|
32406
32658
|
createPullRequest,
|
|
32407
32659
|
createSEOCommit,
|
|
32408
32660
|
createSEOCommits,
|
|
32661
|
+
dedupeIssues,
|
|
32409
32662
|
describeConcerns,
|
|
32410
32663
|
detectDuplicates,
|
|
32411
32664
|
detectFramework4 as detectFramework,
|
|
@@ -32542,6 +32795,7 @@ export {
|
|
|
32542
32795
|
loadAgent,
|
|
32543
32796
|
loadAgentByName,
|
|
32544
32797
|
mergePages,
|
|
32798
|
+
metaRefreshTarget,
|
|
32545
32799
|
openAIKeywordLLM,
|
|
32546
32800
|
optimizeForAI,
|
|
32547
32801
|
optimizeReadme,
|