@rankcli/agent-runtime 0.0.37 → 0.0.38
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.
|
@@ -25,7 +25,8 @@ async function httpGet(url, config = {}) {
|
|
|
25
25
|
timeout = DEFAULT_TIMEOUT,
|
|
26
26
|
validateStatus = (status) => status >= 200 && status < 300,
|
|
27
27
|
maxRedirects = 5,
|
|
28
|
-
params
|
|
28
|
+
params,
|
|
29
|
+
maxBytes
|
|
29
30
|
} = config;
|
|
30
31
|
let finalUrl = url;
|
|
31
32
|
if (params) {
|
|
@@ -52,7 +53,7 @@ async function httpGet(url, config = {}) {
|
|
|
52
53
|
if (contentType.includes("application/json")) {
|
|
53
54
|
data = await response.json();
|
|
54
55
|
} else {
|
|
55
|
-
data = await response
|
|
56
|
+
data = await readText(response, maxBytes);
|
|
56
57
|
}
|
|
57
58
|
clearTimeout(timeoutId);
|
|
58
59
|
if (!validateStatus(response.status)) {
|
|
@@ -79,6 +80,23 @@ async function httpGet(url, config = {}) {
|
|
|
79
80
|
throw error;
|
|
80
81
|
}
|
|
81
82
|
}
|
|
83
|
+
async function readText(response, maxBytes) {
|
|
84
|
+
if (!maxBytes || !response.body) return response.text();
|
|
85
|
+
const reader = response.body.getReader();
|
|
86
|
+
const chunks = [];
|
|
87
|
+
let received = 0;
|
|
88
|
+
while (received < maxBytes) {
|
|
89
|
+
const { done, value } = await reader.read();
|
|
90
|
+
if (done) break;
|
|
91
|
+
chunks.push(value);
|
|
92
|
+
received += value.byteLength;
|
|
93
|
+
}
|
|
94
|
+
if (received >= maxBytes) await reader.cancel().catch(() => void 0);
|
|
95
|
+
const decoder = new TextDecoder();
|
|
96
|
+
let text = "";
|
|
97
|
+
for (const chunk of chunks) text += decoder.decode(chunk, { stream: true });
|
|
98
|
+
return text + decoder.decode();
|
|
99
|
+
}
|
|
82
100
|
async function httpHead(url, config = {}) {
|
|
83
101
|
const {
|
|
84
102
|
headers = {},
|
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 = {},
|
|
@@ -9996,7 +10014,9 @@ function analyzeAnchorText(html, url) {
|
|
|
9996
10014
|
} catch {
|
|
9997
10015
|
internalLinks++;
|
|
9998
10016
|
}
|
|
9999
|
-
const
|
|
10017
|
+
const imgAlt = $(el).find("img[alt]").map((_2, img) => ($(img).attr("alt") || "").trim()).get().join(" ").trim();
|
|
10018
|
+
const svgTitle = $(el).find("svg title").text().trim();
|
|
10019
|
+
const hasAccessibleText = text.length > 0 || ariaLabel || title || imgAlt.length > 0 || svgTitle.length > 0;
|
|
10000
10020
|
if (!hasAccessibleText) {
|
|
10001
10021
|
emptyAnchors++;
|
|
10002
10022
|
if (emptyAnchorUrls.length < 5) {
|
|
@@ -14565,6 +14585,107 @@ function analyzeResponsiveImages(html, url) {
|
|
|
14565
14585
|
// src/audit/checks/bing-optimization.ts
|
|
14566
14586
|
var cheerio28 = __toESM(require("cheerio"));
|
|
14567
14587
|
init_http();
|
|
14588
|
+
|
|
14589
|
+
// src/audit/checks/site-verification.ts
|
|
14590
|
+
init_http();
|
|
14591
|
+
var UNCHECKED = {
|
|
14592
|
+
dnsChecked: false,
|
|
14593
|
+
googleDns: false,
|
|
14594
|
+
bingDns: false,
|
|
14595
|
+
bingSiteAuthFile: false
|
|
14596
|
+
};
|
|
14597
|
+
var DNS_TIMEOUT_MS = 2e3;
|
|
14598
|
+
var FILE_TIMEOUT_MS = 5e3;
|
|
14599
|
+
var CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
14600
|
+
var cache = /* @__PURE__ */ new Map();
|
|
14601
|
+
function lookupSiteVerification(url) {
|
|
14602
|
+
let parsed;
|
|
14603
|
+
try {
|
|
14604
|
+
parsed = new URL(url);
|
|
14605
|
+
} catch {
|
|
14606
|
+
return Promise.resolve(UNCHECKED);
|
|
14607
|
+
}
|
|
14608
|
+
const host = parsed.hostname.toLowerCase();
|
|
14609
|
+
const now = Date.now();
|
|
14610
|
+
const cached = cache.get(host);
|
|
14611
|
+
if (cached && cached.expires > now) return cached.result;
|
|
14612
|
+
const result = Promise.all([readTxtRecords(host), fetchBingSiteAuth(parsed.origin)]).then(
|
|
14613
|
+
([txt, bingSiteAuthFile]) => ({
|
|
14614
|
+
dnsChecked: txt.checked,
|
|
14615
|
+
googleDns: txt.records.some((record) => /^google-site-verification=/i.test(record)),
|
|
14616
|
+
bingDns: txt.records.some((record) => /^MS=/i.test(record)),
|
|
14617
|
+
bingSiteAuthFile
|
|
14618
|
+
})
|
|
14619
|
+
);
|
|
14620
|
+
cache.set(host, { expires: now + CACHE_TTL_MS, result });
|
|
14621
|
+
return result;
|
|
14622
|
+
}
|
|
14623
|
+
var resolveTxtLoader;
|
|
14624
|
+
function loadResolveTxt() {
|
|
14625
|
+
resolveTxtLoader ??= (async () => {
|
|
14626
|
+
if (typeof process === "undefined" || !process.versions?.node) return null;
|
|
14627
|
+
try {
|
|
14628
|
+
const specifier = "node:dns";
|
|
14629
|
+
const dns = await import(specifier);
|
|
14630
|
+
const resolveTxt = dns?.promises?.resolveTxt ?? dns?.default?.promises?.resolveTxt;
|
|
14631
|
+
return typeof resolveTxt === "function" ? resolveTxt : null;
|
|
14632
|
+
} catch {
|
|
14633
|
+
return null;
|
|
14634
|
+
}
|
|
14635
|
+
})();
|
|
14636
|
+
return resolveTxtLoader;
|
|
14637
|
+
}
|
|
14638
|
+
function domainCandidates(host) {
|
|
14639
|
+
if (/^[\d.]+$/.test(host) || host.includes(":")) return [];
|
|
14640
|
+
const labels = host.split(".").filter(Boolean);
|
|
14641
|
+
const names = [];
|
|
14642
|
+
for (let n = 2; n <= labels.length && names.length < 4; n++) {
|
|
14643
|
+
names.push(labels.slice(-n).join("."));
|
|
14644
|
+
}
|
|
14645
|
+
return names.length > 0 ? names : [host];
|
|
14646
|
+
}
|
|
14647
|
+
async function readTxtRecords(host) {
|
|
14648
|
+
const resolveTxt = await loadResolveTxt();
|
|
14649
|
+
if (!resolveTxt) return { checked: false, records: [] };
|
|
14650
|
+
const lookups = await Promise.all(domainCandidates(host).map((name) => resolveTxtWithin(resolveTxt, name)));
|
|
14651
|
+
return {
|
|
14652
|
+
checked: lookups.every((records) => records !== null),
|
|
14653
|
+
records: lookups.flatMap((records) => records ?? [])
|
|
14654
|
+
};
|
|
14655
|
+
}
|
|
14656
|
+
var NO_RECORD_CODES = /* @__PURE__ */ new Set(["ENODATA", "ENOTFOUND"]);
|
|
14657
|
+
async function resolveTxtWithin(resolveTxt, name) {
|
|
14658
|
+
let timer;
|
|
14659
|
+
const timeout = new Promise((resolve) => {
|
|
14660
|
+
timer = setTimeout(() => resolve(null), DNS_TIMEOUT_MS);
|
|
14661
|
+
});
|
|
14662
|
+
try {
|
|
14663
|
+
return await Promise.race([
|
|
14664
|
+
resolveTxt(name).then(
|
|
14665
|
+
(records) => records.map((chunks) => chunks.join("").trim()),
|
|
14666
|
+
(error) => NO_RECORD_CODES.has(error?.code ?? "") ? [] : null
|
|
14667
|
+
),
|
|
14668
|
+
timeout
|
|
14669
|
+
]);
|
|
14670
|
+
} catch {
|
|
14671
|
+
return null;
|
|
14672
|
+
} finally {
|
|
14673
|
+
clearTimeout(timer);
|
|
14674
|
+
}
|
|
14675
|
+
}
|
|
14676
|
+
async function fetchBingSiteAuth(origin) {
|
|
14677
|
+
try {
|
|
14678
|
+
const response = await httpGet(`${origin}/BingSiteAuth.xml`, {
|
|
14679
|
+
timeout: FILE_TIMEOUT_MS,
|
|
14680
|
+
maxBytes: 64 * 1024
|
|
14681
|
+
});
|
|
14682
|
+
return response.status === 200 && typeof response.data === "string" && !looksLikeHtmlFallback(response.data, response.headers["content-type"]) && /<users[\s>]/i.test(response.data);
|
|
14683
|
+
} catch {
|
|
14684
|
+
return false;
|
|
14685
|
+
}
|
|
14686
|
+
}
|
|
14687
|
+
|
|
14688
|
+
// src/audit/checks/bing-optimization.ts
|
|
14568
14689
|
async function analyzeBingOptimization(html, url) {
|
|
14569
14690
|
const issues = [];
|
|
14570
14691
|
const $ = cheerio28.load(html);
|
|
@@ -14573,12 +14694,9 @@ async function analyzeBingOptimization(html, url) {
|
|
|
14573
14694
|
const detectedIssues = [];
|
|
14574
14695
|
const bingMetaTag = $('meta[name="msvalidate.01"]').attr("content");
|
|
14575
14696
|
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
|
-
}
|
|
14697
|
+
const verification = await lookupSiteVerification(url);
|
|
14698
|
+
const hasBingSiteAuth = verification.bingSiteAuthFile;
|
|
14699
|
+
const hasBingDnsRecord = verification.bingDns;
|
|
14582
14700
|
let hasBingIndexNowKey = false;
|
|
14583
14701
|
try {
|
|
14584
14702
|
const keyPatterns = [
|
|
@@ -14626,22 +14744,25 @@ async function analyzeBingOptimization(html, url) {
|
|
|
14626
14744
|
bingSitemapAccessible = sitemapResponse.status === 200 && sitemapResponse.data.includes("<?xml");
|
|
14627
14745
|
} catch {
|
|
14628
14746
|
}
|
|
14629
|
-
if (!hasBingMetaTag && !hasBingSiteAuth) {
|
|
14747
|
+
if (!hasBingMetaTag && !hasBingSiteAuth && !hasBingDnsRecord) {
|
|
14630
14748
|
detectedIssues.push("No Bing site verification");
|
|
14631
14749
|
issues.push({
|
|
14632
14750
|
code: "BING_NOT_VERIFIED",
|
|
14633
14751
|
severity: "warning",
|
|
14634
14752
|
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.",
|
|
14753
|
+
title: verification.dnsChecked ? "Site not verified with Bing Webmaster Tools" : "Bing Webmaster Tools verification not visible in the HTML",
|
|
14754
|
+
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
14755
|
impact: "May reduce visibility in ChatGPT Search results and miss Bing-specific optimization opportunities.",
|
|
14638
|
-
howToFix: "
|
|
14756
|
+
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
14757
|
affectedUrls: [url],
|
|
14640
14758
|
details: {
|
|
14641
14759
|
hasBingMetaTag,
|
|
14642
14760
|
hasBingSiteAuth,
|
|
14761
|
+
hasBingDnsRecord,
|
|
14762
|
+
dnsChecked: verification.dnsChecked,
|
|
14643
14763
|
verificationMethods: [
|
|
14644
14764
|
'Add <meta name="msvalidate.01" content="YOUR_CODE"> to <head>',
|
|
14765
|
+
"Add an MS=YOUR_CODE TXT record to the domain's DNS",
|
|
14645
14766
|
"Upload BingSiteAuth.xml to root directory"
|
|
14646
14767
|
]
|
|
14647
14768
|
}
|
|
@@ -14701,6 +14822,8 @@ async function analyzeBingOptimization(html, url) {
|
|
|
14701
14822
|
data: {
|
|
14702
14823
|
hasBingSiteAuth,
|
|
14703
14824
|
hasBingMetaTag,
|
|
14825
|
+
hasBingDnsRecord,
|
|
14826
|
+
dnsChecked: verification.dnsChecked,
|
|
14704
14827
|
hasBingIndexNowKey,
|
|
14705
14828
|
bingPlacesDetected,
|
|
14706
14829
|
bingSitemapAccessible,
|
|
@@ -16686,6 +16809,10 @@ function analyzePageResources(html, url) {
|
|
|
16686
16809
|
// src/audit/checks/responsive-css.ts
|
|
16687
16810
|
init_http();
|
|
16688
16811
|
var cheerio35 = __toESM(require("cheerio"));
|
|
16812
|
+
var MAX_STYLESHEETS = 3;
|
|
16813
|
+
var MAX_STYLESHEET_BYTES = 1024 * 1024;
|
|
16814
|
+
var STYLESHEET_TIMEOUT_MS = 3e3;
|
|
16815
|
+
var RESPONSIVE_SIGNAL = /@media\b|@container\b|\bclamp\(|(?<![\w-])(?:min|max)\(/i;
|
|
16689
16816
|
function extractMediaQueries(css) {
|
|
16690
16817
|
const mediaQueries = [];
|
|
16691
16818
|
const regex = /@media\s*([^{]+)/g;
|
|
@@ -16695,36 +16822,31 @@ function extractMediaQueries(css) {
|
|
|
16695
16822
|
}
|
|
16696
16823
|
return mediaQueries;
|
|
16697
16824
|
}
|
|
16825
|
+
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
16826
|
function classifyBreakpoints(mediaQueries) {
|
|
16699
16827
|
const breakpoints = /* @__PURE__ */ new Set();
|
|
16700
16828
|
let hasMobile = false;
|
|
16701
16829
|
let hasTablet = false;
|
|
16702
16830
|
let hasDesktop = false;
|
|
16703
16831
|
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
16832
|
if (/print/i.test(query)) {
|
|
16726
16833
|
continue;
|
|
16727
16834
|
}
|
|
16835
|
+
for (const match of query.matchAll(WIDTH_RULE)) {
|
|
16836
|
+
const raw = match[1] ?? match[3];
|
|
16837
|
+
const unit = (match[2] ?? match[4] ?? "px").toLowerCase();
|
|
16838
|
+
const value = Math.round(parseFloat(raw) * (unit === "px" ? 1 : 16));
|
|
16839
|
+
breakpoints.add(`${value}px`);
|
|
16840
|
+
if (value <= 480) {
|
|
16841
|
+
hasMobile = true;
|
|
16842
|
+
} else if (value <= 768) {
|
|
16843
|
+
hasMobile = true;
|
|
16844
|
+
} else if (value <= 1024) {
|
|
16845
|
+
hasTablet = true;
|
|
16846
|
+
} else {
|
|
16847
|
+
hasDesktop = true;
|
|
16848
|
+
}
|
|
16849
|
+
}
|
|
16728
16850
|
}
|
|
16729
16851
|
return {
|
|
16730
16852
|
hasMobile,
|
|
@@ -16742,6 +16864,7 @@ async function analyzeResponsiveCss(html, url) {
|
|
|
16742
16864
|
const allMediaQueries = [];
|
|
16743
16865
|
const externalSources = [];
|
|
16744
16866
|
let inlineCount = 0;
|
|
16867
|
+
let hasResponsiveSignals = false;
|
|
16745
16868
|
$("style").each((_, el) => {
|
|
16746
16869
|
const css = $(el).text();
|
|
16747
16870
|
const queries = extractMediaQueries(css);
|
|
@@ -16749,39 +16872,43 @@ async function analyzeResponsiveCss(html, url) {
|
|
|
16749
16872
|
allMediaQueries.push(...queries);
|
|
16750
16873
|
inlineCount += queries.length;
|
|
16751
16874
|
}
|
|
16875
|
+
if (RESPONSIVE_SIGNAL.test(css)) hasResponsiveSignals = true;
|
|
16752
16876
|
});
|
|
16753
|
-
const
|
|
16754
|
-
|
|
16877
|
+
const sameOrigin = [];
|
|
16878
|
+
let crossOrigin = 0;
|
|
16879
|
+
$("link[rel][href]").each((_, el) => {
|
|
16880
|
+
if (!/\bstylesheet\b/i.test($(el).attr("rel") ?? "")) return;
|
|
16881
|
+
const href = $(el).attr("href");
|
|
16882
|
+
if (!href || href.startsWith("data:")) return;
|
|
16755
16883
|
try {
|
|
16756
|
-
const
|
|
16757
|
-
|
|
16884
|
+
const resolved = new URL(href, url);
|
|
16885
|
+
if (resolved.hostname === baseUrl.hostname) sameOrigin.push(resolved.href);
|
|
16886
|
+
else crossOrigin++;
|
|
16758
16887
|
} catch {
|
|
16759
|
-
|
|
16888
|
+
crossOrigin++;
|
|
16760
16889
|
}
|
|
16761
16890
|
});
|
|
16762
|
-
|
|
16763
|
-
|
|
16764
|
-
|
|
16765
|
-
|
|
16766
|
-
|
|
16767
|
-
|
|
16768
|
-
|
|
16769
|
-
|
|
16770
|
-
|
|
16891
|
+
let fetchFailed = 0;
|
|
16892
|
+
await Promise.all(
|
|
16893
|
+
sameOrigin.slice(0, MAX_STYLESHEETS).map(async (cssUrl) => {
|
|
16894
|
+
try {
|
|
16895
|
+
const response = await httpGet(cssUrl, {
|
|
16896
|
+
timeout: STYLESHEET_TIMEOUT_MS,
|
|
16897
|
+
maxBytes: MAX_STYLESHEET_BYTES
|
|
16898
|
+
});
|
|
16899
|
+
if (typeof response.data !== "string") throw new Error("not text");
|
|
16771
16900
|
const queries = extractMediaQueries(response.data);
|
|
16772
|
-
if (queries.length > 0)
|
|
16773
|
-
|
|
16901
|
+
if (queries.length > 0) allMediaQueries.push(...queries);
|
|
16902
|
+
if (RESPONSIVE_SIGNAL.test(response.data)) {
|
|
16903
|
+
hasResponsiveSignals = true;
|
|
16774
16904
|
externalSources.push(cssUrl);
|
|
16775
16905
|
}
|
|
16906
|
+
} catch {
|
|
16907
|
+
fetchFailed++;
|
|
16776
16908
|
}
|
|
16777
|
-
}
|
|
16778
|
-
|
|
16779
|
-
|
|
16780
|
-
await Promise.race([
|
|
16781
|
-
Promise.all(cssPromises),
|
|
16782
|
-
new Promise((resolve) => setTimeout(resolve, 6e3))
|
|
16783
|
-
// 6s overall timeout
|
|
16784
|
-
]);
|
|
16909
|
+
})
|
|
16910
|
+
);
|
|
16911
|
+
const stylesheetsCovered = crossOrigin === 0 && fetchFailed === 0 && sameOrigin.length <= MAX_STYLESHEETS;
|
|
16785
16912
|
const hasMediaQueries = allMediaQueries.length > 0;
|
|
16786
16913
|
const { hasMobile, hasTablet, hasDesktop, breakpoints } = classifyBreakpoints(allMediaQueries);
|
|
16787
16914
|
if (!hasViewport) {
|
|
@@ -16796,21 +16923,22 @@ async function analyzeResponsiveCss(html, url) {
|
|
|
16796
16923
|
affectedUrls: [url]
|
|
16797
16924
|
});
|
|
16798
16925
|
}
|
|
16799
|
-
if (!
|
|
16926
|
+
if (!hasResponsiveSignals && stylesheetsCovered) {
|
|
16800
16927
|
issues.push({
|
|
16801
16928
|
code: "RESPONSIVE_NO_MEDIA_QUERIES",
|
|
16802
16929
|
severity: "warning",
|
|
16803
16930
|
category: "mobile",
|
|
16804
16931
|
title: "No CSS media queries detected",
|
|
16805
|
-
description: "No @media queries found in inline styles or first-party stylesheets.",
|
|
16932
|
+
description: "No @media or @container queries and no fluid sizing (clamp, min, max) found in inline styles or first-party stylesheets.",
|
|
16806
16933
|
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
16934
|
howToFix: "Add CSS media queries to adapt layout for different screen sizes. Consider using a mobile-first approach with min-width breakpoints.",
|
|
16808
16935
|
affectedUrls: [url],
|
|
16809
16936
|
details: {
|
|
16810
|
-
suggestion: "Common breakpoints: 480px (mobile), 768px (tablet), 1024px (desktop)"
|
|
16937
|
+
suggestion: "Common breakpoints: 480px (mobile), 768px (tablet), 1024px (desktop)",
|
|
16938
|
+
stylesheetsScanned: sameOrigin.length
|
|
16811
16939
|
}
|
|
16812
16940
|
});
|
|
16813
|
-
} else if (!hasMobile && !hasTablet) {
|
|
16941
|
+
} else if (hasMediaQueries && !hasMobile && !hasTablet) {
|
|
16814
16942
|
issues.push({
|
|
16815
16943
|
code: "RESPONSIVE_NO_MOBILE_BREAKPOINT",
|
|
16816
16944
|
severity: "warning",
|
|
@@ -16836,6 +16964,8 @@ async function analyzeResponsiveCss(html, url) {
|
|
|
16836
16964
|
inline: inlineCount,
|
|
16837
16965
|
external: externalSources
|
|
16838
16966
|
},
|
|
16967
|
+
hasResponsiveSignals,
|
|
16968
|
+
stylesheetsCovered,
|
|
16839
16969
|
breakpoints,
|
|
16840
16970
|
hasMobileBreakpoint: hasMobile,
|
|
16841
16971
|
hasTabletBreakpoint: hasTablet,
|
|
@@ -17474,7 +17604,22 @@ var urlSafetyDatabase = {
|
|
|
17474
17604
|
|
|
17475
17605
|
// src/audit/checks/tracking-verification.ts
|
|
17476
17606
|
var cheerio36 = __toESM(require("cheerio"));
|
|
17477
|
-
|
|
17607
|
+
var OTHER_ANALYTICS = [
|
|
17608
|
+
{ name: "Cloudflare Web Analytics", pattern: /cloudflareinsights\.com\/beacon(?:\.min)?\.js/i },
|
|
17609
|
+
{ name: "Plausible", pattern: /plausible\.io\/js\/|<script[^>]*data-domain=[^>]*plausible|<script[^>]*plausible[^>]*data-domain=/i },
|
|
17610
|
+
{ name: "Fathom", pattern: /cdn\.usefathom\.com/i },
|
|
17611
|
+
// data-website-id is Umami's attribute, on the hosted script and on self-hosted ones alike.
|
|
17612
|
+
{ name: "Umami", pattern: /<script[^>]*\bdata-website-id=/i },
|
|
17613
|
+
{ name: "PostHog", pattern: /posthog\.init\s*\(|\.posthog\.com\//i },
|
|
17614
|
+
{ name: "Matomo", pattern: /matomo\.js|piwik\.js|_paq\s*(?:=|\.push)/i },
|
|
17615
|
+
{ name: "Simple Analytics", pattern: /scripts\.simpleanalyticscdn\.com/i },
|
|
17616
|
+
{ name: "Mixpanel", pattern: /cdn\.mxpnl\.com|mixpanel\.init\s*\(/i },
|
|
17617
|
+
{ name: "Segment", pattern: /cdn\.segment\.com/i }
|
|
17618
|
+
];
|
|
17619
|
+
function detectOtherAnalytics(html) {
|
|
17620
|
+
return OTHER_ANALYTICS.filter(({ pattern }) => pattern.test(html)).map(({ name }) => name);
|
|
17621
|
+
}
|
|
17622
|
+
async function analyzeTrackingVerification(html, url) {
|
|
17478
17623
|
const issues = [];
|
|
17479
17624
|
const $ = cheerio36.load(html);
|
|
17480
17625
|
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 +17629,16 @@ function analyzeTrackingVerification(html, url) {
|
|
|
17484
17629
|
const hasGtm = !!gtmMatch;
|
|
17485
17630
|
const gtmContainerId = gtmMatch ? gtmMatch[1] : void 0;
|
|
17486
17631
|
const hasGtmNoscript = /googletagmanager\.com\/ns\.html\?id=GTM-[A-Z0-9]+/i.test(html);
|
|
17632
|
+
const otherAnalytics = detectOtherAnalytics(html);
|
|
17633
|
+
const verification = await lookupSiteVerification(url);
|
|
17487
17634
|
const gscMeta = $('meta[name="google-site-verification"]').attr("content");
|
|
17488
|
-
const
|
|
17635
|
+
const hasGscMeta = !!(gscMeta && gscMeta.length > 10);
|
|
17636
|
+
const hasGscVerification = hasGscMeta || verification.googleDns;
|
|
17637
|
+
const gscSource = hasGscMeta ? "meta" : verification.googleDns ? "dns" : void 0;
|
|
17489
17638
|
const bingMeta = $('meta[name="msvalidate.01"]').attr("content");
|
|
17490
|
-
const
|
|
17639
|
+
const hasBingMeta = !!(bingMeta && bingMeta.length > 10);
|
|
17640
|
+
const hasBingVerification = hasBingMeta || verification.bingDns || verification.bingSiteAuthFile;
|
|
17641
|
+
const bingSource = hasBingMeta ? "meta" : verification.bingDns ? "dns" : verification.bingSiteAuthFile ? "file" : void 0;
|
|
17491
17642
|
const jsonLdScripts = $('script[type="application/ld+json"]');
|
|
17492
17643
|
const hasMicrodata = $('[itemtype*="schema.org"]').length > 0;
|
|
17493
17644
|
const hasJsonLd = jsonLdScripts.length > 0;
|
|
@@ -17509,13 +17660,15 @@ function analyzeTrackingVerification(html, url) {
|
|
|
17509
17660
|
const data = {
|
|
17510
17661
|
ga4: { detected: hasGa4, measurementId: ga4MeasurementId },
|
|
17511
17662
|
gtm: { detected: hasGtm, containerId: gtmContainerId, hasNoscript: hasGtmNoscript },
|
|
17512
|
-
|
|
17513
|
-
|
|
17663
|
+
otherAnalytics,
|
|
17664
|
+
gsc: { verified: hasGscVerification, verificationCode: gscMeta, source: gscSource },
|
|
17665
|
+
bing: { verified: hasBingVerification, verificationCode: bingMeta, source: bingSource },
|
|
17666
|
+
dnsChecked: verification.dnsChecked,
|
|
17514
17667
|
schemaOrg: { detected: hasJsonLd || hasMicrodata, types: schemaTypes, hasValidJson },
|
|
17515
17668
|
sitemap: { inRobotsTxt: false }
|
|
17516
17669
|
// Set later if we check robots.txt
|
|
17517
17670
|
};
|
|
17518
|
-
if (!hasGa4 && !hasGtm) {
|
|
17671
|
+
if (!hasGa4 && !hasGtm && otherAnalytics.length === 0) {
|
|
17519
17672
|
issues.push({
|
|
17520
17673
|
code: "GA4_MISSING",
|
|
17521
17674
|
severity: "warning",
|
|
@@ -17528,40 +17681,46 @@ function analyzeTrackingVerification(html, url) {
|
|
|
17528
17681
|
});
|
|
17529
17682
|
}
|
|
17530
17683
|
if (!hasGscVerification) {
|
|
17684
|
+
const dnsChecked = verification.dnsChecked;
|
|
17531
17685
|
issues.push({
|
|
17532
17686
|
code: "GSC_VERIFICATION_MISSING",
|
|
17533
17687
|
severity: "warning",
|
|
17534
17688
|
category: "on-page",
|
|
17535
|
-
title: "Google Search Console verification not found",
|
|
17536
|
-
description: "No GSC verification meta tag
|
|
17689
|
+
title: dnsChecked ? "Google Search Console verification not found" : "Google Search Console verification not visible in the HTML",
|
|
17690
|
+
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
17691
|
impact: "Missing critical insights into Google search visibility, impressions, and clicks.",
|
|
17538
|
-
howToFix: "
|
|
17539
|
-
affectedUrls: [url]
|
|
17692
|
+
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.",
|
|
17693
|
+
affectedUrls: [url],
|
|
17694
|
+
details: { dnsChecked }
|
|
17540
17695
|
});
|
|
17541
17696
|
}
|
|
17542
17697
|
if (!hasBingVerification) {
|
|
17698
|
+
const dnsChecked = verification.dnsChecked;
|
|
17543
17699
|
issues.push({
|
|
17544
17700
|
code: "BING_VERIFICATION_MISSING",
|
|
17545
17701
|
severity: "notice",
|
|
17546
17702
|
category: "on-page",
|
|
17547
|
-
title: "Bing Webmaster Tools verification not found",
|
|
17548
|
-
description: "No Bing
|
|
17703
|
+
title: dnsChecked ? "Bing Webmaster Tools verification not found" : "Bing Webmaster Tools verification not visible in the HTML",
|
|
17704
|
+
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
17705
|
impact: "Missing insights on Bing/Yahoo search traffic.",
|
|
17550
|
-
howToFix: "
|
|
17551
|
-
affectedUrls: [url]
|
|
17706
|
+
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.",
|
|
17707
|
+
affectedUrls: [url],
|
|
17708
|
+
details: { dnsChecked }
|
|
17552
17709
|
});
|
|
17553
17710
|
}
|
|
17554
17711
|
if (!hasGtm) {
|
|
17555
|
-
|
|
17556
|
-
|
|
17557
|
-
|
|
17558
|
-
|
|
17559
|
-
|
|
17560
|
-
|
|
17561
|
-
|
|
17562
|
-
|
|
17563
|
-
|
|
17564
|
-
|
|
17712
|
+
if (otherAnalytics.length === 0) {
|
|
17713
|
+
issues.push({
|
|
17714
|
+
code: "GTM_MISSING",
|
|
17715
|
+
severity: "notice",
|
|
17716
|
+
category: "on-page",
|
|
17717
|
+
title: "Google Tag Manager not detected",
|
|
17718
|
+
description: "GTM not found. While not required, GTM makes it easier to manage tracking tags without code changes.",
|
|
17719
|
+
impact: "Tag management requires developer involvement for each change.",
|
|
17720
|
+
howToFix: "Consider using GTM for easier tag management. It allows marketing to add tags without developer help.",
|
|
17721
|
+
affectedUrls: [url]
|
|
17722
|
+
});
|
|
17723
|
+
}
|
|
17565
17724
|
} else if (!hasGtmNoscript) {
|
|
17566
17725
|
issues.push({
|
|
17567
17726
|
code: "GTM_NOSCRIPT_MISSING",
|
|
@@ -17701,7 +17860,6 @@ async function runFullAudit(options) {
|
|
|
17701
17860
|
const urlSafetyResult = analyzeUrlSafety(url);
|
|
17702
17861
|
const domSizeResult = analyzeDomSize(html, url);
|
|
17703
17862
|
const imageDimensionsResult = analyzeImageDimensions(html, url);
|
|
17704
|
-
const trackingResult = analyzeTrackingVerification(html, url);
|
|
17705
17863
|
const anchorResult = runExtendedChecks ? analyzeAnchorText(html, url) : { issues: [], data: {} };
|
|
17706
17864
|
const localSeoResult = runExtendedChecks ? analyzeLocalSEO(html, url) : { issues: [], data: {} };
|
|
17707
17865
|
const modernImageResult = runExtendedChecks ? analyzeModernImages(html, url) : { issues: [], data: {} };
|
|
@@ -17776,8 +17934,7 @@ async function runFullAudit(options) {
|
|
|
17776
17934
|
...colorContrastResult.issues,
|
|
17777
17935
|
...pageResourcesResult.issues,
|
|
17778
17936
|
...urlSafetyResult.issues,
|
|
17779
|
-
...paginationResult.issues
|
|
17780
|
-
...trackingResult.issues
|
|
17937
|
+
...paginationResult.issues
|
|
17781
17938
|
);
|
|
17782
17939
|
if (!structuredDataResult.data.hasSchema) {
|
|
17783
17940
|
const suggestions = suggestSchemaTypes(html, url);
|
|
@@ -17823,6 +17980,7 @@ async function runFullAudit(options) {
|
|
|
17823
17980
|
asyncChecks.push(safeAsyncIssues("Certificate", async () => (await checkCertificate(url)).issues));
|
|
17824
17981
|
asyncChecks.push(safeAsync("Redirects", () => analyzeRedirects(url)));
|
|
17825
17982
|
asyncChecks.push(safeAsync("Robots Validation", () => validateRobotsTxt(url)));
|
|
17983
|
+
asyncChecks.push(safeAsync("Tracking Verification", () => analyzeTrackingVerification(html, url)));
|
|
17826
17984
|
asyncChecks.push(safeAsyncIssues("Crawler Access", async () => (await analyzeCrawlerAccess(url)).issues));
|
|
17827
17985
|
asyncChecks.push(safeAsync("Canonical Domain", () => analyzeCanonicalDomain(url)));
|
|
17828
17986
|
asyncChecks.push(safeAsyncIssues("Framework Detection", async () => {
|
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,
|
|
@@ -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) {
|
|
@@ -11015,6 +11017,106 @@ function analyzeResponsiveImages(html, url) {
|
|
|
11015
11017
|
|
|
11016
11018
|
// src/audit/checks/bing-optimization.ts
|
|
11017
11019
|
import * as cheerio28 from "cheerio";
|
|
11020
|
+
|
|
11021
|
+
// src/audit/checks/site-verification.ts
|
|
11022
|
+
var UNCHECKED = {
|
|
11023
|
+
dnsChecked: false,
|
|
11024
|
+
googleDns: false,
|
|
11025
|
+
bingDns: false,
|
|
11026
|
+
bingSiteAuthFile: false
|
|
11027
|
+
};
|
|
11028
|
+
var DNS_TIMEOUT_MS = 2e3;
|
|
11029
|
+
var FILE_TIMEOUT_MS = 5e3;
|
|
11030
|
+
var CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
11031
|
+
var cache = /* @__PURE__ */ new Map();
|
|
11032
|
+
function lookupSiteVerification(url) {
|
|
11033
|
+
let parsed;
|
|
11034
|
+
try {
|
|
11035
|
+
parsed = new URL(url);
|
|
11036
|
+
} catch {
|
|
11037
|
+
return Promise.resolve(UNCHECKED);
|
|
11038
|
+
}
|
|
11039
|
+
const host = parsed.hostname.toLowerCase();
|
|
11040
|
+
const now = Date.now();
|
|
11041
|
+
const cached = cache.get(host);
|
|
11042
|
+
if (cached && cached.expires > now) return cached.result;
|
|
11043
|
+
const result = Promise.all([readTxtRecords(host), fetchBingSiteAuth(parsed.origin)]).then(
|
|
11044
|
+
([txt, bingSiteAuthFile]) => ({
|
|
11045
|
+
dnsChecked: txt.checked,
|
|
11046
|
+
googleDns: txt.records.some((record) => /^google-site-verification=/i.test(record)),
|
|
11047
|
+
bingDns: txt.records.some((record) => /^MS=/i.test(record)),
|
|
11048
|
+
bingSiteAuthFile
|
|
11049
|
+
})
|
|
11050
|
+
);
|
|
11051
|
+
cache.set(host, { expires: now + CACHE_TTL_MS, result });
|
|
11052
|
+
return result;
|
|
11053
|
+
}
|
|
11054
|
+
var resolveTxtLoader;
|
|
11055
|
+
function loadResolveTxt() {
|
|
11056
|
+
resolveTxtLoader ??= (async () => {
|
|
11057
|
+
if (typeof process === "undefined" || !process.versions?.node) return null;
|
|
11058
|
+
try {
|
|
11059
|
+
const specifier = "node:dns";
|
|
11060
|
+
const dns = await import(specifier);
|
|
11061
|
+
const resolveTxt = dns?.promises?.resolveTxt ?? dns?.default?.promises?.resolveTxt;
|
|
11062
|
+
return typeof resolveTxt === "function" ? resolveTxt : null;
|
|
11063
|
+
} catch {
|
|
11064
|
+
return null;
|
|
11065
|
+
}
|
|
11066
|
+
})();
|
|
11067
|
+
return resolveTxtLoader;
|
|
11068
|
+
}
|
|
11069
|
+
function domainCandidates(host) {
|
|
11070
|
+
if (/^[\d.]+$/.test(host) || host.includes(":")) return [];
|
|
11071
|
+
const labels = host.split(".").filter(Boolean);
|
|
11072
|
+
const names = [];
|
|
11073
|
+
for (let n = 2; n <= labels.length && names.length < 4; n++) {
|
|
11074
|
+
names.push(labels.slice(-n).join("."));
|
|
11075
|
+
}
|
|
11076
|
+
return names.length > 0 ? names : [host];
|
|
11077
|
+
}
|
|
11078
|
+
async function readTxtRecords(host) {
|
|
11079
|
+
const resolveTxt = await loadResolveTxt();
|
|
11080
|
+
if (!resolveTxt) return { checked: false, records: [] };
|
|
11081
|
+
const lookups = await Promise.all(domainCandidates(host).map((name) => resolveTxtWithin(resolveTxt, name)));
|
|
11082
|
+
return {
|
|
11083
|
+
checked: lookups.every((records) => records !== null),
|
|
11084
|
+
records: lookups.flatMap((records) => records ?? [])
|
|
11085
|
+
};
|
|
11086
|
+
}
|
|
11087
|
+
var NO_RECORD_CODES = /* @__PURE__ */ new Set(["ENODATA", "ENOTFOUND"]);
|
|
11088
|
+
async function resolveTxtWithin(resolveTxt, name) {
|
|
11089
|
+
let timer;
|
|
11090
|
+
const timeout = new Promise((resolve) => {
|
|
11091
|
+
timer = setTimeout(() => resolve(null), DNS_TIMEOUT_MS);
|
|
11092
|
+
});
|
|
11093
|
+
try {
|
|
11094
|
+
return await Promise.race([
|
|
11095
|
+
resolveTxt(name).then(
|
|
11096
|
+
(records) => records.map((chunks) => chunks.join("").trim()),
|
|
11097
|
+
(error) => NO_RECORD_CODES.has(error?.code ?? "") ? [] : null
|
|
11098
|
+
),
|
|
11099
|
+
timeout
|
|
11100
|
+
]);
|
|
11101
|
+
} catch {
|
|
11102
|
+
return null;
|
|
11103
|
+
} finally {
|
|
11104
|
+
clearTimeout(timer);
|
|
11105
|
+
}
|
|
11106
|
+
}
|
|
11107
|
+
async function fetchBingSiteAuth(origin) {
|
|
11108
|
+
try {
|
|
11109
|
+
const response = await httpGet(`${origin}/BingSiteAuth.xml`, {
|
|
11110
|
+
timeout: FILE_TIMEOUT_MS,
|
|
11111
|
+
maxBytes: 64 * 1024
|
|
11112
|
+
});
|
|
11113
|
+
return response.status === 200 && typeof response.data === "string" && !looksLikeHtmlFallback(response.data, response.headers["content-type"]) && /<users[\s>]/i.test(response.data);
|
|
11114
|
+
} catch {
|
|
11115
|
+
return false;
|
|
11116
|
+
}
|
|
11117
|
+
}
|
|
11118
|
+
|
|
11119
|
+
// src/audit/checks/bing-optimization.ts
|
|
11018
11120
|
async function analyzeBingOptimization(html, url) {
|
|
11019
11121
|
const issues = [];
|
|
11020
11122
|
const $ = cheerio28.load(html);
|
|
@@ -11023,12 +11125,9 @@ async function analyzeBingOptimization(html, url) {
|
|
|
11023
11125
|
const detectedIssues = [];
|
|
11024
11126
|
const bingMetaTag = $('meta[name="msvalidate.01"]').attr("content");
|
|
11025
11127
|
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
|
-
}
|
|
11128
|
+
const verification = await lookupSiteVerification(url);
|
|
11129
|
+
const hasBingSiteAuth = verification.bingSiteAuthFile;
|
|
11130
|
+
const hasBingDnsRecord = verification.bingDns;
|
|
11032
11131
|
let hasBingIndexNowKey = false;
|
|
11033
11132
|
try {
|
|
11034
11133
|
const keyPatterns = [
|
|
@@ -11076,22 +11175,25 @@ async function analyzeBingOptimization(html, url) {
|
|
|
11076
11175
|
bingSitemapAccessible = sitemapResponse.status === 200 && sitemapResponse.data.includes("<?xml");
|
|
11077
11176
|
} catch {
|
|
11078
11177
|
}
|
|
11079
|
-
if (!hasBingMetaTag && !hasBingSiteAuth) {
|
|
11178
|
+
if (!hasBingMetaTag && !hasBingSiteAuth && !hasBingDnsRecord) {
|
|
11080
11179
|
detectedIssues.push("No Bing site verification");
|
|
11081
11180
|
issues.push({
|
|
11082
11181
|
code: "BING_NOT_VERIFIED",
|
|
11083
11182
|
severity: "warning",
|
|
11084
11183
|
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.",
|
|
11184
|
+
title: verification.dnsChecked ? "Site not verified with Bing Webmaster Tools" : "Bing Webmaster Tools verification not visible in the HTML",
|
|
11185
|
+
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
11186
|
impact: "May reduce visibility in ChatGPT Search results and miss Bing-specific optimization opportunities.",
|
|
11088
|
-
howToFix: "
|
|
11187
|
+
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
11188
|
affectedUrls: [url],
|
|
11090
11189
|
details: {
|
|
11091
11190
|
hasBingMetaTag,
|
|
11092
11191
|
hasBingSiteAuth,
|
|
11192
|
+
hasBingDnsRecord,
|
|
11193
|
+
dnsChecked: verification.dnsChecked,
|
|
11093
11194
|
verificationMethods: [
|
|
11094
11195
|
'Add <meta name="msvalidate.01" content="YOUR_CODE"> to <head>',
|
|
11196
|
+
"Add an MS=YOUR_CODE TXT record to the domain's DNS",
|
|
11095
11197
|
"Upload BingSiteAuth.xml to root directory"
|
|
11096
11198
|
]
|
|
11097
11199
|
}
|
|
@@ -11151,6 +11253,8 @@ async function analyzeBingOptimization(html, url) {
|
|
|
11151
11253
|
data: {
|
|
11152
11254
|
hasBingSiteAuth,
|
|
11153
11255
|
hasBingMetaTag,
|
|
11256
|
+
hasBingDnsRecord,
|
|
11257
|
+
dnsChecked: verification.dnsChecked,
|
|
11154
11258
|
hasBingIndexNowKey,
|
|
11155
11259
|
bingPlacesDetected,
|
|
11156
11260
|
bingSitemapAccessible,
|
|
@@ -13129,6 +13233,10 @@ function analyzePageResources(html, url) {
|
|
|
13129
13233
|
|
|
13130
13234
|
// src/audit/checks/responsive-css.ts
|
|
13131
13235
|
import * as cheerio35 from "cheerio";
|
|
13236
|
+
var MAX_STYLESHEETS = 3;
|
|
13237
|
+
var MAX_STYLESHEET_BYTES = 1024 * 1024;
|
|
13238
|
+
var STYLESHEET_TIMEOUT_MS = 3e3;
|
|
13239
|
+
var RESPONSIVE_SIGNAL = /@media\b|@container\b|\bclamp\(|(?<![\w-])(?:min|max)\(/i;
|
|
13132
13240
|
function extractMediaQueries(css) {
|
|
13133
13241
|
const mediaQueries = [];
|
|
13134
13242
|
const regex = /@media\s*([^{]+)/g;
|
|
@@ -13138,36 +13246,31 @@ function extractMediaQueries(css) {
|
|
|
13138
13246
|
}
|
|
13139
13247
|
return mediaQueries;
|
|
13140
13248
|
}
|
|
13249
|
+
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
13250
|
function classifyBreakpoints(mediaQueries) {
|
|
13142
13251
|
const breakpoints = /* @__PURE__ */ new Set();
|
|
13143
13252
|
let hasMobile = false;
|
|
13144
13253
|
let hasTablet = false;
|
|
13145
13254
|
let hasDesktop = false;
|
|
13146
13255
|
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
13256
|
if (/print/i.test(query)) {
|
|
13169
13257
|
continue;
|
|
13170
13258
|
}
|
|
13259
|
+
for (const match of query.matchAll(WIDTH_RULE)) {
|
|
13260
|
+
const raw = match[1] ?? match[3];
|
|
13261
|
+
const unit = (match[2] ?? match[4] ?? "px").toLowerCase();
|
|
13262
|
+
const value = Math.round(parseFloat(raw) * (unit === "px" ? 1 : 16));
|
|
13263
|
+
breakpoints.add(`${value}px`);
|
|
13264
|
+
if (value <= 480) {
|
|
13265
|
+
hasMobile = true;
|
|
13266
|
+
} else if (value <= 768) {
|
|
13267
|
+
hasMobile = true;
|
|
13268
|
+
} else if (value <= 1024) {
|
|
13269
|
+
hasTablet = true;
|
|
13270
|
+
} else {
|
|
13271
|
+
hasDesktop = true;
|
|
13272
|
+
}
|
|
13273
|
+
}
|
|
13171
13274
|
}
|
|
13172
13275
|
return {
|
|
13173
13276
|
hasMobile,
|
|
@@ -13185,6 +13288,7 @@ async function analyzeResponsiveCss(html, url) {
|
|
|
13185
13288
|
const allMediaQueries = [];
|
|
13186
13289
|
const externalSources = [];
|
|
13187
13290
|
let inlineCount = 0;
|
|
13291
|
+
let hasResponsiveSignals = false;
|
|
13188
13292
|
$("style").each((_, el) => {
|
|
13189
13293
|
const css = $(el).text();
|
|
13190
13294
|
const queries = extractMediaQueries(css);
|
|
@@ -13192,39 +13296,43 @@ async function analyzeResponsiveCss(html, url) {
|
|
|
13192
13296
|
allMediaQueries.push(...queries);
|
|
13193
13297
|
inlineCount += queries.length;
|
|
13194
13298
|
}
|
|
13299
|
+
if (RESPONSIVE_SIGNAL.test(css)) hasResponsiveSignals = true;
|
|
13195
13300
|
});
|
|
13196
|
-
const
|
|
13197
|
-
|
|
13301
|
+
const sameOrigin = [];
|
|
13302
|
+
let crossOrigin = 0;
|
|
13303
|
+
$("link[rel][href]").each((_, el) => {
|
|
13304
|
+
if (!/\bstylesheet\b/i.test($(el).attr("rel") ?? "")) return;
|
|
13305
|
+
const href = $(el).attr("href");
|
|
13306
|
+
if (!href || href.startsWith("data:")) return;
|
|
13198
13307
|
try {
|
|
13199
|
-
const
|
|
13200
|
-
|
|
13308
|
+
const resolved = new URL(href, url);
|
|
13309
|
+
if (resolved.hostname === baseUrl.hostname) sameOrigin.push(resolved.href);
|
|
13310
|
+
else crossOrigin++;
|
|
13201
13311
|
} catch {
|
|
13202
|
-
|
|
13312
|
+
crossOrigin++;
|
|
13203
13313
|
}
|
|
13204
13314
|
});
|
|
13205
|
-
|
|
13206
|
-
|
|
13207
|
-
|
|
13208
|
-
|
|
13209
|
-
|
|
13210
|
-
|
|
13211
|
-
|
|
13212
|
-
|
|
13213
|
-
|
|
13315
|
+
let fetchFailed = 0;
|
|
13316
|
+
await Promise.all(
|
|
13317
|
+
sameOrigin.slice(0, MAX_STYLESHEETS).map(async (cssUrl) => {
|
|
13318
|
+
try {
|
|
13319
|
+
const response = await httpGet(cssUrl, {
|
|
13320
|
+
timeout: STYLESHEET_TIMEOUT_MS,
|
|
13321
|
+
maxBytes: MAX_STYLESHEET_BYTES
|
|
13322
|
+
});
|
|
13323
|
+
if (typeof response.data !== "string") throw new Error("not text");
|
|
13214
13324
|
const queries = extractMediaQueries(response.data);
|
|
13215
|
-
if (queries.length > 0)
|
|
13216
|
-
|
|
13325
|
+
if (queries.length > 0) allMediaQueries.push(...queries);
|
|
13326
|
+
if (RESPONSIVE_SIGNAL.test(response.data)) {
|
|
13327
|
+
hasResponsiveSignals = true;
|
|
13217
13328
|
externalSources.push(cssUrl);
|
|
13218
13329
|
}
|
|
13330
|
+
} catch {
|
|
13331
|
+
fetchFailed++;
|
|
13219
13332
|
}
|
|
13220
|
-
}
|
|
13221
|
-
|
|
13222
|
-
|
|
13223
|
-
await Promise.race([
|
|
13224
|
-
Promise.all(cssPromises),
|
|
13225
|
-
new Promise((resolve) => setTimeout(resolve, 6e3))
|
|
13226
|
-
// 6s overall timeout
|
|
13227
|
-
]);
|
|
13333
|
+
})
|
|
13334
|
+
);
|
|
13335
|
+
const stylesheetsCovered = crossOrigin === 0 && fetchFailed === 0 && sameOrigin.length <= MAX_STYLESHEETS;
|
|
13228
13336
|
const hasMediaQueries = allMediaQueries.length > 0;
|
|
13229
13337
|
const { hasMobile, hasTablet, hasDesktop, breakpoints } = classifyBreakpoints(allMediaQueries);
|
|
13230
13338
|
if (!hasViewport) {
|
|
@@ -13239,21 +13347,22 @@ async function analyzeResponsiveCss(html, url) {
|
|
|
13239
13347
|
affectedUrls: [url]
|
|
13240
13348
|
});
|
|
13241
13349
|
}
|
|
13242
|
-
if (!
|
|
13350
|
+
if (!hasResponsiveSignals && stylesheetsCovered) {
|
|
13243
13351
|
issues.push({
|
|
13244
13352
|
code: "RESPONSIVE_NO_MEDIA_QUERIES",
|
|
13245
13353
|
severity: "warning",
|
|
13246
13354
|
category: "mobile",
|
|
13247
13355
|
title: "No CSS media queries detected",
|
|
13248
|
-
description: "No @media queries found in inline styles or first-party stylesheets.",
|
|
13356
|
+
description: "No @media or @container queries and no fluid sizing (clamp, min, max) found in inline styles or first-party stylesheets.",
|
|
13249
13357
|
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
13358
|
howToFix: "Add CSS media queries to adapt layout for different screen sizes. Consider using a mobile-first approach with min-width breakpoints.",
|
|
13251
13359
|
affectedUrls: [url],
|
|
13252
13360
|
details: {
|
|
13253
|
-
suggestion: "Common breakpoints: 480px (mobile), 768px (tablet), 1024px (desktop)"
|
|
13361
|
+
suggestion: "Common breakpoints: 480px (mobile), 768px (tablet), 1024px (desktop)",
|
|
13362
|
+
stylesheetsScanned: sameOrigin.length
|
|
13254
13363
|
}
|
|
13255
13364
|
});
|
|
13256
|
-
} else if (!hasMobile && !hasTablet) {
|
|
13365
|
+
} else if (hasMediaQueries && !hasMobile && !hasTablet) {
|
|
13257
13366
|
issues.push({
|
|
13258
13367
|
code: "RESPONSIVE_NO_MOBILE_BREAKPOINT",
|
|
13259
13368
|
severity: "warning",
|
|
@@ -13279,6 +13388,8 @@ async function analyzeResponsiveCss(html, url) {
|
|
|
13279
13388
|
inline: inlineCount,
|
|
13280
13389
|
external: externalSources
|
|
13281
13390
|
},
|
|
13391
|
+
hasResponsiveSignals,
|
|
13392
|
+
stylesheetsCovered,
|
|
13282
13393
|
breakpoints,
|
|
13283
13394
|
hasMobileBreakpoint: hasMobile,
|
|
13284
13395
|
hasTabletBreakpoint: hasTablet,
|
|
@@ -13916,7 +14027,22 @@ var urlSafetyDatabase = {
|
|
|
13916
14027
|
|
|
13917
14028
|
// src/audit/checks/tracking-verification.ts
|
|
13918
14029
|
import * as cheerio36 from "cheerio";
|
|
13919
|
-
|
|
14030
|
+
var OTHER_ANALYTICS = [
|
|
14031
|
+
{ name: "Cloudflare Web Analytics", pattern: /cloudflareinsights\.com\/beacon(?:\.min)?\.js/i },
|
|
14032
|
+
{ name: "Plausible", pattern: /plausible\.io\/js\/|<script[^>]*data-domain=[^>]*plausible|<script[^>]*plausible[^>]*data-domain=/i },
|
|
14033
|
+
{ name: "Fathom", pattern: /cdn\.usefathom\.com/i },
|
|
14034
|
+
// data-website-id is Umami's attribute, on the hosted script and on self-hosted ones alike.
|
|
14035
|
+
{ name: "Umami", pattern: /<script[^>]*\bdata-website-id=/i },
|
|
14036
|
+
{ name: "PostHog", pattern: /posthog\.init\s*\(|\.posthog\.com\//i },
|
|
14037
|
+
{ name: "Matomo", pattern: /matomo\.js|piwik\.js|_paq\s*(?:=|\.push)/i },
|
|
14038
|
+
{ name: "Simple Analytics", pattern: /scripts\.simpleanalyticscdn\.com/i },
|
|
14039
|
+
{ name: "Mixpanel", pattern: /cdn\.mxpnl\.com|mixpanel\.init\s*\(/i },
|
|
14040
|
+
{ name: "Segment", pattern: /cdn\.segment\.com/i }
|
|
14041
|
+
];
|
|
14042
|
+
function detectOtherAnalytics(html) {
|
|
14043
|
+
return OTHER_ANALYTICS.filter(({ pattern }) => pattern.test(html)).map(({ name }) => name);
|
|
14044
|
+
}
|
|
14045
|
+
async function analyzeTrackingVerification(html, url) {
|
|
13920
14046
|
const issues = [];
|
|
13921
14047
|
const $ = cheerio36.load(html);
|
|
13922
14048
|
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 +14052,16 @@ function analyzeTrackingVerification(html, url) {
|
|
|
13926
14052
|
const hasGtm = !!gtmMatch;
|
|
13927
14053
|
const gtmContainerId = gtmMatch ? gtmMatch[1] : void 0;
|
|
13928
14054
|
const hasGtmNoscript = /googletagmanager\.com\/ns\.html\?id=GTM-[A-Z0-9]+/i.test(html);
|
|
14055
|
+
const otherAnalytics = detectOtherAnalytics(html);
|
|
14056
|
+
const verification = await lookupSiteVerification(url);
|
|
13929
14057
|
const gscMeta = $('meta[name="google-site-verification"]').attr("content");
|
|
13930
|
-
const
|
|
14058
|
+
const hasGscMeta = !!(gscMeta && gscMeta.length > 10);
|
|
14059
|
+
const hasGscVerification = hasGscMeta || verification.googleDns;
|
|
14060
|
+
const gscSource = hasGscMeta ? "meta" : verification.googleDns ? "dns" : void 0;
|
|
13931
14061
|
const bingMeta = $('meta[name="msvalidate.01"]').attr("content");
|
|
13932
|
-
const
|
|
14062
|
+
const hasBingMeta = !!(bingMeta && bingMeta.length > 10);
|
|
14063
|
+
const hasBingVerification = hasBingMeta || verification.bingDns || verification.bingSiteAuthFile;
|
|
14064
|
+
const bingSource = hasBingMeta ? "meta" : verification.bingDns ? "dns" : verification.bingSiteAuthFile ? "file" : void 0;
|
|
13933
14065
|
const jsonLdScripts = $('script[type="application/ld+json"]');
|
|
13934
14066
|
const hasMicrodata = $('[itemtype*="schema.org"]').length > 0;
|
|
13935
14067
|
const hasJsonLd = jsonLdScripts.length > 0;
|
|
@@ -13951,13 +14083,15 @@ function analyzeTrackingVerification(html, url) {
|
|
|
13951
14083
|
const data = {
|
|
13952
14084
|
ga4: { detected: hasGa4, measurementId: ga4MeasurementId },
|
|
13953
14085
|
gtm: { detected: hasGtm, containerId: gtmContainerId, hasNoscript: hasGtmNoscript },
|
|
13954
|
-
|
|
13955
|
-
|
|
14086
|
+
otherAnalytics,
|
|
14087
|
+
gsc: { verified: hasGscVerification, verificationCode: gscMeta, source: gscSource },
|
|
14088
|
+
bing: { verified: hasBingVerification, verificationCode: bingMeta, source: bingSource },
|
|
14089
|
+
dnsChecked: verification.dnsChecked,
|
|
13956
14090
|
schemaOrg: { detected: hasJsonLd || hasMicrodata, types: schemaTypes, hasValidJson },
|
|
13957
14091
|
sitemap: { inRobotsTxt: false }
|
|
13958
14092
|
// Set later if we check robots.txt
|
|
13959
14093
|
};
|
|
13960
|
-
if (!hasGa4 && !hasGtm) {
|
|
14094
|
+
if (!hasGa4 && !hasGtm && otherAnalytics.length === 0) {
|
|
13961
14095
|
issues.push({
|
|
13962
14096
|
code: "GA4_MISSING",
|
|
13963
14097
|
severity: "warning",
|
|
@@ -13970,40 +14104,46 @@ function analyzeTrackingVerification(html, url) {
|
|
|
13970
14104
|
});
|
|
13971
14105
|
}
|
|
13972
14106
|
if (!hasGscVerification) {
|
|
14107
|
+
const dnsChecked = verification.dnsChecked;
|
|
13973
14108
|
issues.push({
|
|
13974
14109
|
code: "GSC_VERIFICATION_MISSING",
|
|
13975
14110
|
severity: "warning",
|
|
13976
14111
|
category: "on-page",
|
|
13977
|
-
title: "Google Search Console verification not found",
|
|
13978
|
-
description: "No GSC verification meta tag
|
|
14112
|
+
title: dnsChecked ? "Google Search Console verification not found" : "Google Search Console verification not visible in the HTML",
|
|
14113
|
+
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
14114
|
impact: "Missing critical insights into Google search visibility, impressions, and clicks.",
|
|
13980
|
-
howToFix: "
|
|
13981
|
-
affectedUrls: [url]
|
|
14115
|
+
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.",
|
|
14116
|
+
affectedUrls: [url],
|
|
14117
|
+
details: { dnsChecked }
|
|
13982
14118
|
});
|
|
13983
14119
|
}
|
|
13984
14120
|
if (!hasBingVerification) {
|
|
14121
|
+
const dnsChecked = verification.dnsChecked;
|
|
13985
14122
|
issues.push({
|
|
13986
14123
|
code: "BING_VERIFICATION_MISSING",
|
|
13987
14124
|
severity: "notice",
|
|
13988
14125
|
category: "on-page",
|
|
13989
|
-
title: "Bing Webmaster Tools verification not found",
|
|
13990
|
-
description: "No Bing
|
|
14126
|
+
title: dnsChecked ? "Bing Webmaster Tools verification not found" : "Bing Webmaster Tools verification not visible in the HTML",
|
|
14127
|
+
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
14128
|
impact: "Missing insights on Bing/Yahoo search traffic.",
|
|
13992
|
-
howToFix: "
|
|
13993
|
-
affectedUrls: [url]
|
|
14129
|
+
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.",
|
|
14130
|
+
affectedUrls: [url],
|
|
14131
|
+
details: { dnsChecked }
|
|
13994
14132
|
});
|
|
13995
14133
|
}
|
|
13996
14134
|
if (!hasGtm) {
|
|
13997
|
-
|
|
13998
|
-
|
|
13999
|
-
|
|
14000
|
-
|
|
14001
|
-
|
|
14002
|
-
|
|
14003
|
-
|
|
14004
|
-
|
|
14005
|
-
|
|
14006
|
-
|
|
14135
|
+
if (otherAnalytics.length === 0) {
|
|
14136
|
+
issues.push({
|
|
14137
|
+
code: "GTM_MISSING",
|
|
14138
|
+
severity: "notice",
|
|
14139
|
+
category: "on-page",
|
|
14140
|
+
title: "Google Tag Manager not detected",
|
|
14141
|
+
description: "GTM not found. While not required, GTM makes it easier to manage tracking tags without code changes.",
|
|
14142
|
+
impact: "Tag management requires developer involvement for each change.",
|
|
14143
|
+
howToFix: "Consider using GTM for easier tag management. It allows marketing to add tags without developer help.",
|
|
14144
|
+
affectedUrls: [url]
|
|
14145
|
+
});
|
|
14146
|
+
}
|
|
14007
14147
|
} else if (!hasGtmNoscript) {
|
|
14008
14148
|
issues.push({
|
|
14009
14149
|
code: "GTM_NOSCRIPT_MISSING",
|
|
@@ -14143,7 +14283,6 @@ async function runFullAudit(options) {
|
|
|
14143
14283
|
const urlSafetyResult = analyzeUrlSafety(url);
|
|
14144
14284
|
const domSizeResult = analyzeDomSize(html, url);
|
|
14145
14285
|
const imageDimensionsResult = analyzeImageDimensions(html, url);
|
|
14146
|
-
const trackingResult = analyzeTrackingVerification(html, url);
|
|
14147
14286
|
const anchorResult = runExtendedChecks ? analyzeAnchorText(html, url) : { issues: [], data: {} };
|
|
14148
14287
|
const localSeoResult = runExtendedChecks ? analyzeLocalSEO(html, url) : { issues: [], data: {} };
|
|
14149
14288
|
const modernImageResult = runExtendedChecks ? analyzeModernImages(html, url) : { issues: [], data: {} };
|
|
@@ -14218,8 +14357,7 @@ async function runFullAudit(options) {
|
|
|
14218
14357
|
...colorContrastResult.issues,
|
|
14219
14358
|
...pageResourcesResult.issues,
|
|
14220
14359
|
...urlSafetyResult.issues,
|
|
14221
|
-
...paginationResult.issues
|
|
14222
|
-
...trackingResult.issues
|
|
14360
|
+
...paginationResult.issues
|
|
14223
14361
|
);
|
|
14224
14362
|
if (!structuredDataResult.data.hasSchema) {
|
|
14225
14363
|
const suggestions = suggestSchemaTypes(html, url);
|
|
@@ -14265,6 +14403,7 @@ async function runFullAudit(options) {
|
|
|
14265
14403
|
asyncChecks.push(safeAsyncIssues("Certificate", async () => (await checkCertificate(url)).issues));
|
|
14266
14404
|
asyncChecks.push(safeAsync("Redirects", () => analyzeRedirects(url)));
|
|
14267
14405
|
asyncChecks.push(safeAsync("Robots Validation", () => validateRobotsTxt(url)));
|
|
14406
|
+
asyncChecks.push(safeAsync("Tracking Verification", () => analyzeTrackingVerification(html, url)));
|
|
14268
14407
|
asyncChecks.push(safeAsyncIssues("Crawler Access", async () => (await analyzeCrawlerAccess(url)).issues));
|
|
14269
14408
|
asyncChecks.push(safeAsync("Canonical Domain", () => analyzeCanonicalDomain(url)));
|
|
14270
14409
|
asyncChecks.push(safeAsyncIssues("Framework Detection", async () => {
|
|
@@ -23143,7 +23282,7 @@ async function executeAgent(agent, options = {}) {
|
|
|
23143
23282
|
}
|
|
23144
23283
|
}
|
|
23145
23284
|
async function runDirectAnalysis(url) {
|
|
23146
|
-
const { analyzeUrl: analyzeUrl2 } = await import("./analyzer-
|
|
23285
|
+
const { analyzeUrl: analyzeUrl2 } = await import("./analyzer-HHRJODCF.mjs");
|
|
23147
23286
|
const result = await analyzeUrl2({ url });
|
|
23148
23287
|
if (!result.success || !result.data) {
|
|
23149
23288
|
throw new Error(result.error || "Analysis failed");
|