@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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  analyzeUrl
3
- } from "./chunk-O7B2LBKG.mjs";
3
+ } from "./chunk-ZLQQXIZ3.mjs";
4
4
  import "./chunk-6BWS3CLP.mjs";
5
5
  export {
6
6
  analyzeUrl
@@ -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.text();
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.text();
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 hasAccessibleText = text.length > 0 || ariaLabel || title;
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
- let hasBingSiteAuth = false;
14577
- try {
14578
- const authResponse = await httpGet(`${baseUrl}/BingSiteAuth.xml`, { timeout: 5e3 });
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: "Add Bing verification: 1) Go to Bing Webmaster Tools, 2) Add your site, 3) Choose verification method (meta tag or XML file), 4) Add the verification code to your site.",
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 cssLinks = $('link[rel="stylesheet"][href]').toArray().slice(0, 3).map((el) => $(el).attr("href")).filter((href) => {
16754
- if (!href || href.startsWith("data:")) return false;
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 cssHostname = new URL(href, url).hostname;
16757
- return cssHostname === baseUrl.hostname;
16884
+ const resolved = new URL(href, url);
16885
+ if (resolved.hostname === baseUrl.hostname) sameOrigin.push(resolved.href);
16886
+ else crossOrigin++;
16758
16887
  } catch {
16759
- return false;
16888
+ crossOrigin++;
16760
16889
  }
16761
16890
  });
16762
- const cssPromises = cssLinks.map(async (cssUrl) => {
16763
- try {
16764
- const fullUrl = new URL(cssUrl, url).href;
16765
- const response = await httpGet(fullUrl, {
16766
- timeout: 3e3,
16767
- // Reduced timeout
16768
- validateStatus: () => true
16769
- });
16770
- if (response.status === 200 && typeof response.data === "string") {
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
- allMediaQueries.push(...queries);
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
- } catch {
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 (!hasMediaQueries) {
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
- function analyzeTrackingVerification(html, url) {
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 hasGscVerification = !!(gscMeta && gscMeta.length > 10);
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 hasBingVerification = !!(bingMeta && bingMeta.length > 10);
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
- gsc: { verified: hasGscVerification, verificationCode: gscMeta },
17513
- bing: { verified: hasBingVerification, verificationCode: bingMeta },
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 detected. GSC provides essential data about your search performance.",
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: "Add your GSC verification meta tag to the <head> section. Get it from search.google.com/search-console.",
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 verification meta tag detected. Bing/Yahoo account for 5-8% of search traffic.",
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: "Add Bing Webmaster Tools verification for insights on Bing/Yahoo traffic. Get it from bing.com/webmasters.",
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
- issues.push({
17556
- code: "GTM_MISSING",
17557
- severity: "notice",
17558
- category: "on-page",
17559
- title: "Google Tag Manager not detected",
17560
- description: "GTM not found. While not required, GTM makes it easier to manage tracking tags without code changes.",
17561
- impact: "Tag management requires developer involvement for each change.",
17562
- howToFix: "Consider using GTM for easier tag management. It allows marketing to add tags without developer help.",
17563
- affectedUrls: [url]
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-O7B2LBKG.mjs";
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 hasAccessibleText = text.length > 0 || ariaLabel || title;
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
- let hasBingSiteAuth = false;
11027
- try {
11028
- const authResponse = await httpGet(`${baseUrl}/BingSiteAuth.xml`, { timeout: 5e3 });
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: "Add Bing verification: 1) Go to Bing Webmaster Tools, 2) Add your site, 3) Choose verification method (meta tag or XML file), 4) Add the verification code to your site.",
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 cssLinks = $('link[rel="stylesheet"][href]').toArray().slice(0, 3).map((el) => $(el).attr("href")).filter((href) => {
13197
- if (!href || href.startsWith("data:")) return false;
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 cssHostname = new URL(href, url).hostname;
13200
- return cssHostname === baseUrl.hostname;
13308
+ const resolved = new URL(href, url);
13309
+ if (resolved.hostname === baseUrl.hostname) sameOrigin.push(resolved.href);
13310
+ else crossOrigin++;
13201
13311
  } catch {
13202
- return false;
13312
+ crossOrigin++;
13203
13313
  }
13204
13314
  });
13205
- const cssPromises = cssLinks.map(async (cssUrl) => {
13206
- try {
13207
- const fullUrl = new URL(cssUrl, url).href;
13208
- const response = await httpGet(fullUrl, {
13209
- timeout: 3e3,
13210
- // Reduced timeout
13211
- validateStatus: () => true
13212
- });
13213
- if (response.status === 200 && typeof response.data === "string") {
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
- allMediaQueries.push(...queries);
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
- } catch {
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 (!hasMediaQueries) {
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
- function analyzeTrackingVerification(html, url) {
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 hasGscVerification = !!(gscMeta && gscMeta.length > 10);
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 hasBingVerification = !!(bingMeta && bingMeta.length > 10);
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
- gsc: { verified: hasGscVerification, verificationCode: gscMeta },
13955
- bing: { verified: hasBingVerification, verificationCode: bingMeta },
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 detected. GSC provides essential data about your search performance.",
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: "Add your GSC verification meta tag to the <head> section. Get it from search.google.com/search-console.",
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 verification meta tag detected. Bing/Yahoo account for 5-8% of search traffic.",
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: "Add Bing Webmaster Tools verification for insights on Bing/Yahoo traffic. Get it from bing.com/webmasters.",
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
- issues.push({
13998
- code: "GTM_MISSING",
13999
- severity: "notice",
14000
- category: "on-page",
14001
- title: "Google Tag Manager not detected",
14002
- description: "GTM not found. While not required, GTM makes it easier to manage tracking tags without code changes.",
14003
- impact: "Tag management requires developer involvement for each change.",
14004
- howToFix: "Consider using GTM for easier tag management. It allows marketing to add tags without developer help.",
14005
- affectedUrls: [url]
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-BYAFIO33.mjs");
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rankcli/agent-runtime",
3
- "version": "0.0.37",
3
+ "version": "0.0.38",
4
4
  "description": "RankCLI agent runtime - executes SEO audits and fixes with AI",
5
5
  "homepage": "https://rankcli.dev",
6
6
  "main": "dist/index.js",