@rankcli/agent-runtime 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +50 -1
- package/dist/index.d.ts +50 -1
- package/dist/index.js +373 -1
- package/dist/index.mjs +369 -1
- package/package.json +1 -1
- package/src/audit/checks/color-contrast.ts +1 -1
- package/src/audit/checks/dom-size.ts +1 -1
- package/src/audit/checks/html-compliance.ts +1 -1
- package/src/audit/checks/image-dimensions.ts +1 -1
- package/src/audit/checks/links.ts +70 -0
- package/src/audit/checks/site-maturity.ts +9 -0
- package/src/audit/runner.test.ts +7 -7
- package/src/audit/types.ts +9 -0
- package/src/fixer.ts +220 -0
- package/src/geo/index.ts +1 -0
- package/src/geo/llm-citation-checker.ts +188 -0
- package/src/keywords/sources/free-sources.ts +1 -1
- package/src/scheduler/scheduled-audit.test.ts +6 -6
package/dist/index.mjs
CHANGED
|
@@ -292,6 +292,15 @@ var ISSUE_DEFINITIONS = {
|
|
|
292
292
|
impact: "Poor user experience and potential trust issues.",
|
|
293
293
|
howToFix: "Update or remove the broken external link."
|
|
294
294
|
},
|
|
295
|
+
JS_ONLY_NAVIGATION: {
|
|
296
|
+
code: "JS_ONLY_NAVIGATION",
|
|
297
|
+
severity: "warning",
|
|
298
|
+
category: "links",
|
|
299
|
+
title: "JavaScript-only navigation detected",
|
|
300
|
+
description: "Navigation elements use onClick handlers without proper <a href> links.",
|
|
301
|
+
impact: "Search engine crawlers cannot follow JavaScript-only navigation. These links are invisible to crawlers, preventing page discovery and indexing.",
|
|
302
|
+
howToFix: "Replace onClick navigation with proper <a href> or <Link> components. In React, use react-router Link instead of navigate() in onClick handlers. Ensure all navigation renders as real anchor tags with href attributes."
|
|
303
|
+
},
|
|
295
304
|
TOO_MANY_LINKS: {
|
|
296
305
|
code: "TOO_MANY_LINKS",
|
|
297
306
|
severity: "notice",
|
|
@@ -2149,6 +2158,7 @@ async function analyzeLinks(html, baseUrl, checkBroken = false) {
|
|
|
2149
2158
|
const external = [];
|
|
2150
2159
|
const brokenInternal = [];
|
|
2151
2160
|
const brokenExternal = [];
|
|
2161
|
+
const jsOnlyNavigation = [];
|
|
2152
2162
|
$("a[href]").each((_, el) => {
|
|
2153
2163
|
const href = $(el).attr("href") || "";
|
|
2154
2164
|
const text = $(el).text().trim();
|
|
@@ -2177,6 +2187,56 @@ async function analyzeLinks(html, baseUrl, checkBroken = false) {
|
|
|
2177
2187
|
}
|
|
2178
2188
|
});
|
|
2179
2189
|
const totalLinks = internal.length + external.length;
|
|
2190
|
+
const navPatterns = /navigate|router|history\.push|location\.href|window\.location|goto|redirect/i;
|
|
2191
|
+
const navTextPatterns = /^(home|about|contact|pricing|blog|docs|features|products?|services?|sign\s*up|log\s*in|register|dashboard|account|settings|help|faq|support|menu|nav)$/i;
|
|
2192
|
+
$("button[onclick], div[onclick], span[onclick], li[onclick]").each((_, el) => {
|
|
2193
|
+
const $el = $(el);
|
|
2194
|
+
const onclick = $el.attr("onclick") || "";
|
|
2195
|
+
const text = $el.text().trim().substring(0, 50);
|
|
2196
|
+
if (navPatterns.test(onclick) || navTextPatterns.test(text)) {
|
|
2197
|
+
jsOnlyNavigation.push({
|
|
2198
|
+
element: el.tagName.toLowerCase(),
|
|
2199
|
+
text: text || "(no text)",
|
|
2200
|
+
reason: "Element uses onClick for navigation instead of <a href>"
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
});
|
|
2204
|
+
$("a[onclick]").each((_, el) => {
|
|
2205
|
+
const $el = $(el);
|
|
2206
|
+
const href = $el.attr("href") || "";
|
|
2207
|
+
const onclick = $el.attr("onclick") || "";
|
|
2208
|
+
const text = $el.text().trim().substring(0, 50);
|
|
2209
|
+
if ((href === "#" || href === "" || href.startsWith("javascript:")) && onclick) {
|
|
2210
|
+
jsOnlyNavigation.push({
|
|
2211
|
+
element: "a",
|
|
2212
|
+
text: text || "(no text)",
|
|
2213
|
+
reason: `Link has href="${href}" with onClick - crawlers cannot follow this`
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2216
|
+
});
|
|
2217
|
+
$("[data-href], [data-to], [data-link], [data-route]").each((_, el) => {
|
|
2218
|
+
const $el = $(el);
|
|
2219
|
+
const tagName = el.tagName.toLowerCase();
|
|
2220
|
+
const text = $el.text().trim().substring(0, 50);
|
|
2221
|
+
if (tagName !== "a" || !$el.attr("href") || $el.attr("href") === "#") {
|
|
2222
|
+
jsOnlyNavigation.push({
|
|
2223
|
+
element: tagName,
|
|
2224
|
+
text: text || "(no text)",
|
|
2225
|
+
reason: "Uses data attribute for routing instead of real href"
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
});
|
|
2229
|
+
if (jsOnlyNavigation.length > 0) {
|
|
2230
|
+
const uniqueNav = jsOnlyNavigation.slice(0, 10);
|
|
2231
|
+
issues.push({
|
|
2232
|
+
...ISSUE_DEFINITIONS.JS_ONLY_NAVIGATION,
|
|
2233
|
+
affectedUrls: [baseUrl],
|
|
2234
|
+
details: {
|
|
2235
|
+
count: jsOnlyNavigation.length,
|
|
2236
|
+
examples: uniqueNav
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2180
2240
|
const internalCount = internal.length;
|
|
2181
2241
|
const externalCount = external.length;
|
|
2182
2242
|
const internalToExternalRatio = externalCount > 0 ? internalCount / externalCount : null;
|
|
@@ -2284,6 +2344,7 @@ async function analyzeLinks(html, baseUrl, checkBroken = false) {
|
|
|
2284
2344
|
totalLinks,
|
|
2285
2345
|
brokenInternal,
|
|
2286
2346
|
brokenExternal,
|
|
2347
|
+
jsOnlyNavigation,
|
|
2287
2348
|
ratio: {
|
|
2288
2349
|
internal: internalCount,
|
|
2289
2350
|
external: externalCount,
|
|
@@ -7647,6 +7708,12 @@ function analyzeContentScience(html, url, targetKeywords = []) {
|
|
|
7647
7708
|
// src/audit/checks/site-maturity.ts
|
|
7648
7709
|
import * as cheerio28 from "cheerio";
|
|
7649
7710
|
async function getSSLCertificateAge(url) {
|
|
7711
|
+
let https;
|
|
7712
|
+
try {
|
|
7713
|
+
https = await import("https");
|
|
7714
|
+
} catch {
|
|
7715
|
+
return null;
|
|
7716
|
+
}
|
|
7650
7717
|
return new Promise((resolve) => {
|
|
7651
7718
|
try {
|
|
7652
7719
|
const parsedUrl = new URL(url);
|
|
@@ -14159,7 +14226,7 @@ function analyzeStructure(content) {
|
|
|
14159
14226
|
import * as cheerio49 from "cheerio";
|
|
14160
14227
|
async function analyzeHtmlCompliance(html, url, headers) {
|
|
14161
14228
|
const issues = [];
|
|
14162
|
-
const $ = cheerio49.load(html
|
|
14229
|
+
const $ = cheerio49.load(html);
|
|
14163
14230
|
const parsedUrl = new URL(url);
|
|
14164
14231
|
const doctypeMatch = html.match(/<!DOCTYPE\s+([^>]+)>/i);
|
|
14165
14232
|
const hasDoctype = doctypeMatch !== null;
|
|
@@ -25611,26 +25678,71 @@ async function generateFixForIssue(issue, context) {
|
|
|
25611
25678
|
const fullUrl = url || "https://example.com";
|
|
25612
25679
|
const siteName = new URL(fullUrl).hostname.replace("www.", "");
|
|
25613
25680
|
switch (issue.code) {
|
|
25681
|
+
// Title issues
|
|
25614
25682
|
case "MISSING_TITLE":
|
|
25683
|
+
case "TITLE_KEYWORD_MISMATCH":
|
|
25684
|
+
case "TITLE_H1_KEYWORD_MISMATCH":
|
|
25685
|
+
case "OUTDATED_YEAR_IN_TITLE":
|
|
25615
25686
|
return generateTitleFix(context, siteName);
|
|
25687
|
+
// Meta description issues
|
|
25616
25688
|
case "MISSING_META_DESC":
|
|
25617
25689
|
return generateMetaDescFix(context, siteName);
|
|
25690
|
+
// Canonical issues
|
|
25618
25691
|
case "MISSING_CANONICAL":
|
|
25692
|
+
case "CANONICAL_NO_HTTPS_REDIRECT":
|
|
25619
25693
|
return generateCanonicalFix(context, fullUrl);
|
|
25694
|
+
// Viewport issues
|
|
25620
25695
|
case "MISSING_VIEWPORT":
|
|
25696
|
+
case "HTML_NO_VIEWPORT":
|
|
25697
|
+
case "RESPONSIVE_NO_VIEWPORT":
|
|
25621
25698
|
return generateViewportFix(context);
|
|
25699
|
+
// Open Graph issues
|
|
25622
25700
|
case "MISSING_OG_TAGS":
|
|
25623
25701
|
return generateOGFix(context, siteName, fullUrl);
|
|
25702
|
+
// Twitter Card issues
|
|
25624
25703
|
case "MISSING_TWITTER_CARD":
|
|
25625
25704
|
return generateTwitterFix(context, siteName);
|
|
25705
|
+
// Schema/structured data issues
|
|
25626
25706
|
case "MISSING_SCHEMA":
|
|
25707
|
+
case "SCHEMA_ORG_MISSING":
|
|
25708
|
+
case "NO_ORGANIZATION_SCHEMA":
|
|
25709
|
+
case "NO_ENTITY_SCHEMA":
|
|
25710
|
+
case "FAQ_SCHEMA_MISSING":
|
|
25627
25711
|
return generateSchemaFix(context, siteName, fullUrl);
|
|
25712
|
+
// Robots.txt issues
|
|
25628
25713
|
case "MISSING_ROBOTS":
|
|
25714
|
+
case "ROBOTS_TXT_WARNINGS":
|
|
25715
|
+
case "ROBOTS_TXT_INVALID_SYNTAX":
|
|
25629
25716
|
return generateRobotsFix(context, fullUrl);
|
|
25717
|
+
// Sitemap issues
|
|
25630
25718
|
case "MISSING_SITEMAP":
|
|
25719
|
+
case "BING_SITEMAP_MISSING":
|
|
25631
25720
|
return generateSitemapFix(context, fullUrl);
|
|
25721
|
+
// H1 issues
|
|
25632
25722
|
case "MISSING_H1":
|
|
25723
|
+
case "NO_VISIBLE_HEADLINE":
|
|
25724
|
+
case "H1_MISSING_KEYWORD":
|
|
25633
25725
|
return await generateH1Fix({ cwd });
|
|
25726
|
+
// SPA-specific: add meta management library recommendation
|
|
25727
|
+
case "SPA_NO_META_MANAGEMENT":
|
|
25728
|
+
return generateSPAMetaFix(context, framework);
|
|
25729
|
+
// Preconnect issues
|
|
25730
|
+
case "GOOGLE_FONTS_NO_PRECONNECT":
|
|
25731
|
+
case "MISSING_PRECONNECT":
|
|
25732
|
+
return generatePreconnectFix(context);
|
|
25733
|
+
// Favicon/icons
|
|
25734
|
+
case "HTML_NO_FAVICON":
|
|
25735
|
+
case "HTML_NO_APPLE_TOUCH_ICON":
|
|
25736
|
+
return generateFaviconFix(context);
|
|
25737
|
+
// Charset/lang
|
|
25738
|
+
case "HTML_NO_CHARSET":
|
|
25739
|
+
case "HTML_NOT_UTF8":
|
|
25740
|
+
return generateCharsetFix(context);
|
|
25741
|
+
case "HTML_NO_LANG":
|
|
25742
|
+
return generateLangFix(context);
|
|
25743
|
+
// AI/LLMs.txt
|
|
25744
|
+
case "AI_NO_LLMS_TXT":
|
|
25745
|
+
return generateLlmsTxtFix(context, siteName, fullUrl);
|
|
25634
25746
|
default:
|
|
25635
25747
|
return null;
|
|
25636
25748
|
}
|
|
@@ -25859,6 +25971,159 @@ function generateSitemapFix(context, url) {
|
|
|
25859
25971
|
explanation: "Created sitemap.xml to help search engines discover all pages"
|
|
25860
25972
|
};
|
|
25861
25973
|
}
|
|
25974
|
+
function generateSPAMetaFix(context, framework) {
|
|
25975
|
+
const { htmlPath } = context;
|
|
25976
|
+
if (framework.name.toLowerCase().includes("react") || framework.name === "Unknown") {
|
|
25977
|
+
return {
|
|
25978
|
+
issue: { code: "SPA_NO_META_MANAGEMENT", message: "SPA without dynamic meta tag management", severity: "warning" },
|
|
25979
|
+
file: "src/components/SEOHead.tsx",
|
|
25980
|
+
before: null,
|
|
25981
|
+
after: `import { Helmet } from 'react-helmet-async';
|
|
25982
|
+
|
|
25983
|
+
interface SEOHeadProps {
|
|
25984
|
+
title?: string;
|
|
25985
|
+
description?: string;
|
|
25986
|
+
image?: string;
|
|
25987
|
+
url?: string;
|
|
25988
|
+
}
|
|
25989
|
+
|
|
25990
|
+
export function SEOHead({
|
|
25991
|
+
title = 'Your Site Name',
|
|
25992
|
+
description = 'Your site description',
|
|
25993
|
+
image = '/og-image.png',
|
|
25994
|
+
url = window.location.href,
|
|
25995
|
+
}: SEOHeadProps) {
|
|
25996
|
+
return (
|
|
25997
|
+
<Helmet>
|
|
25998
|
+
<title>{title}</title>
|
|
25999
|
+
<meta name="description" content={description} />
|
|
26000
|
+
<link rel="canonical" href={url} />
|
|
26001
|
+
|
|
26002
|
+
{/* Open Graph */}
|
|
26003
|
+
<meta property="og:title" content={title} />
|
|
26004
|
+
<meta property="og:description" content={description} />
|
|
26005
|
+
<meta property="og:image" content={image} />
|
|
26006
|
+
<meta property="og:url" content={url} />
|
|
26007
|
+
<meta property="og:type" content="website" />
|
|
26008
|
+
|
|
26009
|
+
{/* Twitter */}
|
|
26010
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
26011
|
+
<meta name="twitter:title" content={title} />
|
|
26012
|
+
<meta name="twitter:description" content={description} />
|
|
26013
|
+
<meta name="twitter:image" content={image} />
|
|
26014
|
+
</Helmet>
|
|
26015
|
+
);
|
|
26016
|
+
}`,
|
|
26017
|
+
explanation: "Created SEOHead component using react-helmet-async for dynamic meta tags. Install: npm install react-helmet-async"
|
|
26018
|
+
};
|
|
26019
|
+
}
|
|
26020
|
+
return {
|
|
26021
|
+
issue: { code: "SPA_NO_META_MANAGEMENT", message: "SPA without meta management", severity: "warning" },
|
|
26022
|
+
file: htmlPath,
|
|
26023
|
+
before: null,
|
|
26024
|
+
after: "<!-- Add a meta management library for your framework -->",
|
|
26025
|
+
explanation: `Add dynamic meta tag management for ${framework.name}`,
|
|
26026
|
+
skipped: true,
|
|
26027
|
+
skipReason: `Framework-specific solution needed for ${framework.name}`
|
|
26028
|
+
};
|
|
26029
|
+
}
|
|
26030
|
+
function generatePreconnectFix(context) {
|
|
26031
|
+
const { htmlPath, htmlContent } = context;
|
|
26032
|
+
const preconnects = `<!-- Preconnect to external origins -->
|
|
26033
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
26034
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />`;
|
|
26035
|
+
return {
|
|
26036
|
+
issue: { code: "MISSING_PRECONNECT", message: "Missing preconnect hints", severity: "info" },
|
|
26037
|
+
file: htmlPath,
|
|
26038
|
+
before: "<head>",
|
|
26039
|
+
after: `<head>
|
|
26040
|
+
${preconnects}`,
|
|
26041
|
+
explanation: "Added preconnect hints to speed up loading of external resources"
|
|
26042
|
+
};
|
|
26043
|
+
}
|
|
26044
|
+
function generateFaviconFix(context) {
|
|
26045
|
+
const { htmlPath } = context;
|
|
26046
|
+
const faviconTags = `<!-- Favicons -->
|
|
26047
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
26048
|
+
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
|
26049
|
+
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />`;
|
|
26050
|
+
return {
|
|
26051
|
+
issue: { code: "HTML_NO_FAVICON", message: "Missing favicon", severity: "info" },
|
|
26052
|
+
file: htmlPath,
|
|
26053
|
+
before: "<head>",
|
|
26054
|
+
after: `<head>
|
|
26055
|
+
${faviconTags}`,
|
|
26056
|
+
explanation: "Added favicon links. Create favicon files in public/ directory."
|
|
26057
|
+
};
|
|
26058
|
+
}
|
|
26059
|
+
function generateCharsetFix(context) {
|
|
26060
|
+
const { htmlPath, htmlContent } = context;
|
|
26061
|
+
if (htmlContent.includes("charset")) {
|
|
26062
|
+
return {
|
|
26063
|
+
issue: { code: "HTML_NO_CHARSET", message: "Missing charset", severity: "warning" },
|
|
26064
|
+
file: htmlPath,
|
|
26065
|
+
before: null,
|
|
26066
|
+
after: '<meta charset="UTF-8" />',
|
|
26067
|
+
skipped: true,
|
|
26068
|
+
skipReason: "Charset already defined",
|
|
26069
|
+
explanation: "Charset is already present"
|
|
26070
|
+
};
|
|
26071
|
+
}
|
|
26072
|
+
return {
|
|
26073
|
+
issue: { code: "HTML_NO_CHARSET", message: "Missing charset declaration", severity: "warning" },
|
|
26074
|
+
file: htmlPath,
|
|
26075
|
+
before: "<head>",
|
|
26076
|
+
after: '<head>\n <meta charset="UTF-8" />',
|
|
26077
|
+
explanation: "Added UTF-8 charset declaration as first element in head"
|
|
26078
|
+
};
|
|
26079
|
+
}
|
|
26080
|
+
function generateLangFix(context) {
|
|
26081
|
+
const { htmlPath, htmlContent } = context;
|
|
26082
|
+
if (htmlContent.includes("<html") && !htmlContent.includes("lang=")) {
|
|
26083
|
+
return {
|
|
26084
|
+
issue: { code: "HTML_NO_LANG", message: "Missing lang attribute", severity: "warning" },
|
|
26085
|
+
file: htmlPath,
|
|
26086
|
+
before: "<html>",
|
|
26087
|
+
after: '<html lang="en">',
|
|
26088
|
+
explanation: "Added lang attribute for accessibility and SEO"
|
|
26089
|
+
};
|
|
26090
|
+
}
|
|
26091
|
+
return {
|
|
26092
|
+
issue: { code: "HTML_NO_LANG", message: "Missing lang attribute", severity: "warning" },
|
|
26093
|
+
file: htmlPath,
|
|
26094
|
+
before: "<html",
|
|
26095
|
+
after: '<html lang="en"',
|
|
26096
|
+
explanation: "Added lang attribute for accessibility and SEO"
|
|
26097
|
+
};
|
|
26098
|
+
}
|
|
26099
|
+
function generateLlmsTxtFix(context, siteName, url) {
|
|
26100
|
+
const llmsTxt = `# ${siteName}
|
|
26101
|
+
> ${siteName} - A brief description of your product/service
|
|
26102
|
+
|
|
26103
|
+
## About
|
|
26104
|
+
${siteName} is... [Add your description here]
|
|
26105
|
+
|
|
26106
|
+
## Features
|
|
26107
|
+
- Feature 1
|
|
26108
|
+
- Feature 2
|
|
26109
|
+
- Feature 3
|
|
26110
|
+
|
|
26111
|
+
## Links
|
|
26112
|
+
- Homepage: ${url}
|
|
26113
|
+
- Documentation: ${url}/docs
|
|
26114
|
+
- API: ${url}/api
|
|
26115
|
+
|
|
26116
|
+
## Contact
|
|
26117
|
+
- Email: hello@${new URL(url).hostname}
|
|
26118
|
+
`;
|
|
26119
|
+
return {
|
|
26120
|
+
issue: { code: "AI_NO_LLMS_TXT", message: "No llms.txt file for AI crawlers", severity: "info" },
|
|
26121
|
+
file: "public/llms.txt",
|
|
26122
|
+
before: null,
|
|
26123
|
+
after: llmsTxt,
|
|
26124
|
+
explanation: "Created llms.txt to help AI systems understand your site. Customize the content."
|
|
26125
|
+
};
|
|
26126
|
+
}
|
|
25862
26127
|
async function applyFixes(fixes, options) {
|
|
25863
26128
|
const { cwd, dryRun = false } = options;
|
|
25864
26129
|
const applied = [];
|
|
@@ -27657,6 +27922,105 @@ function escapeRegex2(str) {
|
|
|
27657
27922
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
27658
27923
|
}
|
|
27659
27924
|
|
|
27925
|
+
// src/geo/llm-citation-checker.ts
|
|
27926
|
+
var PROVIDER_NAMES = {
|
|
27927
|
+
openai: "ChatGPT",
|
|
27928
|
+
anthropic: "Claude",
|
|
27929
|
+
google: "Gemini",
|
|
27930
|
+
perplexity: "Perplexity"
|
|
27931
|
+
};
|
|
27932
|
+
var DEFAULT_PROVIDERS = ["openai", "anthropic", "google", "perplexity"];
|
|
27933
|
+
function generateRecommendationQueries(brand, industry) {
|
|
27934
|
+
const queries = [
|
|
27935
|
+
`What are the best ${brand.toLowerCase()} alternatives?`,
|
|
27936
|
+
`Can you recommend tools similar to ${brand}?`,
|
|
27937
|
+
`What do you think about ${brand}?`
|
|
27938
|
+
];
|
|
27939
|
+
if (industry) {
|
|
27940
|
+
queries.push(`What are the best ${industry} tools?`);
|
|
27941
|
+
queries.push(`Recommend a good ${industry} solution`);
|
|
27942
|
+
}
|
|
27943
|
+
return queries;
|
|
27944
|
+
}
|
|
27945
|
+
async function checkLLMCitations(brand, domain, options = {}) {
|
|
27946
|
+
const {
|
|
27947
|
+
providers = DEFAULT_PROVIDERS,
|
|
27948
|
+
queries = generateRecommendationQueries(brand),
|
|
27949
|
+
...trackingOptions
|
|
27950
|
+
} = options;
|
|
27951
|
+
const brandConfig = {
|
|
27952
|
+
brandName: brand,
|
|
27953
|
+
domains: [domain],
|
|
27954
|
+
alternativeNames: [brand.toLowerCase(), brand.toUpperCase()]
|
|
27955
|
+
};
|
|
27956
|
+
const query = queries[0];
|
|
27957
|
+
const results = await trackLLMVisibility(
|
|
27958
|
+
{
|
|
27959
|
+
keyword: query,
|
|
27960
|
+
brand: brandConfig,
|
|
27961
|
+
providers
|
|
27962
|
+
},
|
|
27963
|
+
trackingOptions
|
|
27964
|
+
);
|
|
27965
|
+
return results.map((result) => ({
|
|
27966
|
+
provider: result.provider,
|
|
27967
|
+
providerName: PROVIDER_NAMES[result.provider],
|
|
27968
|
+
mentioned: result.mentioned,
|
|
27969
|
+
position: result.position,
|
|
27970
|
+
sentiment: result.sentiment,
|
|
27971
|
+
context: result.contextSnippet || null,
|
|
27972
|
+
score: result.score,
|
|
27973
|
+
error: result.error
|
|
27974
|
+
}));
|
|
27975
|
+
}
|
|
27976
|
+
function calculateAIVisibilityScore(results) {
|
|
27977
|
+
if (results.length === 0) return 0;
|
|
27978
|
+
const mentionedCount = results.filter((r) => r.mentioned && !r.error).length;
|
|
27979
|
+
const validResults = results.filter((r) => !r.error);
|
|
27980
|
+
if (validResults.length === 0) return 0;
|
|
27981
|
+
let score = mentionedCount / validResults.length * 100;
|
|
27982
|
+
const topPositions = results.filter(
|
|
27983
|
+
(r) => r.mentioned && r.position !== null && r.position <= 3
|
|
27984
|
+
);
|
|
27985
|
+
if (topPositions.length > 0) {
|
|
27986
|
+
score = Math.min(100, score + 10);
|
|
27987
|
+
}
|
|
27988
|
+
const positiveResults = results.filter((r) => r.sentiment === "positive");
|
|
27989
|
+
if (positiveResults.length > mentionedCount / 2) {
|
|
27990
|
+
score = Math.min(100, score + 5);
|
|
27991
|
+
}
|
|
27992
|
+
return Math.round(score);
|
|
27993
|
+
}
|
|
27994
|
+
function getAIVisibilitySummary(results) {
|
|
27995
|
+
const score = calculateAIVisibilityScore(results);
|
|
27996
|
+
const mentionedIn = results.filter((r) => r.mentioned).map((r) => r.providerName);
|
|
27997
|
+
const notMentionedIn = results.filter((r) => !r.mentioned && !r.error).map((r) => r.providerName);
|
|
27998
|
+
const positions = results.filter((r) => r.position !== null).map((r) => r.position);
|
|
27999
|
+
const bestPosition = positions.length > 0 ? Math.min(...positions) : null;
|
|
28000
|
+
const sentiments = results.filter((r) => r.sentiment !== null).map((r) => r.sentiment);
|
|
28001
|
+
let overallSentiment = null;
|
|
28002
|
+
if (sentiments.length > 0) {
|
|
28003
|
+
const positiveCount = sentiments.filter((s) => s === "positive").length;
|
|
28004
|
+
const negativeCount = sentiments.filter((s) => s === "negative").length;
|
|
28005
|
+
if (positiveCount > 0 && negativeCount > 0) {
|
|
28006
|
+
overallSentiment = "mixed";
|
|
28007
|
+
} else if (positiveCount > negativeCount) {
|
|
28008
|
+
overallSentiment = "positive";
|
|
28009
|
+
} else if (negativeCount > positiveCount) {
|
|
28010
|
+
overallSentiment = "negative";
|
|
28011
|
+
} else {
|
|
28012
|
+
overallSentiment = "neutral";
|
|
28013
|
+
}
|
|
28014
|
+
}
|
|
28015
|
+
return {
|
|
28016
|
+
score,
|
|
28017
|
+
mentionedIn,
|
|
28018
|
+
notMentionedIn,
|
|
28019
|
+
bestPosition,
|
|
28020
|
+
overallSentiment
|
|
28021
|
+
};
|
|
28022
|
+
}
|
|
28023
|
+
|
|
27660
28024
|
// src/frameworks/index.ts
|
|
27661
28025
|
var frameworks_exports = {};
|
|
27662
28026
|
__export(frameworks_exports, {
|
|
@@ -28411,6 +28775,7 @@ export {
|
|
|
28411
28775
|
applyFixes,
|
|
28412
28776
|
buildGSCApiRequest,
|
|
28413
28777
|
buildGSCRequest,
|
|
28778
|
+
calculateAIVisibilityScore,
|
|
28414
28779
|
calculateBM252 as calculateBM25,
|
|
28415
28780
|
calculateTFIDF as calculateKeywordTFIDF,
|
|
28416
28781
|
calculateNextRun,
|
|
@@ -28425,6 +28790,7 @@ export {
|
|
|
28425
28790
|
checkGitHubCLI,
|
|
28426
28791
|
checkInternalRedirects,
|
|
28427
28792
|
checkJSRenderingRatio,
|
|
28793
|
+
checkLLMCitations,
|
|
28428
28794
|
checkLlmsTxt,
|
|
28429
28795
|
checkMobileResources,
|
|
28430
28796
|
checkPlaintextEmails,
|
|
@@ -28528,6 +28894,7 @@ export {
|
|
|
28528
28894
|
generatePDFReport,
|
|
28529
28895
|
generatePRDescription,
|
|
28530
28896
|
generateReactHelmetSocialMeta,
|
|
28897
|
+
generateRecommendationQueries,
|
|
28531
28898
|
generateRemixMeta,
|
|
28532
28899
|
generateSecretsDoc,
|
|
28533
28900
|
generateSocialMetaFix,
|
|
@@ -28535,6 +28902,7 @@ export {
|
|
|
28535
28902
|
generateUncertaintyQuestions,
|
|
28536
28903
|
generateWizardQuestions,
|
|
28537
28904
|
generateWorkflow,
|
|
28905
|
+
getAIVisibilitySummary,
|
|
28538
28906
|
getAutocompleteSuggestions,
|
|
28539
28907
|
getDateRange,
|
|
28540
28908
|
getExpandedSuggestions,
|
package/package.json
CHANGED
|
@@ -145,7 +145,7 @@ export function analyzeColorContrast(
|
|
|
145
145
|
failedChecks++;
|
|
146
146
|
|
|
147
147
|
// Create selector for identification
|
|
148
|
-
const tagName = (element as
|
|
148
|
+
const tagName = (element as any).name;
|
|
149
149
|
const id = $el.attr('id');
|
|
150
150
|
const className = $el.attr('class')?.split(' ')[0];
|
|
151
151
|
const selector = id ? `#${id}` : (className ? `${tagName}.${className}` : tagName);
|
|
@@ -69,7 +69,7 @@ export function analyzeDomSize(
|
|
|
69
69
|
let maxDepth = 0;
|
|
70
70
|
let deepestPath: string[] = [];
|
|
71
71
|
|
|
72
|
-
function getDepth(element: cheerio.Cheerio<
|
|
72
|
+
function getDepth(element: cheerio.Cheerio<any>, path: string[]): number {
|
|
73
73
|
const children = element.children();
|
|
74
74
|
if (children.length === 0) {
|
|
75
75
|
if (path.length > maxDepth) {
|
|
@@ -48,7 +48,7 @@ export async function analyzeHtmlCompliance(
|
|
|
48
48
|
headers?: Record<string, string>
|
|
49
49
|
): Promise<{ issues: AuditIssue[]; data: HtmlComplianceData }> {
|
|
50
50
|
const issues: AuditIssue[] = [];
|
|
51
|
-
const $ = cheerio.load(html
|
|
51
|
+
const $ = cheerio.load(html);
|
|
52
52
|
const parsedUrl = new URL(url);
|
|
53
53
|
|
|
54
54
|
// === DOCTYPE CHECK ===
|
|
@@ -77,7 +77,7 @@ export function analyzeImageDimensions(
|
|
|
77
77
|
if (src && !src.includes('data:image/') && !src.includes('placeholder')) {
|
|
78
78
|
// Get context (parent element for identification)
|
|
79
79
|
const parent = $img.parent();
|
|
80
|
-
const parentTag = parent.length > 0 ? (parent[0] as
|
|
80
|
+
const parentTag = parent.length > 0 ? (parent[0] as any).name || '' : '';
|
|
81
81
|
const parentClass = parent.attr('class')?.split(' ')[0] || '';
|
|
82
82
|
const context = parentClass ? `${parentTag}.${parentClass}` : parentTag;
|
|
83
83
|
|
|
@@ -9,6 +9,7 @@ export interface LinkData {
|
|
|
9
9
|
totalLinks: number;
|
|
10
10
|
brokenInternal: string[];
|
|
11
11
|
brokenExternal: string[];
|
|
12
|
+
jsOnlyNavigation: { element: string; text: string; reason: string }[];
|
|
12
13
|
ratio: {
|
|
13
14
|
internal: number;
|
|
14
15
|
external: number;
|
|
@@ -29,6 +30,7 @@ export async function analyzeLinks(
|
|
|
29
30
|
const external: LinkData['external'] = [];
|
|
30
31
|
const brokenInternal: string[] = [];
|
|
31
32
|
const brokenExternal: string[] = [];
|
|
33
|
+
const jsOnlyNavigation: LinkData['jsOnlyNavigation'] = [];
|
|
32
34
|
|
|
33
35
|
// Extract all links
|
|
34
36
|
$('a[href]').each((_, el) => {
|
|
@@ -67,6 +69,73 @@ export async function analyzeLinks(
|
|
|
67
69
|
|
|
68
70
|
const totalLinks = internal.length + external.length;
|
|
69
71
|
|
|
72
|
+
// Check for JavaScript-only navigation
|
|
73
|
+
// Look for elements with onClick that appear to be navigation
|
|
74
|
+
const navPatterns = /navigate|router|history\.push|location\.href|window\.location|goto|redirect/i;
|
|
75
|
+
const navTextPatterns = /^(home|about|contact|pricing|blog|docs|features|products?|services?|sign\s*up|log\s*in|register|dashboard|account|settings|help|faq|support|menu|nav)$/i;
|
|
76
|
+
|
|
77
|
+
// Check buttons and other elements with onClick
|
|
78
|
+
$('button[onclick], div[onclick], span[onclick], li[onclick]').each((_, el) => {
|
|
79
|
+
const $el = $(el);
|
|
80
|
+
const onclick = $el.attr('onclick') || '';
|
|
81
|
+
const text = $el.text().trim().substring(0, 50);
|
|
82
|
+
|
|
83
|
+
// Check if onClick contains navigation-like code
|
|
84
|
+
if (navPatterns.test(onclick) || navTextPatterns.test(text)) {
|
|
85
|
+
jsOnlyNavigation.push({
|
|
86
|
+
element: el.tagName.toLowerCase(),
|
|
87
|
+
text: text || '(no text)',
|
|
88
|
+
reason: 'Element uses onClick for navigation instead of <a href>',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Check <a> tags with non-functional href but onClick (common anti-pattern)
|
|
94
|
+
$('a[onclick]').each((_, el) => {
|
|
95
|
+
const $el = $(el);
|
|
96
|
+
const href = $el.attr('href') || '';
|
|
97
|
+
const onclick = $el.attr('onclick') || '';
|
|
98
|
+
const text = $el.text().trim().substring(0, 50);
|
|
99
|
+
|
|
100
|
+
// href="#" or "javascript:" with onClick is a red flag
|
|
101
|
+
if ((href === '#' || href === '' || href.startsWith('javascript:')) && onclick) {
|
|
102
|
+
jsOnlyNavigation.push({
|
|
103
|
+
element: 'a',
|
|
104
|
+
text: text || '(no text)',
|
|
105
|
+
reason: `Link has href="${href}" with onClick - crawlers cannot follow this`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Check for React/Vue patterns in inline scripts (data-* attributes often used)
|
|
111
|
+
$('[data-href], [data-to], [data-link], [data-route]').each((_, el) => {
|
|
112
|
+
const $el = $(el);
|
|
113
|
+
const tagName = el.tagName.toLowerCase();
|
|
114
|
+
const text = $el.text().trim().substring(0, 50);
|
|
115
|
+
|
|
116
|
+
// If it's not an <a> tag with a real href, it's JS-only navigation
|
|
117
|
+
if (tagName !== 'a' || !$el.attr('href') || $el.attr('href') === '#') {
|
|
118
|
+
jsOnlyNavigation.push({
|
|
119
|
+
element: tagName,
|
|
120
|
+
text: text || '(no text)',
|
|
121
|
+
reason: 'Uses data attribute for routing instead of real href',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Report JS-only navigation issues (deduplicated, limit to 10)
|
|
127
|
+
if (jsOnlyNavigation.length > 0) {
|
|
128
|
+
const uniqueNav = jsOnlyNavigation.slice(0, 10);
|
|
129
|
+
issues.push({
|
|
130
|
+
...ISSUE_DEFINITIONS.JS_ONLY_NAVIGATION,
|
|
131
|
+
affectedUrls: [baseUrl],
|
|
132
|
+
details: {
|
|
133
|
+
count: jsOnlyNavigation.length,
|
|
134
|
+
examples: uniqueNav,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
70
139
|
// Calculate link ratio
|
|
71
140
|
const internalCount = internal.length;
|
|
72
141
|
const externalCount = external.length;
|
|
@@ -193,6 +262,7 @@ export async function analyzeLinks(
|
|
|
193
262
|
totalLinks,
|
|
194
263
|
brokenInternal,
|
|
195
264
|
brokenExternal,
|
|
265
|
+
jsOnlyNavigation,
|
|
196
266
|
ratio: {
|
|
197
267
|
internal: internalCount,
|
|
198
268
|
external: externalCount,
|
|
@@ -38,6 +38,15 @@ export interface SiteMaturityData {
|
|
|
38
38
|
* Estimate domain age from SSL certificate issuance date
|
|
39
39
|
*/
|
|
40
40
|
export async function getSSLCertificateAge(url: string): Promise<number | null> {
|
|
41
|
+
// Dynamically import https module (Node.js only)
|
|
42
|
+
let https: typeof import('https');
|
|
43
|
+
try {
|
|
44
|
+
https = await import('https');
|
|
45
|
+
} catch {
|
|
46
|
+
// Not available in Deno/browser
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
41
50
|
return new Promise((resolve) => {
|
|
42
51
|
try {
|
|
43
52
|
const parsedUrl = new URL(url);
|