@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.js
CHANGED
|
@@ -55,7 +55,8 @@ async function httpGet(url, config = {}) {
|
|
|
55
55
|
timeout = DEFAULT_TIMEOUT,
|
|
56
56
|
validateStatus = (status) => status >= 200 && status < 300,
|
|
57
57
|
maxRedirects = 5,
|
|
58
|
-
params
|
|
58
|
+
params,
|
|
59
|
+
maxBytes
|
|
59
60
|
} = config;
|
|
60
61
|
let finalUrl = url;
|
|
61
62
|
if (params) {
|
|
@@ -82,7 +83,7 @@ async function httpGet(url, config = {}) {
|
|
|
82
83
|
if (contentType.includes("application/json")) {
|
|
83
84
|
data = await response.json();
|
|
84
85
|
} else {
|
|
85
|
-
data = await response
|
|
86
|
+
data = await readText(response, maxBytes);
|
|
86
87
|
}
|
|
87
88
|
clearTimeout(timeoutId);
|
|
88
89
|
if (!validateStatus(response.status)) {
|
|
@@ -109,6 +110,23 @@ async function httpGet(url, config = {}) {
|
|
|
109
110
|
throw error;
|
|
110
111
|
}
|
|
111
112
|
}
|
|
113
|
+
async function readText(response, maxBytes) {
|
|
114
|
+
if (!maxBytes || !response.body) return response.text();
|
|
115
|
+
const reader = response.body.getReader();
|
|
116
|
+
const chunks = [];
|
|
117
|
+
let received = 0;
|
|
118
|
+
while (received < maxBytes) {
|
|
119
|
+
const { done, value } = await reader.read();
|
|
120
|
+
if (done) break;
|
|
121
|
+
chunks.push(value);
|
|
122
|
+
received += value.byteLength;
|
|
123
|
+
}
|
|
124
|
+
if (received >= maxBytes) await reader.cancel().catch(() => void 0);
|
|
125
|
+
const decoder = new TextDecoder();
|
|
126
|
+
let text = "";
|
|
127
|
+
for (const chunk of chunks) text += decoder.decode(chunk, { stream: true });
|
|
128
|
+
return text + decoder.decode();
|
|
129
|
+
}
|
|
112
130
|
async function httpHead(url, config = {}) {
|
|
113
131
|
const {
|
|
114
132
|
headers = {},
|
|
@@ -245,7 +263,7 @@ async function crawlUrl(params) {
|
|
|
245
263
|
async function extractMeta(params) {
|
|
246
264
|
const { html, url } = params;
|
|
247
265
|
try {
|
|
248
|
-
const $ =
|
|
266
|
+
const $ = cheerio43.load(html);
|
|
249
267
|
const meta = {
|
|
250
268
|
title: $("title").text().trim() || void 0,
|
|
251
269
|
description: $('meta[name="description"]').attr("content")?.trim(),
|
|
@@ -290,7 +308,7 @@ async function extractMeta(params) {
|
|
|
290
308
|
async function analyzeHeadings(params) {
|
|
291
309
|
const { html } = params;
|
|
292
310
|
try {
|
|
293
|
-
const $ =
|
|
311
|
+
const $ = cheerio43.load(html);
|
|
294
312
|
const headings = [];
|
|
295
313
|
$("h1, h2, h3, h4, h5, h6").each((_, el) => {
|
|
296
314
|
const tag = el.tagName.toLowerCase();
|
|
@@ -311,7 +329,7 @@ async function analyzeHeadings(params) {
|
|
|
311
329
|
async function extractImages2(params) {
|
|
312
330
|
const { html } = params;
|
|
313
331
|
try {
|
|
314
|
-
const $ =
|
|
332
|
+
const $ = cheerio43.load(html);
|
|
315
333
|
const images = [];
|
|
316
334
|
$("img").each((_, el) => {
|
|
317
335
|
images.push({
|
|
@@ -333,7 +351,7 @@ async function extractImages2(params) {
|
|
|
333
351
|
async function extractLinks2(params) {
|
|
334
352
|
const { html, baseUrl } = params;
|
|
335
353
|
try {
|
|
336
|
-
const $ =
|
|
354
|
+
const $ = cheerio43.load(html);
|
|
337
355
|
const links = [];
|
|
338
356
|
const baseHostname = new URL(baseUrl).hostname;
|
|
339
357
|
$("a[href]").each((_, el) => {
|
|
@@ -368,7 +386,7 @@ async function extractLinks2(params) {
|
|
|
368
386
|
async function extractSchema2(params) {
|
|
369
387
|
const { html } = params;
|
|
370
388
|
try {
|
|
371
|
-
const $ =
|
|
389
|
+
const $ = cheerio43.load(html);
|
|
372
390
|
const schemas = [];
|
|
373
391
|
$('script[type="application/ld+json"]').each((_, el) => {
|
|
374
392
|
try {
|
|
@@ -437,7 +455,7 @@ async function checkSitemap2(params) {
|
|
|
437
455
|
timeout: 1e4
|
|
438
456
|
});
|
|
439
457
|
if (response.status === 200 && response.data.includes("<?xml")) {
|
|
440
|
-
const $ =
|
|
458
|
+
const $ = cheerio43.load(response.data, { xmlMode: true });
|
|
441
459
|
const urlCount = $("url").length || $("sitemap").length;
|
|
442
460
|
return {
|
|
443
461
|
success: true,
|
|
@@ -466,12 +484,12 @@ async function checkSitemap2(params) {
|
|
|
466
484
|
};
|
|
467
485
|
}
|
|
468
486
|
}
|
|
469
|
-
var
|
|
487
|
+
var cheerio43;
|
|
470
488
|
var init_crawl = __esm({
|
|
471
489
|
"src/tools/crawl.ts"() {
|
|
472
490
|
"use strict";
|
|
473
491
|
init_http();
|
|
474
|
-
|
|
492
|
+
cheerio43 = __toESM(require("cheerio"));
|
|
475
493
|
}
|
|
476
494
|
});
|
|
477
495
|
|
|
@@ -868,7 +886,7 @@ function analyzeRobotsTxtForAI(robotsTxt) {
|
|
|
868
886
|
return { allowed, blocked, recommendations };
|
|
869
887
|
}
|
|
870
888
|
function detectRenderingMode(html) {
|
|
871
|
-
const $ =
|
|
889
|
+
const $ = cheerio44.load(html);
|
|
872
890
|
const signals = [];
|
|
873
891
|
const hasNextData = html.includes("__NEXT_DATA__");
|
|
874
892
|
const hasNuxtData = html.includes("__NUXT__");
|
|
@@ -909,7 +927,7 @@ function detectRenderingMode(html) {
|
|
|
909
927
|
};
|
|
910
928
|
}
|
|
911
929
|
function analyzeContentStructure(html) {
|
|
912
|
-
const $ =
|
|
930
|
+
const $ = cheerio44.load(html);
|
|
913
931
|
const jsonLdScripts = $('script[type="application/ld+json"]');
|
|
914
932
|
let hasStructuredData = jsonLdScripts.length > 0;
|
|
915
933
|
let hasFAQSchema = false;
|
|
@@ -959,7 +977,7 @@ function analyzeContentStructure(html) {
|
|
|
959
977
|
};
|
|
960
978
|
}
|
|
961
979
|
function analyzeCitationReadiness(html) {
|
|
962
|
-
const $ =
|
|
980
|
+
const $ = cheerio44.load(html);
|
|
963
981
|
const bodyText = $("body").text();
|
|
964
982
|
const hasCitations = $('cite, blockquote, [class*="citation"], [class*="reference"]').length > 0 || /\[\d+\]|\(\d{4}\)/.test(bodyText);
|
|
965
983
|
const externalLinks = $('a[href^="http"]').filter((_, el) => {
|
|
@@ -992,7 +1010,7 @@ function analyzeCitationReadiness(html) {
|
|
|
992
1010
|
};
|
|
993
1011
|
}
|
|
994
1012
|
function analyzeEntityExtraction(html) {
|
|
995
|
-
const $ =
|
|
1013
|
+
const $ = cheerio44.load(html);
|
|
996
1014
|
const bodyText = $("body").text();
|
|
997
1015
|
const definedTerms = [];
|
|
998
1016
|
$("dfn, abbr[title]").each((_, el) => {
|
|
@@ -1028,7 +1046,7 @@ function analyzeEntityExtraction(html) {
|
|
|
1028
1046
|
};
|
|
1029
1047
|
}
|
|
1030
1048
|
function calculateLLMSignals(structure, citation, entity, html) {
|
|
1031
|
-
const $ =
|
|
1049
|
+
const $ = cheerio44.load(html);
|
|
1032
1050
|
let contentClarity = 0;
|
|
1033
1051
|
if (structure.headingHierarchy === "good") contentClarity += 30;
|
|
1034
1052
|
else if (structure.headingHierarchy === "needs-work") contentClarity += 15;
|
|
@@ -1334,11 +1352,11 @@ Allow: /
|
|
|
1334
1352
|
Sitemap: ${siteUrl}/sitemap.xml
|
|
1335
1353
|
`;
|
|
1336
1354
|
}
|
|
1337
|
-
var
|
|
1355
|
+
var cheerio44, AI_CRAWLERS, AI_CRAWLERS_INFO;
|
|
1338
1356
|
var init_geo_analyzer = __esm({
|
|
1339
1357
|
"src/analyzers/geo-analyzer.ts"() {
|
|
1340
1358
|
"use strict";
|
|
1341
|
-
|
|
1359
|
+
cheerio44 = __toESM(require("cheerio"));
|
|
1342
1360
|
AI_CRAWLERS = {
|
|
1343
1361
|
// OpenAI
|
|
1344
1362
|
GPTBot: { userAgent: "GPTBot", company: "OpenAI", purpose: "ChatGPT training & browsing" },
|
|
@@ -1697,7 +1715,7 @@ function generateCWVIssues(lcp, fidInp, cls, ttfb, url) {
|
|
|
1697
1715
|
return issues;
|
|
1698
1716
|
}
|
|
1699
1717
|
function analyzeCoreWebVitals(html, url, headers) {
|
|
1700
|
-
const $ =
|
|
1718
|
+
const $ = cheerio45.load(html);
|
|
1701
1719
|
const lcp = analyzeLCP(html, $);
|
|
1702
1720
|
const fidInp = analyzeFID_INP(html, $);
|
|
1703
1721
|
const cls = analyzeCLS(html, $);
|
|
@@ -1742,11 +1760,11 @@ function analyzeCoreWebVitals(html, url, headers) {
|
|
|
1742
1760
|
issues
|
|
1743
1761
|
};
|
|
1744
1762
|
}
|
|
1745
|
-
var
|
|
1763
|
+
var cheerio45;
|
|
1746
1764
|
var init_core_web_vitals_analyzer = __esm({
|
|
1747
1765
|
"src/analyzers/core-web-vitals-analyzer.ts"() {
|
|
1748
1766
|
"use strict";
|
|
1749
|
-
|
|
1767
|
+
cheerio45 = __toESM(require("cheerio"));
|
|
1750
1768
|
}
|
|
1751
1769
|
});
|
|
1752
1770
|
|
|
@@ -2081,7 +2099,7 @@ function validateSchema(data, type) {
|
|
|
2081
2099
|
};
|
|
2082
2100
|
}
|
|
2083
2101
|
function analyzeStructuredData2(html, url) {
|
|
2084
|
-
const $ =
|
|
2102
|
+
const $ = cheerio46.load(html);
|
|
2085
2103
|
const schemas = [];
|
|
2086
2104
|
const issues = [];
|
|
2087
2105
|
const recommendations = [];
|
|
@@ -2345,11 +2363,11 @@ function generateSchemaTemplate(pageType, options) {
|
|
|
2345
2363
|
}
|
|
2346
2364
|
return JSON.stringify(result, null, 2);
|
|
2347
2365
|
}
|
|
2348
|
-
var
|
|
2366
|
+
var cheerio46, SCHEMA_REQUIREMENTS;
|
|
2349
2367
|
var init_structured_data_analyzer = __esm({
|
|
2350
2368
|
"src/analyzers/structured-data-analyzer.ts"() {
|
|
2351
2369
|
"use strict";
|
|
2352
|
-
|
|
2370
|
+
cheerio46 = __toESM(require("cheerio"));
|
|
2353
2371
|
SCHEMA_REQUIREMENTS = {
|
|
2354
2372
|
"Organization": {
|
|
2355
2373
|
required: ["name", "url"],
|
|
@@ -2465,7 +2483,7 @@ function getImageFormat2(src) {
|
|
|
2465
2483
|
}
|
|
2466
2484
|
}
|
|
2467
2485
|
function analyzeImages3(html, url) {
|
|
2468
|
-
const $ =
|
|
2486
|
+
const $ = cheerio47.load(html);
|
|
2469
2487
|
const issues = [];
|
|
2470
2488
|
const recommendations = [];
|
|
2471
2489
|
let score = 100;
|
|
@@ -2683,11 +2701,11 @@ function generateResponsiveImage(options) {
|
|
|
2683
2701
|
/>
|
|
2684
2702
|
</picture>`;
|
|
2685
2703
|
}
|
|
2686
|
-
var
|
|
2704
|
+
var cheerio47, MODERN_FORMATS3, LEGACY_FORMATS2, MAX_RECOMMENDED_DIMENSION;
|
|
2687
2705
|
var init_image_optimization_analyzer = __esm({
|
|
2688
2706
|
"src/analyzers/image-optimization-analyzer.ts"() {
|
|
2689
2707
|
"use strict";
|
|
2690
|
-
|
|
2708
|
+
cheerio47 = __toESM(require("cheerio"));
|
|
2691
2709
|
MODERN_FORMATS3 = ["webp", "avif"];
|
|
2692
2710
|
LEGACY_FORMATS2 = ["jpg", "jpeg", "png", "gif", "bmp"];
|
|
2693
2711
|
MAX_RECOMMENDED_DIMENSION = 2e3;
|
|
@@ -2728,7 +2746,7 @@ function analyzeAnchorText2(text) {
|
|
|
2728
2746
|
};
|
|
2729
2747
|
}
|
|
2730
2748
|
function analyzeInternalLinking(html, url) {
|
|
2731
|
-
const $ =
|
|
2749
|
+
const $ = cheerio48.load(html);
|
|
2732
2750
|
const issues = [];
|
|
2733
2751
|
const recommendations = [];
|
|
2734
2752
|
let score = 100;
|
|
@@ -2967,11 +2985,11 @@ function suggestInternalLinks(content, availablePages) {
|
|
|
2967
2985
|
}
|
|
2968
2986
|
return suggestions.sort((a, b) => b.relevance - a.relevance).filter((s, i, arr) => arr.findIndex((x) => x.suggestedUrl === s.suggestedUrl) === i).slice(0, 10);
|
|
2969
2987
|
}
|
|
2970
|
-
var
|
|
2988
|
+
var cheerio48, GENERIC_ANCHOR_TEXTS;
|
|
2971
2989
|
var init_internal_linking_analyzer = __esm({
|
|
2972
2990
|
"src/analyzers/internal-linking-analyzer.ts"() {
|
|
2973
2991
|
"use strict";
|
|
2974
|
-
|
|
2992
|
+
cheerio48 = __toESM(require("cheerio"));
|
|
2975
2993
|
GENERIC_ANCHOR_TEXTS = [
|
|
2976
2994
|
"click here",
|
|
2977
2995
|
"read more",
|
|
@@ -3165,7 +3183,7 @@ function analyzeMobileSpecific($) {
|
|
|
3165
3183
|
};
|
|
3166
3184
|
}
|
|
3167
3185
|
function analyzeMobileSEO(html, url) {
|
|
3168
|
-
const $ =
|
|
3186
|
+
const $ = cheerio49.load(html);
|
|
3169
3187
|
const issues = [];
|
|
3170
3188
|
const recommendations = [];
|
|
3171
3189
|
let score = 100;
|
|
@@ -3296,11 +3314,11 @@ function analyzeMobileSEO(html, url) {
|
|
|
3296
3314
|
recommendations
|
|
3297
3315
|
};
|
|
3298
3316
|
}
|
|
3299
|
-
var
|
|
3317
|
+
var cheerio49;
|
|
3300
3318
|
var init_mobile_seo_analyzer = __esm({
|
|
3301
3319
|
"src/analyzers/mobile-seo-analyzer.ts"() {
|
|
3302
3320
|
"use strict";
|
|
3303
|
-
|
|
3321
|
+
cheerio49 = __toESM(require("cheerio"));
|
|
3304
3322
|
}
|
|
3305
3323
|
});
|
|
3306
3324
|
|
|
@@ -3312,6 +3330,7 @@ __export(index_exports, {
|
|
|
3312
3330
|
LOCATION_CODES: () => LOCATION_CODES,
|
|
3313
3331
|
OG_IMAGE_SPECS: () => OG_IMAGE_SPECS,
|
|
3314
3332
|
PRIORITY_WEIGHTS: () => PRIORITY_WEIGHTS,
|
|
3333
|
+
REDUNDANT_WHEN_PRESENT: () => REDUNDANT_WHEN_PRESENT,
|
|
3315
3334
|
RankTracker: () => RankTracker,
|
|
3316
3335
|
SEO_SCOPES: () => SEO_SCOPES,
|
|
3317
3336
|
SITE_PROFILE_QUESTIONS: () => SITE_PROFILE_QUESTIONS,
|
|
@@ -3395,6 +3414,7 @@ __export(index_exports, {
|
|
|
3395
3414
|
createPullRequest: () => createPullRequest,
|
|
3396
3415
|
createSEOCommit: () => createSEOCommit,
|
|
3397
3416
|
createSEOCommits: () => createSEOCommits,
|
|
3417
|
+
dedupeIssues: () => dedupeIssues,
|
|
3398
3418
|
describeConcerns: () => describeConcerns,
|
|
3399
3419
|
detectDuplicates: () => detectDuplicates,
|
|
3400
3420
|
detectFramework: () => detectFramework4,
|
|
@@ -3531,6 +3551,7 @@ __export(index_exports, {
|
|
|
3531
3551
|
loadAgent: () => loadAgent,
|
|
3532
3552
|
loadAgentByName: () => loadAgentByName,
|
|
3533
3553
|
mergePages: () => mergePages,
|
|
3554
|
+
metaRefreshTarget: () => metaRefreshTarget,
|
|
3534
3555
|
openAIKeywordLLM: () => openAIKeywordLLM,
|
|
3535
3556
|
optimizeForAI: () => optimizeForAI,
|
|
3536
3557
|
optimizeReadme: () => optimizeReadme,
|
|
@@ -6353,7 +6374,7 @@ var ISSUE_DEFINITIONS = {
|
|
|
6353
6374
|
severity: "warning",
|
|
6354
6375
|
category: "performance",
|
|
6355
6376
|
title: "HTML document too large",
|
|
6356
|
-
description: "HTML size exceeds
|
|
6377
|
+
description: "HTML size exceeds 300KB.",
|
|
6357
6378
|
impact: "Increases load time and memory usage.",
|
|
6358
6379
|
howToFix: "Reduce inline CSS/JS, remove unnecessary HTML."
|
|
6359
6380
|
},
|
|
@@ -9095,7 +9116,7 @@ async function analyzePerformance(url) {
|
|
|
9095
9116
|
details: { ttfb: `${ttfbTime}ms` }
|
|
9096
9117
|
});
|
|
9097
9118
|
}
|
|
9098
|
-
if (htmlSize >
|
|
9119
|
+
if (htmlSize > 300 * 1024) {
|
|
9099
9120
|
issues.push({
|
|
9100
9121
|
...ISSUE_DEFINITIONS.HTML_TOO_LARGE,
|
|
9101
9122
|
affectedUrls: [url],
|
|
@@ -9996,7 +10017,9 @@ function analyzeAnchorText(html, url) {
|
|
|
9996
10017
|
} catch {
|
|
9997
10018
|
internalLinks++;
|
|
9998
10019
|
}
|
|
9999
|
-
const
|
|
10020
|
+
const imgAlt = $(el).find("img[alt]").map((_2, img) => ($(img).attr("alt") || "").trim()).get().join(" ").trim();
|
|
10021
|
+
const svgTitle = $(el).find("svg title").text().trim();
|
|
10022
|
+
const hasAccessibleText = text.length > 0 || ariaLabel || title || imgAlt.length > 0 || svgTitle.length > 0;
|
|
10000
10023
|
if (!hasAccessibleText) {
|
|
10001
10024
|
emptyAnchors++;
|
|
10002
10025
|
if (emptyAnchorUrls.length < 5) {
|
|
@@ -11257,7 +11280,7 @@ function analyzeLocalSEO(html, url) {
|
|
|
11257
11280
|
const detectedPhones = detectPhones(html);
|
|
11258
11281
|
const detectedAddresses = detectAddresses(html);
|
|
11259
11282
|
const socialProfiles = detectSocialProfiles(html);
|
|
11260
|
-
if (!hasLocalBusiness) {
|
|
11283
|
+
if (!hasLocalBusiness && (detectedPhones.length > 0 || detectedAddresses.length > 0)) {
|
|
11261
11284
|
issues.push({
|
|
11262
11285
|
...ISSUE_DEFINITIONS.LOCAL_BUSINESS_SCHEMA_MISSING,
|
|
11263
11286
|
affectedUrls: [url]
|
|
@@ -12320,7 +12343,17 @@ function analyzeKeywords(html, url) {
|
|
|
12320
12343
|
const sortPhrases = (phrases, n) => {
|
|
12321
12344
|
return Array.from(phrases).filter(([_, count]) => count >= 2).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([phrase, count]) => ({ phrase, count, wordCount: n }));
|
|
12322
12345
|
};
|
|
12323
|
-
const
|
|
12346
|
+
const brandLabels = /* @__PURE__ */ new Set();
|
|
12347
|
+
try {
|
|
12348
|
+
const host = new URL(url).hostname.toLowerCase().replace(/^www\./, "");
|
|
12349
|
+
for (const label of host.split(".").slice(0, -1)) brandLabels.add(label);
|
|
12350
|
+
} catch {
|
|
12351
|
+
}
|
|
12352
|
+
for (const name of [$('meta[property="og:site_name"]').attr("content"), $('meta[name="application-name"]').attr("content")]) {
|
|
12353
|
+
for (const token of tokenize(name || "")) brandLabels.add(token);
|
|
12354
|
+
}
|
|
12355
|
+
const isBrandWord = (word) => [...brandLabels].some((label) => label.includes(word) || word.includes(label));
|
|
12356
|
+
const potentialStuffing = totalWords >= 200 ? topKeywords.filter((k) => k.density > 4 && !isBrandWord(k.word)).map((k) => k.word) : [];
|
|
12324
12357
|
if (potentialStuffing.length > 0) {
|
|
12325
12358
|
issues.push({
|
|
12326
12359
|
...ISSUE_DEFINITIONS.KEYWORD_STUFFING,
|
|
@@ -12547,13 +12580,6 @@ function checkAMP(html, url) {
|
|
|
12547
12580
|
}
|
|
12548
12581
|
};
|
|
12549
12582
|
}
|
|
12550
|
-
const isArticle = $("article").length > 0 || $('meta[property="og:type"][content="article"]').length > 0 || html.toLowerCase().includes("blog") || html.toLowerCase().includes("news");
|
|
12551
|
-
if (isArticle) {
|
|
12552
|
-
issues.push({
|
|
12553
|
-
...ISSUE_DEFINITIONS.AMP_MISSING,
|
|
12554
|
-
affectedUrls: [url]
|
|
12555
|
-
});
|
|
12556
|
-
}
|
|
12557
12583
|
return {
|
|
12558
12584
|
issues,
|
|
12559
12585
|
data: {
|
|
@@ -13265,13 +13291,13 @@ async function analyzeSiteMaturity(html, url) {
|
|
|
13265
13291
|
contentToAuthorityRatio
|
|
13266
13292
|
};
|
|
13267
13293
|
const { strategy, recommendations } = determineStrategy(maturityLevel, authorityIndicators, contentMetrics);
|
|
13268
|
-
if (maturityLevel === "new") {
|
|
13294
|
+
if (maturityLevel === "new" && ageSource === "wayback" && estimatedAgeDays !== null) {
|
|
13269
13295
|
issues.push({
|
|
13270
13296
|
code: "NEW_SITE_STRATEGY",
|
|
13271
13297
|
severity: "notice",
|
|
13272
13298
|
category: "content",
|
|
13273
13299
|
title: "New website detected - Authority-first strategy recommended",
|
|
13274
|
-
description:
|
|
13300
|
+
description: `The Wayback Machine first saw this site ${estimatedAgeDays} days ago. Standard SEO advice won't work well for a site under 90 days old.`,
|
|
13275
13301
|
impact: "Creating lots of content without authority is wasted effort. Content from new sites rarely ranks.",
|
|
13276
13302
|
howToFix: "Focus on building 10+ quality backlinks to ONE great page before expanding content. See strategic recommendations.",
|
|
13277
13303
|
affectedUrls: [url],
|
|
@@ -13282,24 +13308,7 @@ async function analyzeSiteMaturity(html, url) {
|
|
|
13282
13308
|
}
|
|
13283
13309
|
});
|
|
13284
13310
|
}
|
|
13285
|
-
if (
|
|
13286
|
-
issues.push({
|
|
13287
|
-
code: "CONTENT_AUTHORITY_IMBALANCE",
|
|
13288
|
-
severity: "warning",
|
|
13289
|
-
category: "content",
|
|
13290
|
-
title: "Content-to-authority imbalance",
|
|
13291
|
-
description: `Site has ~${indexedPages} pages but limited backlink signals. Content is unlikely to rank.`,
|
|
13292
|
-
impact: "Most of your content may be invisible to search engines due to low domain authority.",
|
|
13293
|
-
howToFix: "Stop publishing new content. Focus 100% on link building until you see ranking improvements.",
|
|
13294
|
-
affectedUrls: [url],
|
|
13295
|
-
details: {
|
|
13296
|
-
indexedPages,
|
|
13297
|
-
contentToAuthorityRatio,
|
|
13298
|
-
maturityLevel
|
|
13299
|
-
}
|
|
13300
|
-
});
|
|
13301
|
-
}
|
|
13302
|
-
if (maturityLevel === "growing" && !htmlSignals.hasBlog) {
|
|
13311
|
+
if (maturityLevel === "growing" && ageSource === "wayback" && !htmlSignals.hasBlog) {
|
|
13303
13312
|
issues.push({
|
|
13304
13313
|
code: "GROWING_SITE_NO_BLOG",
|
|
13305
13314
|
severity: "notice",
|
|
@@ -13793,8 +13802,8 @@ function analyzeRendering(html) {
|
|
|
13793
13802
|
const hasSemanticStructure = hasH1 && (hasMain || hasArticle);
|
|
13794
13803
|
const title = $("title").text().trim();
|
|
13795
13804
|
const h1Text = $("h1").first().text().trim();
|
|
13796
|
-
const
|
|
13797
|
-
const keyContentInHTML = title.length >
|
|
13805
|
+
const hasSubstantialParagraph = $("p").toArray().some((p) => $(p).text().trim().length > 50);
|
|
13806
|
+
const keyContentInHTML = title.length > 0 && h1Text.length > 0 && hasSubstantialParagraph;
|
|
13798
13807
|
const EXPECTED_MIN_CONTENT = 1e3;
|
|
13799
13808
|
const htmlContentRatio = Math.min(100, mainContentCharCount / EXPECTED_MIN_CONTENT * 100);
|
|
13800
13809
|
let aiReadableScore = 50;
|
|
@@ -14014,9 +14023,9 @@ function analyzeClientRendering(html, url) {
|
|
|
14014
14023
|
howToFix: "Ensure the initial HTML contains: meaningful <title>, descriptive <h1>, and a substantial first paragraph with your key message.",
|
|
14015
14024
|
affectedUrls: [url],
|
|
14016
14025
|
details: {
|
|
14017
|
-
hasTitle: $("title").text().trim().length >
|
|
14018
|
-
hasH1: $("h1").text().trim().length >
|
|
14019
|
-
|
|
14026
|
+
hasTitle: $("title").text().trim().length > 0,
|
|
14027
|
+
hasH1: $("h1").text().trim().length > 0,
|
|
14028
|
+
hasSubstantialParagraph: $("p").toArray().some((p) => $(p).text().trim().length > 50)
|
|
14020
14029
|
}
|
|
14021
14030
|
});
|
|
14022
14031
|
}
|
|
@@ -14565,6 +14574,107 @@ function analyzeResponsiveImages(html, url) {
|
|
|
14565
14574
|
// src/audit/checks/bing-optimization.ts
|
|
14566
14575
|
var cheerio28 = __toESM(require("cheerio"));
|
|
14567
14576
|
init_http();
|
|
14577
|
+
|
|
14578
|
+
// src/audit/checks/site-verification.ts
|
|
14579
|
+
init_http();
|
|
14580
|
+
var UNCHECKED = {
|
|
14581
|
+
dnsChecked: false,
|
|
14582
|
+
googleDns: false,
|
|
14583
|
+
bingDns: false,
|
|
14584
|
+
bingSiteAuthFile: false
|
|
14585
|
+
};
|
|
14586
|
+
var DNS_TIMEOUT_MS = 2e3;
|
|
14587
|
+
var FILE_TIMEOUT_MS = 5e3;
|
|
14588
|
+
var CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
14589
|
+
var cache = /* @__PURE__ */ new Map();
|
|
14590
|
+
function lookupSiteVerification(url) {
|
|
14591
|
+
let parsed;
|
|
14592
|
+
try {
|
|
14593
|
+
parsed = new URL(url);
|
|
14594
|
+
} catch {
|
|
14595
|
+
return Promise.resolve(UNCHECKED);
|
|
14596
|
+
}
|
|
14597
|
+
const host = parsed.hostname.toLowerCase();
|
|
14598
|
+
const now = Date.now();
|
|
14599
|
+
const cached = cache.get(host);
|
|
14600
|
+
if (cached && cached.expires > now) return cached.result;
|
|
14601
|
+
const result = Promise.all([readTxtRecords(host), fetchBingSiteAuth(parsed.origin)]).then(
|
|
14602
|
+
([txt, bingSiteAuthFile]) => ({
|
|
14603
|
+
dnsChecked: txt.checked,
|
|
14604
|
+
googleDns: txt.records.some((record) => /^google-site-verification=/i.test(record)),
|
|
14605
|
+
bingDns: txt.records.some((record) => /^MS=/i.test(record)),
|
|
14606
|
+
bingSiteAuthFile
|
|
14607
|
+
})
|
|
14608
|
+
);
|
|
14609
|
+
cache.set(host, { expires: now + CACHE_TTL_MS, result });
|
|
14610
|
+
return result;
|
|
14611
|
+
}
|
|
14612
|
+
var resolveTxtLoader;
|
|
14613
|
+
function loadResolveTxt() {
|
|
14614
|
+
resolveTxtLoader ??= (async () => {
|
|
14615
|
+
if (typeof process === "undefined" || !process.versions?.node) return null;
|
|
14616
|
+
try {
|
|
14617
|
+
const specifier = "node:dns";
|
|
14618
|
+
const dns = await import(specifier);
|
|
14619
|
+
const resolveTxt = dns?.promises?.resolveTxt ?? dns?.default?.promises?.resolveTxt;
|
|
14620
|
+
return typeof resolveTxt === "function" ? resolveTxt : null;
|
|
14621
|
+
} catch {
|
|
14622
|
+
return null;
|
|
14623
|
+
}
|
|
14624
|
+
})();
|
|
14625
|
+
return resolveTxtLoader;
|
|
14626
|
+
}
|
|
14627
|
+
function domainCandidates(host) {
|
|
14628
|
+
if (/^[\d.]+$/.test(host) || host.includes(":")) return [];
|
|
14629
|
+
const labels = host.split(".").filter(Boolean);
|
|
14630
|
+
const names = [];
|
|
14631
|
+
for (let n = 2; n <= labels.length && names.length < 4; n++) {
|
|
14632
|
+
names.push(labels.slice(-n).join("."));
|
|
14633
|
+
}
|
|
14634
|
+
return names.length > 0 ? names : [host];
|
|
14635
|
+
}
|
|
14636
|
+
async function readTxtRecords(host) {
|
|
14637
|
+
const resolveTxt = await loadResolveTxt();
|
|
14638
|
+
if (!resolveTxt) return { checked: false, records: [] };
|
|
14639
|
+
const lookups = await Promise.all(domainCandidates(host).map((name) => resolveTxtWithin(resolveTxt, name)));
|
|
14640
|
+
return {
|
|
14641
|
+
checked: lookups.every((records) => records !== null),
|
|
14642
|
+
records: lookups.flatMap((records) => records ?? [])
|
|
14643
|
+
};
|
|
14644
|
+
}
|
|
14645
|
+
var NO_RECORD_CODES = /* @__PURE__ */ new Set(["ENODATA", "ENOTFOUND"]);
|
|
14646
|
+
async function resolveTxtWithin(resolveTxt, name) {
|
|
14647
|
+
let timer;
|
|
14648
|
+
const timeout = new Promise((resolve) => {
|
|
14649
|
+
timer = setTimeout(() => resolve(null), DNS_TIMEOUT_MS);
|
|
14650
|
+
});
|
|
14651
|
+
try {
|
|
14652
|
+
return await Promise.race([
|
|
14653
|
+
resolveTxt(name).then(
|
|
14654
|
+
(records) => records.map((chunks) => chunks.join("").trim()),
|
|
14655
|
+
(error) => NO_RECORD_CODES.has(error?.code ?? "") ? [] : null
|
|
14656
|
+
),
|
|
14657
|
+
timeout
|
|
14658
|
+
]);
|
|
14659
|
+
} catch {
|
|
14660
|
+
return null;
|
|
14661
|
+
} finally {
|
|
14662
|
+
clearTimeout(timer);
|
|
14663
|
+
}
|
|
14664
|
+
}
|
|
14665
|
+
async function fetchBingSiteAuth(origin) {
|
|
14666
|
+
try {
|
|
14667
|
+
const response = await httpGet(`${origin}/BingSiteAuth.xml`, {
|
|
14668
|
+
timeout: FILE_TIMEOUT_MS,
|
|
14669
|
+
maxBytes: 64 * 1024
|
|
14670
|
+
});
|
|
14671
|
+
return response.status === 200 && typeof response.data === "string" && !looksLikeHtmlFallback(response.data, response.headers["content-type"]) && /<users[\s>]/i.test(response.data);
|
|
14672
|
+
} catch {
|
|
14673
|
+
return false;
|
|
14674
|
+
}
|
|
14675
|
+
}
|
|
14676
|
+
|
|
14677
|
+
// src/audit/checks/bing-optimization.ts
|
|
14568
14678
|
async function analyzeBingOptimization(html, url) {
|
|
14569
14679
|
const issues = [];
|
|
14570
14680
|
const $ = cheerio28.load(html);
|
|
@@ -14573,12 +14683,9 @@ async function analyzeBingOptimization(html, url) {
|
|
|
14573
14683
|
const detectedIssues = [];
|
|
14574
14684
|
const bingMetaTag = $('meta[name="msvalidate.01"]').attr("content");
|
|
14575
14685
|
const hasBingMetaTag = !!bingMetaTag && bingMetaTag.length > 0;
|
|
14576
|
-
|
|
14577
|
-
|
|
14578
|
-
|
|
14579
|
-
hasBingSiteAuth = authResponse.status === 200 && authResponse.data.includes("<?xml") && authResponse.data.toLowerCase().includes("users");
|
|
14580
|
-
} catch {
|
|
14581
|
-
}
|
|
14686
|
+
const verification = await lookupSiteVerification(url);
|
|
14687
|
+
const hasBingSiteAuth = verification.bingSiteAuthFile;
|
|
14688
|
+
const hasBingDnsRecord = verification.bingDns;
|
|
14582
14689
|
let hasBingIndexNowKey = false;
|
|
14583
14690
|
try {
|
|
14584
14691
|
const keyPatterns = [
|
|
@@ -14626,22 +14733,27 @@ async function analyzeBingOptimization(html, url) {
|
|
|
14626
14733
|
bingSitemapAccessible = sitemapResponse.status === 200 && sitemapResponse.data.includes("<?xml");
|
|
14627
14734
|
} catch {
|
|
14628
14735
|
}
|
|
14629
|
-
if (!hasBingMetaTag && !hasBingSiteAuth) {
|
|
14736
|
+
if (!hasBingMetaTag && !hasBingSiteAuth && !hasBingDnsRecord) {
|
|
14630
14737
|
detectedIssues.push("No Bing site verification");
|
|
14631
14738
|
issues.push({
|
|
14632
14739
|
code: "BING_NOT_VERIFIED",
|
|
14633
|
-
|
|
14740
|
+
// A notice: verification done through Search Console import or a DNS record this run
|
|
14741
|
+
// could not see leaves no trace in the HTML, so this cannot be a warning.
|
|
14742
|
+
severity: "notice",
|
|
14634
14743
|
category: "ai-readiness",
|
|
14635
|
-
title: "Site not verified with Bing Webmaster Tools",
|
|
14636
|
-
description: "ChatGPT Search uses Bing's index. Verifying your site with Bing Webmaster Tools ensures proper indexing and access to Bing-specific SEO tools.",
|
|
14744
|
+
title: verification.dnsChecked ? "Site not verified with Bing Webmaster Tools" : "Bing Webmaster Tools verification not visible in the HTML",
|
|
14745
|
+
description: verification.dnsChecked ? "No msvalidate.01 meta tag in the HTML, no MS= TXT record on the domain and no /BingSiteAuth.xml file. ChatGPT Search uses Bing's index. Verifying your site with Bing Webmaster Tools ensures proper indexing and access to Bing-specific SEO tools." : "No msvalidate.01 meta tag in the HTML and no /BingSiteAuth.xml file. The site may be verified through a DNS record instead, which this run could not check. ChatGPT Search uses Bing's index. Verifying your site with Bing Webmaster Tools ensures proper indexing and access to Bing-specific SEO tools.",
|
|
14637
14746
|
impact: "May reduce visibility in ChatGPT Search results and miss Bing-specific optimization opportunities.",
|
|
14638
|
-
howToFix: "
|
|
14747
|
+
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.",
|
|
14639
14748
|
affectedUrls: [url],
|
|
14640
14749
|
details: {
|
|
14641
14750
|
hasBingMetaTag,
|
|
14642
14751
|
hasBingSiteAuth,
|
|
14752
|
+
hasBingDnsRecord,
|
|
14753
|
+
dnsChecked: verification.dnsChecked,
|
|
14643
14754
|
verificationMethods: [
|
|
14644
14755
|
'Add <meta name="msvalidate.01" content="YOUR_CODE"> to <head>',
|
|
14756
|
+
"Add an MS=YOUR_CODE TXT record to the domain's DNS",
|
|
14645
14757
|
"Upload BingSiteAuth.xml to root directory"
|
|
14646
14758
|
]
|
|
14647
14759
|
}
|
|
@@ -14678,7 +14790,7 @@ async function analyzeBingOptimization(html, url) {
|
|
|
14678
14790
|
});
|
|
14679
14791
|
}
|
|
14680
14792
|
const hasLocalBusinessSchema = bingPlacesDetected;
|
|
14681
|
-
const hasAddressOnPage = $("address").length > 0 || html
|
|
14793
|
+
const hasAddressOnPage = $("address").text().trim().length > 0 || detectAddresses(html).length > 0;
|
|
14682
14794
|
if (hasAddressOnPage && !hasLocalBusinessSchema) {
|
|
14683
14795
|
detectedIssues.push("Local business may not be on Bing Places");
|
|
14684
14796
|
issues.push({
|
|
@@ -14701,6 +14813,8 @@ async function analyzeBingOptimization(html, url) {
|
|
|
14701
14813
|
data: {
|
|
14702
14814
|
hasBingSiteAuth,
|
|
14703
14815
|
hasBingMetaTag,
|
|
14816
|
+
hasBingDnsRecord,
|
|
14817
|
+
dnsChecked: verification.dnsChecked,
|
|
14704
14818
|
hasBingIndexNowKey,
|
|
14705
14819
|
bingPlacesDetected,
|
|
14706
14820
|
bingSitemapAccessible,
|
|
@@ -16207,23 +16321,6 @@ function analyzeColorContrast(html, url) {
|
|
|
16207
16321
|
});
|
|
16208
16322
|
}
|
|
16209
16323
|
}
|
|
16210
|
-
if (cannotDetermine > elementsAnalyzed * 0.8) {
|
|
16211
|
-
issues.push({
|
|
16212
|
-
code: "A11Y_CONTRAST_UNDETERMINED",
|
|
16213
|
-
severity: "notice",
|
|
16214
|
-
category: "accessibility",
|
|
16215
|
-
title: "Color contrast could not be fully analyzed",
|
|
16216
|
-
description: "Most text colors are set via CSS stylesheets rather than inline styles. Full contrast checking requires browser rendering.",
|
|
16217
|
-
impact: "Potential contrast issues may exist that were not detected.",
|
|
16218
|
-
howToFix: "Use browser DevTools (Lighthouse accessibility audit) or dedicated tools like axe-core for comprehensive contrast checking.",
|
|
16219
|
-
affectedUrls: [url],
|
|
16220
|
-
details: {
|
|
16221
|
-
elementsAnalyzed,
|
|
16222
|
-
cannotDetermine,
|
|
16223
|
-
recommendation: "Run Lighthouse accessibility audit for complete contrast analysis."
|
|
16224
|
-
}
|
|
16225
|
-
});
|
|
16226
|
-
}
|
|
16227
16324
|
return {
|
|
16228
16325
|
issues,
|
|
16229
16326
|
data: {
|
|
@@ -16686,6 +16783,10 @@ function analyzePageResources(html, url) {
|
|
|
16686
16783
|
// src/audit/checks/responsive-css.ts
|
|
16687
16784
|
init_http();
|
|
16688
16785
|
var cheerio35 = __toESM(require("cheerio"));
|
|
16786
|
+
var MAX_STYLESHEETS = 3;
|
|
16787
|
+
var MAX_STYLESHEET_BYTES = 1024 * 1024;
|
|
16788
|
+
var STYLESHEET_TIMEOUT_MS = 3e3;
|
|
16789
|
+
var RESPONSIVE_SIGNAL = /@media\b|@container\b|\bclamp\(|(?<![\w-])(?:min|max)\(/i;
|
|
16689
16790
|
function extractMediaQueries(css) {
|
|
16690
16791
|
const mediaQueries = [];
|
|
16691
16792
|
const regex = /@media\s*([^{]+)/g;
|
|
@@ -16695,36 +16796,31 @@ function extractMediaQueries(css) {
|
|
|
16695
16796
|
}
|
|
16696
16797
|
return mediaQueries;
|
|
16697
16798
|
}
|
|
16799
|
+
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;
|
|
16698
16800
|
function classifyBreakpoints(mediaQueries) {
|
|
16699
16801
|
const breakpoints = /* @__PURE__ */ new Set();
|
|
16700
16802
|
let hasMobile = false;
|
|
16701
16803
|
let hasTablet = false;
|
|
16702
16804
|
let hasDesktop = false;
|
|
16703
16805
|
for (const query of mediaQueries) {
|
|
16704
|
-
const widthMatches = query.match(/(?:min|max)-width\s*:\s*(\d+)(?:px|em|rem)?/gi);
|
|
16705
|
-
if (widthMatches) {
|
|
16706
|
-
for (const match of widthMatches) {
|
|
16707
|
-
const valueMatch = match.match(/(\d+)/);
|
|
16708
|
-
if (valueMatch) {
|
|
16709
|
-
const value = parseInt(valueMatch[1], 10);
|
|
16710
|
-
breakpoints.add(`${value}px`);
|
|
16711
|
-
if (value <= 480) {
|
|
16712
|
-
hasMobile = true;
|
|
16713
|
-
} else if (value <= 768) {
|
|
16714
|
-
hasMobile = true;
|
|
16715
|
-
} else if (value <= 1024) {
|
|
16716
|
-
hasTablet = true;
|
|
16717
|
-
} else {
|
|
16718
|
-
hasDesktop = true;
|
|
16719
|
-
}
|
|
16720
|
-
}
|
|
16721
|
-
}
|
|
16722
|
-
}
|
|
16723
|
-
if (/screen\s+and/i.test(query)) {
|
|
16724
|
-
}
|
|
16725
16806
|
if (/print/i.test(query)) {
|
|
16726
16807
|
continue;
|
|
16727
16808
|
}
|
|
16809
|
+
for (const match of query.matchAll(WIDTH_RULE)) {
|
|
16810
|
+
const raw = match[1] ?? match[3];
|
|
16811
|
+
const unit = (match[2] ?? match[4] ?? "px").toLowerCase();
|
|
16812
|
+
const value = Math.round(parseFloat(raw) * (unit === "px" ? 1 : 16));
|
|
16813
|
+
breakpoints.add(`${value}px`);
|
|
16814
|
+
if (value <= 480) {
|
|
16815
|
+
hasMobile = true;
|
|
16816
|
+
} else if (value <= 768) {
|
|
16817
|
+
hasMobile = true;
|
|
16818
|
+
} else if (value <= 1024) {
|
|
16819
|
+
hasTablet = true;
|
|
16820
|
+
} else {
|
|
16821
|
+
hasDesktop = true;
|
|
16822
|
+
}
|
|
16823
|
+
}
|
|
16728
16824
|
}
|
|
16729
16825
|
return {
|
|
16730
16826
|
hasMobile,
|
|
@@ -16742,6 +16838,7 @@ async function analyzeResponsiveCss(html, url) {
|
|
|
16742
16838
|
const allMediaQueries = [];
|
|
16743
16839
|
const externalSources = [];
|
|
16744
16840
|
let inlineCount = 0;
|
|
16841
|
+
let hasResponsiveSignals = false;
|
|
16745
16842
|
$("style").each((_, el) => {
|
|
16746
16843
|
const css = $(el).text();
|
|
16747
16844
|
const queries = extractMediaQueries(css);
|
|
@@ -16749,39 +16846,43 @@ async function analyzeResponsiveCss(html, url) {
|
|
|
16749
16846
|
allMediaQueries.push(...queries);
|
|
16750
16847
|
inlineCount += queries.length;
|
|
16751
16848
|
}
|
|
16849
|
+
if (RESPONSIVE_SIGNAL.test(css)) hasResponsiveSignals = true;
|
|
16752
16850
|
});
|
|
16753
|
-
const
|
|
16754
|
-
|
|
16851
|
+
const sameOrigin = [];
|
|
16852
|
+
let crossOrigin = 0;
|
|
16853
|
+
$("link[rel][href]").each((_, el) => {
|
|
16854
|
+
if (!/\bstylesheet\b/i.test($(el).attr("rel") ?? "")) return;
|
|
16855
|
+
const href = $(el).attr("href");
|
|
16856
|
+
if (!href || href.startsWith("data:")) return;
|
|
16755
16857
|
try {
|
|
16756
|
-
const
|
|
16757
|
-
|
|
16858
|
+
const resolved = new URL(href, url);
|
|
16859
|
+
if (resolved.hostname === baseUrl.hostname) sameOrigin.push(resolved.href);
|
|
16860
|
+
else crossOrigin++;
|
|
16758
16861
|
} catch {
|
|
16759
|
-
|
|
16862
|
+
crossOrigin++;
|
|
16760
16863
|
}
|
|
16761
16864
|
});
|
|
16762
|
-
|
|
16763
|
-
|
|
16764
|
-
|
|
16765
|
-
|
|
16766
|
-
|
|
16767
|
-
|
|
16768
|
-
|
|
16769
|
-
|
|
16770
|
-
|
|
16865
|
+
let fetchFailed = 0;
|
|
16866
|
+
await Promise.all(
|
|
16867
|
+
sameOrigin.slice(0, MAX_STYLESHEETS).map(async (cssUrl) => {
|
|
16868
|
+
try {
|
|
16869
|
+
const response = await httpGet(cssUrl, {
|
|
16870
|
+
timeout: STYLESHEET_TIMEOUT_MS,
|
|
16871
|
+
maxBytes: MAX_STYLESHEET_BYTES
|
|
16872
|
+
});
|
|
16873
|
+
if (typeof response.data !== "string") throw new Error("not text");
|
|
16771
16874
|
const queries = extractMediaQueries(response.data);
|
|
16772
|
-
if (queries.length > 0)
|
|
16773
|
-
|
|
16875
|
+
if (queries.length > 0) allMediaQueries.push(...queries);
|
|
16876
|
+
if (RESPONSIVE_SIGNAL.test(response.data)) {
|
|
16877
|
+
hasResponsiveSignals = true;
|
|
16774
16878
|
externalSources.push(cssUrl);
|
|
16775
16879
|
}
|
|
16880
|
+
} catch {
|
|
16881
|
+
fetchFailed++;
|
|
16776
16882
|
}
|
|
16777
|
-
}
|
|
16778
|
-
|
|
16779
|
-
|
|
16780
|
-
await Promise.race([
|
|
16781
|
-
Promise.all(cssPromises),
|
|
16782
|
-
new Promise((resolve) => setTimeout(resolve, 6e3))
|
|
16783
|
-
// 6s overall timeout
|
|
16784
|
-
]);
|
|
16883
|
+
})
|
|
16884
|
+
);
|
|
16885
|
+
const stylesheetsCovered = crossOrigin === 0 && fetchFailed === 0 && sameOrigin.length <= MAX_STYLESHEETS;
|
|
16785
16886
|
const hasMediaQueries = allMediaQueries.length > 0;
|
|
16786
16887
|
const { hasMobile, hasTablet, hasDesktop, breakpoints } = classifyBreakpoints(allMediaQueries);
|
|
16787
16888
|
if (!hasViewport) {
|
|
@@ -16796,21 +16897,22 @@ async function analyzeResponsiveCss(html, url) {
|
|
|
16796
16897
|
affectedUrls: [url]
|
|
16797
16898
|
});
|
|
16798
16899
|
}
|
|
16799
|
-
if (!
|
|
16900
|
+
if (!hasResponsiveSignals && stylesheetsCovered) {
|
|
16800
16901
|
issues.push({
|
|
16801
16902
|
code: "RESPONSIVE_NO_MEDIA_QUERIES",
|
|
16802
16903
|
severity: "warning",
|
|
16803
16904
|
category: "mobile",
|
|
16804
16905
|
title: "No CSS media queries detected",
|
|
16805
|
-
description: "No @media queries found in inline styles or first-party stylesheets.",
|
|
16906
|
+
description: "No @media or @container queries and no fluid sizing (clamp, min, max) found in inline styles or first-party stylesheets.",
|
|
16806
16907
|
impact: "Without media queries, the site likely uses fixed layouts that do not adapt to different screen sizes. This hurts mobile usability and SEO.",
|
|
16807
16908
|
howToFix: "Add CSS media queries to adapt layout for different screen sizes. Consider using a mobile-first approach with min-width breakpoints.",
|
|
16808
16909
|
affectedUrls: [url],
|
|
16809
16910
|
details: {
|
|
16810
|
-
suggestion: "Common breakpoints: 480px (mobile), 768px (tablet), 1024px (desktop)"
|
|
16911
|
+
suggestion: "Common breakpoints: 480px (mobile), 768px (tablet), 1024px (desktop)",
|
|
16912
|
+
stylesheetsScanned: sameOrigin.length
|
|
16811
16913
|
}
|
|
16812
16914
|
});
|
|
16813
|
-
} else if (!hasMobile && !hasTablet) {
|
|
16915
|
+
} else if (hasMediaQueries && !hasMobile && !hasTablet) {
|
|
16814
16916
|
issues.push({
|
|
16815
16917
|
code: "RESPONSIVE_NO_MOBILE_BREAKPOINT",
|
|
16816
16918
|
severity: "warning",
|
|
@@ -16836,6 +16938,8 @@ async function analyzeResponsiveCss(html, url) {
|
|
|
16836
16938
|
inline: inlineCount,
|
|
16837
16939
|
external: externalSources
|
|
16838
16940
|
},
|
|
16941
|
+
hasResponsiveSignals,
|
|
16942
|
+
stylesheetsCovered,
|
|
16839
16943
|
breakpoints,
|
|
16840
16944
|
hasMobileBreakpoint: hasMobile,
|
|
16841
16945
|
hasTabletBreakpoint: hasTablet,
|
|
@@ -17474,7 +17578,22 @@ var urlSafetyDatabase = {
|
|
|
17474
17578
|
|
|
17475
17579
|
// src/audit/checks/tracking-verification.ts
|
|
17476
17580
|
var cheerio36 = __toESM(require("cheerio"));
|
|
17477
|
-
|
|
17581
|
+
var OTHER_ANALYTICS = [
|
|
17582
|
+
{ name: "Cloudflare Web Analytics", pattern: /cloudflareinsights\.com\/beacon(?:\.min)?\.js/i },
|
|
17583
|
+
{ name: "Plausible", pattern: /plausible\.io\/js\/|<script[^>]*data-domain=[^>]*plausible|<script[^>]*plausible[^>]*data-domain=/i },
|
|
17584
|
+
{ name: "Fathom", pattern: /cdn\.usefathom\.com/i },
|
|
17585
|
+
// data-website-id is Umami's attribute, on the hosted script and on self-hosted ones alike.
|
|
17586
|
+
{ name: "Umami", pattern: /<script[^>]*\bdata-website-id=/i },
|
|
17587
|
+
{ name: "PostHog", pattern: /posthog\.init\s*\(|\.posthog\.com\//i },
|
|
17588
|
+
{ name: "Matomo", pattern: /matomo\.js|piwik\.js|_paq\s*(?:=|\.push)/i },
|
|
17589
|
+
{ name: "Simple Analytics", pattern: /scripts\.simpleanalyticscdn\.com/i },
|
|
17590
|
+
{ name: "Mixpanel", pattern: /cdn\.mxpnl\.com|mixpanel\.init\s*\(/i },
|
|
17591
|
+
{ name: "Segment", pattern: /cdn\.segment\.com/i }
|
|
17592
|
+
];
|
|
17593
|
+
function detectOtherAnalytics(html) {
|
|
17594
|
+
return OTHER_ANALYTICS.filter(({ pattern }) => pattern.test(html)).map(({ name }) => name);
|
|
17595
|
+
}
|
|
17596
|
+
async function analyzeTrackingVerification(html, url) {
|
|
17478
17597
|
const issues = [];
|
|
17479
17598
|
const $ = cheerio36.load(html);
|
|
17480
17599
|
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);
|
|
@@ -17484,10 +17603,16 @@ function analyzeTrackingVerification(html, url) {
|
|
|
17484
17603
|
const hasGtm = !!gtmMatch;
|
|
17485
17604
|
const gtmContainerId = gtmMatch ? gtmMatch[1] : void 0;
|
|
17486
17605
|
const hasGtmNoscript = /googletagmanager\.com\/ns\.html\?id=GTM-[A-Z0-9]+/i.test(html);
|
|
17606
|
+
const otherAnalytics = detectOtherAnalytics(html);
|
|
17607
|
+
const verification = await lookupSiteVerification(url);
|
|
17487
17608
|
const gscMeta = $('meta[name="google-site-verification"]').attr("content");
|
|
17488
|
-
const
|
|
17609
|
+
const hasGscMeta = !!(gscMeta && gscMeta.length > 10);
|
|
17610
|
+
const hasGscVerification = hasGscMeta || verification.googleDns;
|
|
17611
|
+
const gscSource = hasGscMeta ? "meta" : verification.googleDns ? "dns" : void 0;
|
|
17489
17612
|
const bingMeta = $('meta[name="msvalidate.01"]').attr("content");
|
|
17490
|
-
const
|
|
17613
|
+
const hasBingMeta = !!(bingMeta && bingMeta.length > 10);
|
|
17614
|
+
const hasBingVerification = hasBingMeta || verification.bingDns || verification.bingSiteAuthFile;
|
|
17615
|
+
const bingSource = hasBingMeta ? "meta" : verification.bingDns ? "dns" : verification.bingSiteAuthFile ? "file" : void 0;
|
|
17491
17616
|
const jsonLdScripts = $('script[type="application/ld+json"]');
|
|
17492
17617
|
const hasMicrodata = $('[itemtype*="schema.org"]').length > 0;
|
|
17493
17618
|
const hasJsonLd = jsonLdScripts.length > 0;
|
|
@@ -17509,16 +17634,19 @@ function analyzeTrackingVerification(html, url) {
|
|
|
17509
17634
|
const data = {
|
|
17510
17635
|
ga4: { detected: hasGa4, measurementId: ga4MeasurementId },
|
|
17511
17636
|
gtm: { detected: hasGtm, containerId: gtmContainerId, hasNoscript: hasGtmNoscript },
|
|
17512
|
-
|
|
17513
|
-
|
|
17637
|
+
otherAnalytics,
|
|
17638
|
+
gsc: { verified: hasGscVerification, verificationCode: gscMeta, source: gscSource },
|
|
17639
|
+
bing: { verified: hasBingVerification, verificationCode: bingMeta, source: bingSource },
|
|
17640
|
+
dnsChecked: verification.dnsChecked,
|
|
17514
17641
|
schemaOrg: { detected: hasJsonLd || hasMicrodata, types: schemaTypes, hasValidJson },
|
|
17515
17642
|
sitemap: { inRobotsTxt: false }
|
|
17516
17643
|
// Set later if we check robots.txt
|
|
17517
17644
|
};
|
|
17518
|
-
if (!hasGa4 && !hasGtm) {
|
|
17645
|
+
if (!hasGa4 && !hasGtm && otherAnalytics.length === 0) {
|
|
17519
17646
|
issues.push({
|
|
17520
17647
|
code: "GA4_MISSING",
|
|
17521
|
-
|
|
17648
|
+
// A notice: which analytics a site runs is a choice, not an SEO defect.
|
|
17649
|
+
severity: "notice",
|
|
17522
17650
|
category: "on-page",
|
|
17523
17651
|
title: "Google Analytics 4 not detected",
|
|
17524
17652
|
description: "No GA4 tracking code found. GA4 is essential for measuring organic traffic and conversions.",
|
|
@@ -17528,40 +17656,47 @@ function analyzeTrackingVerification(html, url) {
|
|
|
17528
17656
|
});
|
|
17529
17657
|
}
|
|
17530
17658
|
if (!hasGscVerification) {
|
|
17659
|
+
const dnsChecked = verification.dnsChecked;
|
|
17531
17660
|
issues.push({
|
|
17532
17661
|
code: "GSC_VERIFICATION_MISSING",
|
|
17533
|
-
|
|
17662
|
+
// A notice: a site verified by DNS, Google Analytics or Tag Manager leaves no trace here.
|
|
17663
|
+
severity: "notice",
|
|
17534
17664
|
category: "on-page",
|
|
17535
|
-
title: "Google Search Console verification not found",
|
|
17536
|
-
description: "No GSC verification meta tag
|
|
17665
|
+
title: dnsChecked ? "Google Search Console verification not found" : "Google Search Console verification not visible in the HTML",
|
|
17666
|
+
description: dnsChecked ? "No google-site-verification meta tag in the HTML and no google-site-verification TXT record on the domain. GSC provides essential data about your search performance." : "No google-site-verification meta tag in the HTML. The site may be verified through a DNS TXT record instead, which this run could not check. GSC provides essential data about your search performance.",
|
|
17537
17667
|
impact: "Missing critical insights into Google search visibility, impressions, and clicks.",
|
|
17538
|
-
howToFix: "
|
|
17539
|
-
affectedUrls: [url]
|
|
17668
|
+
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.",
|
|
17669
|
+
affectedUrls: [url],
|
|
17670
|
+
details: { dnsChecked }
|
|
17540
17671
|
});
|
|
17541
17672
|
}
|
|
17542
17673
|
if (!hasBingVerification) {
|
|
17674
|
+
const dnsChecked = verification.dnsChecked;
|
|
17543
17675
|
issues.push({
|
|
17544
17676
|
code: "BING_VERIFICATION_MISSING",
|
|
17545
17677
|
severity: "notice",
|
|
17546
17678
|
category: "on-page",
|
|
17547
|
-
title: "Bing Webmaster Tools verification not found",
|
|
17548
|
-
description: "No Bing
|
|
17679
|
+
title: dnsChecked ? "Bing Webmaster Tools verification not found" : "Bing Webmaster Tools verification not visible in the HTML",
|
|
17680
|
+
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.",
|
|
17549
17681
|
impact: "Missing insights on Bing/Yahoo search traffic.",
|
|
17550
|
-
howToFix: "
|
|
17551
|
-
affectedUrls: [url]
|
|
17682
|
+
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.",
|
|
17683
|
+
affectedUrls: [url],
|
|
17684
|
+
details: { dnsChecked }
|
|
17552
17685
|
});
|
|
17553
17686
|
}
|
|
17554
17687
|
if (!hasGtm) {
|
|
17555
|
-
|
|
17556
|
-
|
|
17557
|
-
|
|
17558
|
-
|
|
17559
|
-
|
|
17560
|
-
|
|
17561
|
-
|
|
17562
|
-
|
|
17563
|
-
|
|
17564
|
-
|
|
17688
|
+
if (otherAnalytics.length === 0) {
|
|
17689
|
+
issues.push({
|
|
17690
|
+
code: "GTM_MISSING",
|
|
17691
|
+
severity: "notice",
|
|
17692
|
+
category: "on-page",
|
|
17693
|
+
title: "Google Tag Manager not detected",
|
|
17694
|
+
description: "GTM not found. While not required, GTM makes it easier to manage tracking tags without code changes.",
|
|
17695
|
+
impact: "Tag management requires developer involvement for each change.",
|
|
17696
|
+
howToFix: "Consider using GTM for easier tag management. It allows marketing to add tags without developer help.",
|
|
17697
|
+
affectedUrls: [url]
|
|
17698
|
+
});
|
|
17699
|
+
}
|
|
17565
17700
|
} else if (!hasGtmNoscript) {
|
|
17566
17701
|
issues.push({
|
|
17567
17702
|
code: "GTM_NOSCRIPT_MISSING",
|
|
@@ -17600,6 +17735,113 @@ function analyzeTrackingVerification(html, url) {
|
|
|
17600
17735
|
return { issues, data };
|
|
17601
17736
|
}
|
|
17602
17737
|
|
|
17738
|
+
// src/audit/meta-refresh.ts
|
|
17739
|
+
var cheerio37 = __toESM(require("cheerio"));
|
|
17740
|
+
function metaRefreshTarget(html, pageUrl) {
|
|
17741
|
+
if (!/http-equiv\s*=\s*["']?refresh/i.test(html)) return null;
|
|
17742
|
+
const $ = cheerio37.load(html);
|
|
17743
|
+
for (const el of $("meta[http-equiv]").toArray()) {
|
|
17744
|
+
if (($(el).attr("http-equiv") || "").trim().toLowerCase() !== "refresh") continue;
|
|
17745
|
+
const content = $(el).attr("content") || "";
|
|
17746
|
+
const match = content.match(/^\s*(\d+(?:\.\d+)?)\s*[;,]\s*(?:url\s*=\s*)?['"]?\s*([^'"\s>]+)/i);
|
|
17747
|
+
if (!match) continue;
|
|
17748
|
+
if (Number(match[1]) > 5) continue;
|
|
17749
|
+
try {
|
|
17750
|
+
const target = new URL(match[2], pageUrl);
|
|
17751
|
+
if (target.protocol !== "http:" && target.protocol !== "https:") continue;
|
|
17752
|
+
const strip = (u) => u.origin + (u.pathname.replace(/\/+$/, "") || "/") + u.search;
|
|
17753
|
+
if (strip(target) === strip(new URL(pageUrl))) continue;
|
|
17754
|
+
return target.toString();
|
|
17755
|
+
} catch {
|
|
17756
|
+
continue;
|
|
17757
|
+
}
|
|
17758
|
+
}
|
|
17759
|
+
return null;
|
|
17760
|
+
}
|
|
17761
|
+
|
|
17762
|
+
// src/audit/dedupe.ts
|
|
17763
|
+
var REDUNDANT_WHEN_PRESENT = [
|
|
17764
|
+
// The same tag, reported by three checks.
|
|
17765
|
+
{ keep: "VIEWPORT_MISSING", drop: ["HTML_NO_VIEWPORT", "RESPONSIVE_NO_VIEWPORT"] },
|
|
17766
|
+
{ keep: "LANG_ATTR_MISSING", drop: ["HTML_NO_LANG"] },
|
|
17767
|
+
{ keep: "APPLE_TOUCH_ICON_MISSING", drop: ["HTML_NO_APPLE_TOUCH_ICON"] },
|
|
17768
|
+
{ keep: "LLMS_TXT_MISSING", drop: ["AI_NO_LLMS_TXT"] },
|
|
17769
|
+
{ keep: "INDEXNOW_NOT_IMPLEMENTED", drop: ["BING_NO_INDEXNOW"] },
|
|
17770
|
+
{ keep: "BING_VERIFICATION_MISSING", drop: ["BING_NOT_VERIFIED"] },
|
|
17771
|
+
{ keep: "SITEMAP_MISSING", drop: ["BING_SITEMAP_MISSING", "SITEMAP_NOT_IN_ROBOTS"] },
|
|
17772
|
+
{ keep: "ROBOTS_TXT_MISSING", drop: ["SITEMAP_NOT_IN_ROBOTS"] },
|
|
17773
|
+
{ keep: "IMAGES_MISSING_DIMENSIONS", drop: ["IMG_NO_DIMENSIONS"] },
|
|
17774
|
+
{ keep: "NO_MODERN_IMAGE_FORMATS", drop: ["IMAGE_NOT_WEBP_AVIF"] },
|
|
17775
|
+
{ keep: "SOCIAL_PROFILES_NOT_LINKED", drop: ["NO_SOCIAL_PROFILES"] },
|
|
17776
|
+
{ keep: "NO_EXPERIENCE_SIGNALS", drop: ["AI_NO_EXPERIENCE_SIGNALS"] },
|
|
17777
|
+
// No structured data at all makes every "your schema lacks X" finding moot.
|
|
17778
|
+
{
|
|
17779
|
+
keep: "SCHEMA_MISSING",
|
|
17780
|
+
drop: [
|
|
17781
|
+
"SCHEMA_ORG_MISSING",
|
|
17782
|
+
"NO_ORGANIZATION_SCHEMA",
|
|
17783
|
+
"ORGANIZATION_SCHEMA_MISSING",
|
|
17784
|
+
"NO_ENTITY_SCHEMA",
|
|
17785
|
+
"NO_DATE_MODIFIED_SCHEMA",
|
|
17786
|
+
"NO_MAIN_ENTITY_OF_PAGE",
|
|
17787
|
+
"AUTHOR_NO_SCHEMA",
|
|
17788
|
+
"AI_NO_REVIEW_SCHEMA",
|
|
17789
|
+
"NO_SCHEMA_SAMEAS"
|
|
17790
|
+
]
|
|
17791
|
+
},
|
|
17792
|
+
{ keep: "NO_ORGANIZATION_SCHEMA", drop: ["ORGANIZATION_SCHEMA_MISSING"] },
|
|
17793
|
+
// No title, no H1: the findings about their wording are implied.
|
|
17794
|
+
{ keep: "TITLE_MISSING", drop: ["LOW_CTR_POTENTIAL", "NO_KEYWORDS_IN_TITLE", "TITLE_TOO_SHORT"] },
|
|
17795
|
+
{ keep: "META_DESC_MISSING", drop: ["LOW_CTR_POTENTIAL"] },
|
|
17796
|
+
{ keep: "H1_MISSING", drop: ["NO_VISIBLE_HEADLINE", "NO_KEYWORDS_IN_H1", "H1_MISSING_KEYWORD"] },
|
|
17797
|
+
// Client-side rendering, diagnosed once. CLIENT_SIDE_RENDERING carries the per-framework fix.
|
|
17798
|
+
{
|
|
17799
|
+
keep: "CLIENT_SIDE_RENDERING",
|
|
17800
|
+
drop: [
|
|
17801
|
+
"REACT_CSR_DETECTED",
|
|
17802
|
+
"VUE_CSR_DETECTED",
|
|
17803
|
+
"ANGULAR_NO_UNIVERSAL",
|
|
17804
|
+
"SPA_WITHOUT_SSR",
|
|
17805
|
+
"EMPTY_BODY_CONTENT",
|
|
17806
|
+
"HIGH_JS_RENDERING_RATIO",
|
|
17807
|
+
"AI_LOW_HTML_READABILITY"
|
|
17808
|
+
]
|
|
17809
|
+
},
|
|
17810
|
+
// An empty body has no vocabulary to be undiverse.
|
|
17811
|
+
{ keep: "EMPTY_BODY_CONTENT", drop: ["THIN_CONTENT", "LOW_CONTENT_ENTROPY", "HIGH_CONTENT_REPETITION", "LOW_DWELL_TIME_CONTENT", "LOW_TEXT_HTML_RATIO"] },
|
|
17812
|
+
{ keep: "THIN_CONTENT", drop: ["LOW_DWELL_TIME_CONTENT"] },
|
|
17813
|
+
{ keep: "NO_INTERNAL_LINKS", drop: ["NO_CONTENT_INTERNAL_LINKS", "HIGH_ORPHAN_RISK", "LINKS_NO_INTERNAL"] },
|
|
17814
|
+
// No testimonials on the page, said once instead of six times.
|
|
17815
|
+
{ keep: "NO_SOCIAL_PROOF", drop: ["BOFU_NO_SOCIAL_PROOF", "AI_NO_TESTIMONIALS", "AI_NO_REVIEW_INTEGRATION", "AI_NO_REVIEW_PLATFORMS", "AI_WEAK_REVIEW_ECOSYSTEM"] },
|
|
17816
|
+
{ keep: "AI_NO_TESTIMONIALS", drop: ["AI_NO_REVIEW_INTEGRATION", "AI_NO_REVIEW_PLATFORMS", "AI_WEAK_REVIEW_ECOSYSTEM"] },
|
|
17817
|
+
// The brand-score composite restates the brand-definition finding it is built from.
|
|
17818
|
+
{ keep: "AI_NO_BRAND_DEFINITION", drop: ["AI_LOW_BRAND_SCORE", "NO_ENTITY_DEFINITION"] }
|
|
17819
|
+
];
|
|
17820
|
+
function dedupeIssues(issues) {
|
|
17821
|
+
const present = new Set(issues.map((i) => i.code));
|
|
17822
|
+
const redundant = /* @__PURE__ */ new Set();
|
|
17823
|
+
for (const rule of REDUNDANT_WHEN_PRESENT) {
|
|
17824
|
+
const fires = typeof rule.keep === "string" ? present.has(rule.keep) : [...present].some((code) => rule.keep.test(code));
|
|
17825
|
+
if (!fires) continue;
|
|
17826
|
+
for (const code of rule.drop) redundant.add(code);
|
|
17827
|
+
}
|
|
17828
|
+
const kept = /* @__PURE__ */ new Map();
|
|
17829
|
+
const out = [];
|
|
17830
|
+
for (const issue of issues) {
|
|
17831
|
+
if (redundant.has(issue.code)) continue;
|
|
17832
|
+
const earlier = kept.get(issue.code);
|
|
17833
|
+
if (earlier) {
|
|
17834
|
+
const urls = /* @__PURE__ */ new Set([...earlier.affectedUrls || [], ...issue.affectedUrls || []]);
|
|
17835
|
+
earlier.affectedUrls = [...urls];
|
|
17836
|
+
continue;
|
|
17837
|
+
}
|
|
17838
|
+
const copy = { ...issue };
|
|
17839
|
+
kept.set(issue.code, copy);
|
|
17840
|
+
out.push(copy);
|
|
17841
|
+
}
|
|
17842
|
+
return out;
|
|
17843
|
+
}
|
|
17844
|
+
|
|
17603
17845
|
// src/audit/engine.ts
|
|
17604
17846
|
function sameUrl(a, b) {
|
|
17605
17847
|
try {
|
|
@@ -17612,15 +17854,15 @@ function sameUrl(a, b) {
|
|
|
17612
17854
|
}
|
|
17613
17855
|
}
|
|
17614
17856
|
async function runFullAudit(options) {
|
|
17615
|
-
const {
|
|
17857
|
+
const { checkBrokenLinks = false, checkHreflangUrls = false, checkCanonicalChain = false, cruxApiKey, checksLimit = 280, premiumChecks } = options;
|
|
17858
|
+
let url = options.url;
|
|
17616
17859
|
const auditRoot = isSiteRoot(url) || sameUrl(url, options.auditRootUrl ?? url);
|
|
17617
17860
|
const allIssues = [];
|
|
17618
17861
|
const pages = [];
|
|
17619
17862
|
const tier = checksLimit <= 50 ? "free" : checksLimit <= 100 ? "registered" : "paid";
|
|
17620
17863
|
const runPremiumChecks = tier === "paid" && !!premiumChecks;
|
|
17621
17864
|
const runExtendedChecks = tier !== "free";
|
|
17622
|
-
|
|
17623
|
-
const domain = parsedUrl.hostname;
|
|
17865
|
+
let domain = new URL(url).hostname;
|
|
17624
17866
|
console.error(`
|
|
17625
17867
|
\u{1F50D} Running comprehensive SEO audit on ${url}...
|
|
17626
17868
|
`);
|
|
@@ -17647,7 +17889,9 @@ async function runFullAudit(options) {
|
|
|
17647
17889
|
timeout: 3e4,
|
|
17648
17890
|
validateStatus: () => true
|
|
17649
17891
|
}).catch((err) => {
|
|
17650
|
-
|
|
17892
|
+
const cause = err instanceof Error ? err.cause : void 0;
|
|
17893
|
+
const causeText = cause instanceof Error ? ` (${cause.code || cause.message})` : "";
|
|
17894
|
+
console.error(`Main page fetch failed: ${err instanceof Error ? err.message : String(err)}${causeText}`);
|
|
17651
17895
|
return { error: err, data: "", headers: {} };
|
|
17652
17896
|
})
|
|
17653
17897
|
]);
|
|
@@ -17670,7 +17914,7 @@ async function runFullAudit(options) {
|
|
|
17670
17914
|
});
|
|
17671
17915
|
return createReport(url, domain, allIssues, pages, { kind: "unreachable" });
|
|
17672
17916
|
}
|
|
17673
|
-
|
|
17917
|
+
let statusCode = fetchResult.status;
|
|
17674
17918
|
if (statusCode >= 400) {
|
|
17675
17919
|
const blocked = statusCode === 401 || statusCode === 403 || statusCode === 429;
|
|
17676
17920
|
allIssues.push({
|
|
@@ -17683,10 +17927,42 @@ async function runFullAudit(options) {
|
|
|
17683
17927
|
pages.push({ url, statusCode, wordCount: 0, loadTime: 0, issues: allIssues.map((i) => i.code) });
|
|
17684
17928
|
return createReport(url, domain, allIssues, pages, { kind: "http-error", statusCode });
|
|
17685
17929
|
}
|
|
17686
|
-
const
|
|
17930
|
+
const asText = (data) => typeof data === "string" ? data : JSON.stringify(data ?? "");
|
|
17931
|
+
let body = asText(fetchResult.data);
|
|
17932
|
+
let headers = fetchResult.headers;
|
|
17933
|
+
const requestedUrl = url;
|
|
17934
|
+
const refreshHops = [];
|
|
17935
|
+
for (let hop = 0; hop < 3; hop++) {
|
|
17936
|
+
const target = metaRefreshTarget(body, url);
|
|
17937
|
+
if (!target) break;
|
|
17938
|
+
try {
|
|
17939
|
+
const next = await httpGet(target, { timeout: 3e4, validateStatus: () => true });
|
|
17940
|
+
if (next.status >= 400) break;
|
|
17941
|
+
refreshHops.push(target);
|
|
17942
|
+
url = target;
|
|
17943
|
+
body = asText(next.data);
|
|
17944
|
+
headers = next.headers;
|
|
17945
|
+
statusCode = next.status;
|
|
17946
|
+
} catch {
|
|
17947
|
+
break;
|
|
17948
|
+
}
|
|
17949
|
+
}
|
|
17950
|
+
if (refreshHops.length > 0) {
|
|
17951
|
+
domain = new URL(url).hostname;
|
|
17952
|
+
allIssues.push({
|
|
17953
|
+
code: "META_REFRESH_REDIRECT",
|
|
17954
|
+
severity: "notice",
|
|
17955
|
+
category: "crawlability",
|
|
17956
|
+
title: "Page redirects with a meta refresh",
|
|
17957
|
+
description: `${requestedUrl} answers HTTP 200 with a <meta http-equiv="refresh"> to ${url}, so that page was audited. Search engines follow an instant meta refresh, but a server redirect is the documented way to move a URL and the only one every crawler and tool honours.`,
|
|
17958
|
+
impact: "Some crawlers and link-checking tools stop at the stub; link signals to the original URL may not all pass to the target.",
|
|
17959
|
+
howToFix: "Return a 301 (or 308) redirect from the server or CDN for this URL instead of a meta refresh.",
|
|
17960
|
+
affectedUrls: [requestedUrl],
|
|
17961
|
+
details: { from: requestedUrl, to: url, hops: refreshHops }
|
|
17962
|
+
});
|
|
17963
|
+
}
|
|
17687
17964
|
const html = body.replace(/<\/?noscript[^>]*>/gi, "");
|
|
17688
|
-
|
|
17689
|
-
console.error(`\u{1F4DD} Phase 2: Running synchronous HTML checks (tier: ${tier}, limit: ${checksLimit})...`);
|
|
17965
|
+
console.error("\u{1F4DD} Phase 2: Running HTML checks...");
|
|
17690
17966
|
const onPageResult = analyzeOnPage(html, url, headers, { auditRoot });
|
|
17691
17967
|
const structuredDataResult = analyzeStructuredData(html, url);
|
|
17692
17968
|
const mobileResult = analyzeMobile(html, url);
|
|
@@ -17701,7 +17977,6 @@ async function runFullAudit(options) {
|
|
|
17701
17977
|
const urlSafetyResult = analyzeUrlSafety(url);
|
|
17702
17978
|
const domSizeResult = analyzeDomSize(html, url);
|
|
17703
17979
|
const imageDimensionsResult = analyzeImageDimensions(html, url);
|
|
17704
|
-
const trackingResult = analyzeTrackingVerification(html, url);
|
|
17705
17980
|
const anchorResult = runExtendedChecks ? analyzeAnchorText(html, url) : { issues: [], data: {} };
|
|
17706
17981
|
const localSeoResult = runExtendedChecks ? analyzeLocalSEO(html, url) : { issues: [], data: {} };
|
|
17707
17982
|
const modernImageResult = runExtendedChecks ? analyzeModernImages(html, url) : { issues: [], data: {} };
|
|
@@ -17776,8 +18051,7 @@ async function runFullAudit(options) {
|
|
|
17776
18051
|
...colorContrastResult.issues,
|
|
17777
18052
|
...pageResourcesResult.issues,
|
|
17778
18053
|
...urlSafetyResult.issues,
|
|
17779
|
-
...paginationResult.issues
|
|
17780
|
-
...trackingResult.issues
|
|
18054
|
+
...paginationResult.issues
|
|
17781
18055
|
);
|
|
17782
18056
|
if (!structuredDataResult.data.hasSchema) {
|
|
17783
18057
|
const suggestions = suggestSchemaTypes(html, url);
|
|
@@ -17823,6 +18097,7 @@ async function runFullAudit(options) {
|
|
|
17823
18097
|
asyncChecks.push(safeAsyncIssues("Certificate", async () => (await checkCertificate(url)).issues));
|
|
17824
18098
|
asyncChecks.push(safeAsync("Redirects", () => analyzeRedirects(url)));
|
|
17825
18099
|
asyncChecks.push(safeAsync("Robots Validation", () => validateRobotsTxt(url)));
|
|
18100
|
+
asyncChecks.push(safeAsync("Tracking Verification", () => analyzeTrackingVerification(html, url)));
|
|
17826
18101
|
asyncChecks.push(safeAsyncIssues("Crawler Access", async () => (await analyzeCrawlerAccess(url)).issues));
|
|
17827
18102
|
asyncChecks.push(safeAsync("Canonical Domain", () => analyzeCanonicalDomain(url)));
|
|
17828
18103
|
asyncChecks.push(safeAsyncIssues("Framework Detection", async () => {
|
|
@@ -17854,10 +18129,10 @@ async function runFullAudit(options) {
|
|
|
17854
18129
|
if (cruxApiKey) {
|
|
17855
18130
|
asyncChecks.push(safeAsyncIssues("Core Web Vitals", async () => {
|
|
17856
18131
|
const cwv = await fetchCoreWebVitals(url, cruxApiKey);
|
|
17857
|
-
const
|
|
18132
|
+
const issues2 = [];
|
|
17858
18133
|
if (cwv) {
|
|
17859
18134
|
if (cwv.lcp?.rating === "poor") {
|
|
17860
|
-
|
|
18135
|
+
issues2.push({
|
|
17861
18136
|
code: "LCP_POOR",
|
|
17862
18137
|
severity: "error",
|
|
17863
18138
|
category: "performance",
|
|
@@ -17870,7 +18145,7 @@ async function runFullAudit(options) {
|
|
|
17870
18145
|
});
|
|
17871
18146
|
}
|
|
17872
18147
|
if (cwv.cls?.rating === "poor") {
|
|
17873
|
-
|
|
18148
|
+
issues2.push({
|
|
17874
18149
|
code: "CLS_POOR",
|
|
17875
18150
|
severity: "error",
|
|
17876
18151
|
category: "performance",
|
|
@@ -17883,14 +18158,15 @@ async function runFullAudit(options) {
|
|
|
17883
18158
|
});
|
|
17884
18159
|
}
|
|
17885
18160
|
}
|
|
17886
|
-
return
|
|
18161
|
+
return issues2;
|
|
17887
18162
|
}));
|
|
17888
18163
|
}
|
|
17889
18164
|
}
|
|
17890
18165
|
const asyncResults = await Promise.all(asyncChecks);
|
|
17891
|
-
for (const
|
|
17892
|
-
allIssues.push(...
|
|
18166
|
+
for (const issues2 of asyncResults) {
|
|
18167
|
+
allIssues.push(...issues2);
|
|
17893
18168
|
}
|
|
18169
|
+
const issues = dedupeIssues(allIssues);
|
|
17894
18170
|
pages.push({
|
|
17895
18171
|
url,
|
|
17896
18172
|
statusCode,
|
|
@@ -17900,10 +18176,10 @@ async function runFullAudit(options) {
|
|
|
17900
18176
|
h1: onPageResult.data.h1s,
|
|
17901
18177
|
wordCount: onPageResult.data.wordCount,
|
|
17902
18178
|
loadTime: perfData.loadTime,
|
|
17903
|
-
issues:
|
|
18179
|
+
issues: issues.map((i) => i.code)
|
|
17904
18180
|
});
|
|
17905
18181
|
console.error("\n\u2705 Audit complete!\n");
|
|
17906
|
-
return createReport(url, domain,
|
|
18182
|
+
return createReport(url, domain, issues, pages);
|
|
17907
18183
|
}
|
|
17908
18184
|
function createReport(url, domain, issues, pages, pageFailure) {
|
|
17909
18185
|
const healthScore = calculateHealthScore(issues);
|
|
@@ -18068,10 +18344,10 @@ function groupIssuesByCategory(issues) {
|
|
|
18068
18344
|
}
|
|
18069
18345
|
|
|
18070
18346
|
// src/audit/checks/duplicate-content.ts
|
|
18071
|
-
var
|
|
18347
|
+
var cheerio38 = __toESM(require("cheerio"));
|
|
18072
18348
|
var import_crypto = require("crypto");
|
|
18073
18349
|
function extractContentHash(html, url) {
|
|
18074
|
-
const $ =
|
|
18350
|
+
const $ = cheerio38.load(html);
|
|
18075
18351
|
$("script, style, nav, header, footer, aside, .nav, .header, .footer, .sidebar").remove();
|
|
18076
18352
|
const title = $("title").text().trim();
|
|
18077
18353
|
const bodyText = $("body").text().replace(/\s+/g, " ").trim();
|
|
@@ -19299,7 +19575,7 @@ var PRIORITY_WEIGHTS = {
|
|
|
19299
19575
|
};
|
|
19300
19576
|
|
|
19301
19577
|
// src/keywords/engine.ts
|
|
19302
|
-
var
|
|
19578
|
+
var cheerio40 = __toESM(require("cheerio"));
|
|
19303
19579
|
init_http();
|
|
19304
19580
|
|
|
19305
19581
|
// src/keywords/prioritizer.ts
|
|
@@ -19596,7 +19872,7 @@ function enrichKeywordsWithEstimates(keywords) {
|
|
|
19596
19872
|
|
|
19597
19873
|
// src/keywords/sources/free-sources.ts
|
|
19598
19874
|
init_http();
|
|
19599
|
-
var
|
|
19875
|
+
var cheerio39 = __toESM(require("cheerio"));
|
|
19600
19876
|
var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
|
|
19601
19877
|
async function getPeopleAlsoAsk(query) {
|
|
19602
19878
|
try {
|
|
@@ -19605,7 +19881,7 @@ async function getPeopleAlsoAsk(query) {
|
|
|
19605
19881
|
headers: { "User-Agent": USER_AGENT },
|
|
19606
19882
|
timeout: 1e4
|
|
19607
19883
|
});
|
|
19608
|
-
const $ =
|
|
19884
|
+
const $ = cheerio39.load(response.data);
|
|
19609
19885
|
const questions = [];
|
|
19610
19886
|
$("[data-sgrd]").each((_, el) => {
|
|
19611
19887
|
const text = $(el).text().trim();
|
|
@@ -19631,7 +19907,7 @@ async function getRelatedSearches(query) {
|
|
|
19631
19907
|
headers: { "User-Agent": USER_AGENT },
|
|
19632
19908
|
timeout: 1e4
|
|
19633
19909
|
});
|
|
19634
|
-
const $ =
|
|
19910
|
+
const $ = cheerio39.load(response.data);
|
|
19635
19911
|
const related = [];
|
|
19636
19912
|
$("div[data-ved] a").each((_, el) => {
|
|
19637
19913
|
const href = $(el).attr("href");
|
|
@@ -19721,7 +19997,7 @@ async function analyzeCompetitorTitles(query) {
|
|
|
19721
19997
|
headers: { "User-Agent": USER_AGENT },
|
|
19722
19998
|
timeout: 1e4
|
|
19723
19999
|
});
|
|
19724
|
-
const $ =
|
|
20000
|
+
const $ = cheerio39.load(response.data);
|
|
19725
20001
|
const titles = [];
|
|
19726
20002
|
const keywords = /* @__PURE__ */ new Set();
|
|
19727
20003
|
$("h3").each((_, el) => {
|
|
@@ -20054,7 +20330,7 @@ async function fetchPageMeta(url) {
|
|
|
20054
20330
|
const response = await httpGet(url, {
|
|
20055
20331
|
timeout: 1e4
|
|
20056
20332
|
});
|
|
20057
|
-
const $ =
|
|
20333
|
+
const $ = cheerio40.load(response.data);
|
|
20058
20334
|
return {
|
|
20059
20335
|
url,
|
|
20060
20336
|
title: $("title").text().trim() || void 0,
|
|
@@ -20185,20 +20461,17 @@ async function extractSeedKeywords(url) {
|
|
|
20185
20461
|
const response = await httpGet(url, {
|
|
20186
20462
|
timeout: 1e4
|
|
20187
20463
|
});
|
|
20188
|
-
const $ =
|
|
20464
|
+
const $ = cheerio40.load(response.data);
|
|
20189
20465
|
const seeds = /* @__PURE__ */ new Set();
|
|
20190
|
-
const
|
|
20191
|
-
|
|
20192
|
-
|
|
20193
|
-
const h1 = $("h1").first().text().toLowerCase();
|
|
20194
|
-
const h1Words = h1.split(/[\s\-|:]+/).filter((w) => w.length > 3);
|
|
20195
|
-
h1Words.forEach((w) => seeds.add(w));
|
|
20466
|
+
const words = (text) => text.toLowerCase().split(/[\s\-|:]+/).map((w) => w.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "")).filter((w) => w.length > 3);
|
|
20467
|
+
words($("title").text()).forEach((w) => seeds.add(w));
|
|
20468
|
+
words($("h1").first().text()).forEach((w) => seeds.add(w));
|
|
20196
20469
|
const metaKeywords = $('meta[name="keywords"]').attr("content");
|
|
20197
20470
|
if (metaKeywords) {
|
|
20198
20471
|
metaKeywords.split(",").forEach((k) => seeds.add(k.trim().toLowerCase()));
|
|
20199
20472
|
}
|
|
20200
20473
|
const desc = $('meta[name="description"]').attr("content")?.toLowerCase() || "";
|
|
20201
|
-
const descWords = desc
|
|
20474
|
+
const descWords = words(desc).filter((w) => w.length > 5);
|
|
20202
20475
|
descWords.slice(0, 5).forEach((w) => seeds.add(w));
|
|
20203
20476
|
return Array.from(seeds).slice(0, 10);
|
|
20204
20477
|
} catch {
|
|
@@ -20251,7 +20524,7 @@ function parseJsonReply(text) {
|
|
|
20251
20524
|
}
|
|
20252
20525
|
|
|
20253
20526
|
// src/keywords/site-crawler.ts
|
|
20254
|
-
var
|
|
20527
|
+
var cheerio41 = __toESM(require("cheerio"));
|
|
20255
20528
|
init_http();
|
|
20256
20529
|
var EXCLUDED_PATHS = [
|
|
20257
20530
|
"/cdn-cgi/",
|
|
@@ -20364,7 +20637,7 @@ async function crawlPage(url, timeout) {
|
|
|
20364
20637
|
validateStatus: (status) => status === 200
|
|
20365
20638
|
});
|
|
20366
20639
|
const html = response.data;
|
|
20367
|
-
const $ =
|
|
20640
|
+
const $ = cheerio41.load(html);
|
|
20368
20641
|
$('script, style, noscript, iframe, nav, footer, header, aside, [role="navigation"]').remove();
|
|
20369
20642
|
const title = $("title").text().trim();
|
|
20370
20643
|
const description = $('meta[name="description"]').attr("content")?.trim() || "";
|
|
@@ -23000,7 +23273,7 @@ function getDateRange(days = 28) {
|
|
|
23000
23273
|
|
|
23001
23274
|
// src/keywords/sources/competitor-analysis.ts
|
|
23002
23275
|
init_http();
|
|
23003
|
-
var
|
|
23276
|
+
var cheerio42 = __toESM(require("cheerio"));
|
|
23004
23277
|
var USER_AGENT2 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
|
|
23005
23278
|
async function discoverCompetitorKeywords(yourDomain, seedKeywords, competitors) {
|
|
23006
23279
|
const yourKeywords = /* @__PURE__ */ new Set();
|
|
@@ -23097,7 +23370,7 @@ async function analyzeSERP(query) {
|
|
|
23097
23370
|
headers: { "User-Agent": USER_AGENT2 },
|
|
23098
23371
|
timeout: 1e4
|
|
23099
23372
|
});
|
|
23100
|
-
const $ =
|
|
23373
|
+
const $ = cheerio42.load(response.data);
|
|
23101
23374
|
const results = [];
|
|
23102
23375
|
const relatedSearches = [];
|
|
23103
23376
|
const peopleAlsoAsk = [];
|
|
@@ -23550,7 +23823,7 @@ function formatTopicReport(result) {
|
|
|
23550
23823
|
|
|
23551
23824
|
// src/content/readability.ts
|
|
23552
23825
|
function analyzeReadability(text) {
|
|
23553
|
-
const cleanText = text.replace(/<[^>]*>/g, " ").replace(
|
|
23826
|
+
const cleanText = text.replace(/<[^>]*>/g, " ").replace(/[ \t\r\f\v]+/g, " ").replace(/ ?\n ?/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
23554
23827
|
const words = getWords(cleanText);
|
|
23555
23828
|
const sentences = getSentences(cleanText);
|
|
23556
23829
|
const paragraphs = getParagraphs(cleanText);
|
|
@@ -23619,7 +23892,7 @@ function getSentences(text) {
|
|
|
23619
23892
|
return text.split(/[.!?]+/).filter((s) => s.trim().length > 0);
|
|
23620
23893
|
}
|
|
23621
23894
|
function getParagraphs(text) {
|
|
23622
|
-
return text.split(/\n\n+/).filter((p) => p.trim().length
|
|
23895
|
+
return text.split(/\n\n+/).filter((p) => p.trim().split(/\s+/).length >= 3);
|
|
23623
23896
|
}
|
|
23624
23897
|
function countSyllables(word) {
|
|
23625
23898
|
word = word.toLowerCase().replace(/[^a-z]/g, "");
|
|
@@ -35904,6 +36177,7 @@ var SUPPORTED_FRAMEWORKS = [
|
|
|
35904
36177
|
LOCATION_CODES,
|
|
35905
36178
|
OG_IMAGE_SPECS,
|
|
35906
36179
|
PRIORITY_WEIGHTS,
|
|
36180
|
+
REDUNDANT_WHEN_PRESENT,
|
|
35907
36181
|
RankTracker,
|
|
35908
36182
|
SEO_SCOPES,
|
|
35909
36183
|
SITE_PROFILE_QUESTIONS,
|
|
@@ -35987,6 +36261,7 @@ var SUPPORTED_FRAMEWORKS = [
|
|
|
35987
36261
|
createPullRequest,
|
|
35988
36262
|
createSEOCommit,
|
|
35989
36263
|
createSEOCommits,
|
|
36264
|
+
dedupeIssues,
|
|
35990
36265
|
describeConcerns,
|
|
35991
36266
|
detectDuplicates,
|
|
35992
36267
|
detectFramework,
|
|
@@ -36123,6 +36398,7 @@ var SUPPORTED_FRAMEWORKS = [
|
|
|
36123
36398
|
loadAgent,
|
|
36124
36399
|
loadAgentByName,
|
|
36125
36400
|
mergePages,
|
|
36401
|
+
metaRefreshTarget,
|
|
36126
36402
|
openAIKeywordLLM,
|
|
36127
36403
|
optimizeForAI,
|
|
36128
36404
|
optimizeReadme,
|