@rankcli/agent-runtime 0.0.1 → 0.0.3
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 +406 -1
- package/dist/index.mjs +402 -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 +250 -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,104 @@ 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_MISSING":
|
|
25684
|
+
// Full audit code
|
|
25685
|
+
case "TITLE_KEYWORD_MISMATCH":
|
|
25686
|
+
case "TITLE_H1_KEYWORD_MISMATCH":
|
|
25687
|
+
case "OUTDATED_YEAR_IN_TITLE":
|
|
25615
25688
|
return generateTitleFix(context, siteName);
|
|
25689
|
+
// Meta description issues
|
|
25616
25690
|
case "MISSING_META_DESC":
|
|
25691
|
+
case "META_DESC_MISSING":
|
|
25617
25692
|
return generateMetaDescFix(context, siteName);
|
|
25693
|
+
// Canonical issues
|
|
25618
25694
|
case "MISSING_CANONICAL":
|
|
25695
|
+
case "CANONICAL_NO_HTTPS_REDIRECT":
|
|
25619
25696
|
return generateCanonicalFix(context, fullUrl);
|
|
25697
|
+
// Viewport issues
|
|
25620
25698
|
case "MISSING_VIEWPORT":
|
|
25699
|
+
case "HTML_NO_VIEWPORT":
|
|
25700
|
+
case "RESPONSIVE_NO_VIEWPORT":
|
|
25621
25701
|
return generateViewportFix(context);
|
|
25702
|
+
// Open Graph issues
|
|
25622
25703
|
case "MISSING_OG_TAGS":
|
|
25704
|
+
case "OG_TITLE_MISSING":
|
|
25705
|
+
case "OG_DESC_MISSING":
|
|
25706
|
+
case "OG_IMAGE_MISSING":
|
|
25707
|
+
case "OG_URL_MISSING":
|
|
25623
25708
|
return generateOGFix(context, siteName, fullUrl);
|
|
25709
|
+
// Twitter Card issues
|
|
25624
25710
|
case "MISSING_TWITTER_CARD":
|
|
25711
|
+
case "TWITTER_CARD_MISSING":
|
|
25712
|
+
case "TWITTER_TITLE_MISSING":
|
|
25713
|
+
case "TWITTER_DESC_MISSING":
|
|
25714
|
+
case "TWITTER_IMAGE_MISSING":
|
|
25625
25715
|
return generateTwitterFix(context, siteName);
|
|
25716
|
+
// Schema/structured data issues
|
|
25626
25717
|
case "MISSING_SCHEMA":
|
|
25718
|
+
case "SCHEMA_MISSING":
|
|
25719
|
+
// Full audit code
|
|
25720
|
+
case "SCHEMA_ORG_MISSING":
|
|
25721
|
+
case "NO_ORGANIZATION_SCHEMA":
|
|
25722
|
+
case "NO_ENTITY_SCHEMA":
|
|
25723
|
+
case "FAQ_SCHEMA_MISSING":
|
|
25627
25724
|
return generateSchemaFix(context, siteName, fullUrl);
|
|
25725
|
+
// Robots.txt issues
|
|
25628
25726
|
case "MISSING_ROBOTS":
|
|
25727
|
+
case "ROBOTS_TXT_MISSING":
|
|
25728
|
+
// Full audit code
|
|
25729
|
+
case "ROBOTS_TXT_WARNINGS":
|
|
25730
|
+
case "ROBOTS_TXT_INVALID_SYNTAX":
|
|
25629
25731
|
return generateRobotsFix(context, fullUrl);
|
|
25732
|
+
// Sitemap issues
|
|
25630
25733
|
case "MISSING_SITEMAP":
|
|
25734
|
+
case "SITEMAP_MISSING":
|
|
25735
|
+
// Full audit code
|
|
25736
|
+
case "BING_SITEMAP_MISSING":
|
|
25631
25737
|
return generateSitemapFix(context, fullUrl);
|
|
25738
|
+
// H1 issues
|
|
25632
25739
|
case "MISSING_H1":
|
|
25740
|
+
case "NO_VISIBLE_HEADLINE":
|
|
25741
|
+
case "H1_MISSING_KEYWORD":
|
|
25633
25742
|
return await generateH1Fix({ cwd });
|
|
25743
|
+
// SPA-specific: add meta management library recommendation
|
|
25744
|
+
case "SPA_NO_META_MANAGEMENT":
|
|
25745
|
+
case "SPA_WITHOUT_SSR":
|
|
25746
|
+
case "CLIENT_SIDE_RENDERING":
|
|
25747
|
+
return generateSPAMetaFix(context, framework);
|
|
25748
|
+
// Internal links - suggest adding links
|
|
25749
|
+
case "NO_INTERNAL_LINKS":
|
|
25750
|
+
case "LINKS_NO_INTERNAL":
|
|
25751
|
+
case "NO_CONTENT_INTERNAL_LINKS":
|
|
25752
|
+
return {
|
|
25753
|
+
issue: { code: issue.code, message: "No internal links found", severity: "warning" },
|
|
25754
|
+
file: "src/components/Footer.tsx",
|
|
25755
|
+
before: null,
|
|
25756
|
+
after: `// Add internal navigation links to your footer or content
|
|
25757
|
+
// Example: <Link to="/about">About</Link>`,
|
|
25758
|
+
explanation: "Add internal links to help search engines discover your content",
|
|
25759
|
+
skipped: true,
|
|
25760
|
+
skipReason: "Requires manual content updates - add links to your navigation and content"
|
|
25761
|
+
};
|
|
25762
|
+
// Preconnect issues
|
|
25763
|
+
case "GOOGLE_FONTS_NO_PRECONNECT":
|
|
25764
|
+
case "MISSING_PRECONNECT":
|
|
25765
|
+
return generatePreconnectFix(context);
|
|
25766
|
+
// Favicon/icons
|
|
25767
|
+
case "HTML_NO_FAVICON":
|
|
25768
|
+
case "HTML_NO_APPLE_TOUCH_ICON":
|
|
25769
|
+
return generateFaviconFix(context);
|
|
25770
|
+
// Charset/lang
|
|
25771
|
+
case "HTML_NO_CHARSET":
|
|
25772
|
+
case "HTML_NOT_UTF8":
|
|
25773
|
+
return generateCharsetFix(context);
|
|
25774
|
+
case "HTML_NO_LANG":
|
|
25775
|
+
return generateLangFix(context);
|
|
25776
|
+
// AI/LLMs.txt
|
|
25777
|
+
case "AI_NO_LLMS_TXT":
|
|
25778
|
+
return generateLlmsTxtFix(context, siteName, fullUrl);
|
|
25634
25779
|
default:
|
|
25635
25780
|
return null;
|
|
25636
25781
|
}
|
|
@@ -25859,6 +26004,159 @@ function generateSitemapFix(context, url) {
|
|
|
25859
26004
|
explanation: "Created sitemap.xml to help search engines discover all pages"
|
|
25860
26005
|
};
|
|
25861
26006
|
}
|
|
26007
|
+
function generateSPAMetaFix(context, framework) {
|
|
26008
|
+
const { htmlPath } = context;
|
|
26009
|
+
if (framework.name.toLowerCase().includes("react") || framework.name === "Unknown") {
|
|
26010
|
+
return {
|
|
26011
|
+
issue: { code: "SPA_NO_META_MANAGEMENT", message: "SPA without dynamic meta tag management", severity: "warning" },
|
|
26012
|
+
file: "src/components/SEOHead.tsx",
|
|
26013
|
+
before: null,
|
|
26014
|
+
after: `import { Helmet } from 'react-helmet-async';
|
|
26015
|
+
|
|
26016
|
+
interface SEOHeadProps {
|
|
26017
|
+
title?: string;
|
|
26018
|
+
description?: string;
|
|
26019
|
+
image?: string;
|
|
26020
|
+
url?: string;
|
|
26021
|
+
}
|
|
26022
|
+
|
|
26023
|
+
export function SEOHead({
|
|
26024
|
+
title = 'Your Site Name',
|
|
26025
|
+
description = 'Your site description',
|
|
26026
|
+
image = '/og-image.png',
|
|
26027
|
+
url = window.location.href,
|
|
26028
|
+
}: SEOHeadProps) {
|
|
26029
|
+
return (
|
|
26030
|
+
<Helmet>
|
|
26031
|
+
<title>{title}</title>
|
|
26032
|
+
<meta name="description" content={description} />
|
|
26033
|
+
<link rel="canonical" href={url} />
|
|
26034
|
+
|
|
26035
|
+
{/* Open Graph */}
|
|
26036
|
+
<meta property="og:title" content={title} />
|
|
26037
|
+
<meta property="og:description" content={description} />
|
|
26038
|
+
<meta property="og:image" content={image} />
|
|
26039
|
+
<meta property="og:url" content={url} />
|
|
26040
|
+
<meta property="og:type" content="website" />
|
|
26041
|
+
|
|
26042
|
+
{/* Twitter */}
|
|
26043
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
26044
|
+
<meta name="twitter:title" content={title} />
|
|
26045
|
+
<meta name="twitter:description" content={description} />
|
|
26046
|
+
<meta name="twitter:image" content={image} />
|
|
26047
|
+
</Helmet>
|
|
26048
|
+
);
|
|
26049
|
+
}`,
|
|
26050
|
+
explanation: "Created SEOHead component using react-helmet-async for dynamic meta tags. Install: npm install react-helmet-async"
|
|
26051
|
+
};
|
|
26052
|
+
}
|
|
26053
|
+
return {
|
|
26054
|
+
issue: { code: "SPA_NO_META_MANAGEMENT", message: "SPA without meta management", severity: "warning" },
|
|
26055
|
+
file: htmlPath,
|
|
26056
|
+
before: null,
|
|
26057
|
+
after: "<!-- Add a meta management library for your framework -->",
|
|
26058
|
+
explanation: `Add dynamic meta tag management for ${framework.name}`,
|
|
26059
|
+
skipped: true,
|
|
26060
|
+
skipReason: `Framework-specific solution needed for ${framework.name}`
|
|
26061
|
+
};
|
|
26062
|
+
}
|
|
26063
|
+
function generatePreconnectFix(context) {
|
|
26064
|
+
const { htmlPath, htmlContent } = context;
|
|
26065
|
+
const preconnects = `<!-- Preconnect to external origins -->
|
|
26066
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
26067
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />`;
|
|
26068
|
+
return {
|
|
26069
|
+
issue: { code: "MISSING_PRECONNECT", message: "Missing preconnect hints", severity: "info" },
|
|
26070
|
+
file: htmlPath,
|
|
26071
|
+
before: "<head>",
|
|
26072
|
+
after: `<head>
|
|
26073
|
+
${preconnects}`,
|
|
26074
|
+
explanation: "Added preconnect hints to speed up loading of external resources"
|
|
26075
|
+
};
|
|
26076
|
+
}
|
|
26077
|
+
function generateFaviconFix(context) {
|
|
26078
|
+
const { htmlPath } = context;
|
|
26079
|
+
const faviconTags = `<!-- Favicons -->
|
|
26080
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
26081
|
+
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
|
26082
|
+
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />`;
|
|
26083
|
+
return {
|
|
26084
|
+
issue: { code: "HTML_NO_FAVICON", message: "Missing favicon", severity: "info" },
|
|
26085
|
+
file: htmlPath,
|
|
26086
|
+
before: "<head>",
|
|
26087
|
+
after: `<head>
|
|
26088
|
+
${faviconTags}`,
|
|
26089
|
+
explanation: "Added favicon links. Create favicon files in public/ directory."
|
|
26090
|
+
};
|
|
26091
|
+
}
|
|
26092
|
+
function generateCharsetFix(context) {
|
|
26093
|
+
const { htmlPath, htmlContent } = context;
|
|
26094
|
+
if (htmlContent.includes("charset")) {
|
|
26095
|
+
return {
|
|
26096
|
+
issue: { code: "HTML_NO_CHARSET", message: "Missing charset", severity: "warning" },
|
|
26097
|
+
file: htmlPath,
|
|
26098
|
+
before: null,
|
|
26099
|
+
after: '<meta charset="UTF-8" />',
|
|
26100
|
+
skipped: true,
|
|
26101
|
+
skipReason: "Charset already defined",
|
|
26102
|
+
explanation: "Charset is already present"
|
|
26103
|
+
};
|
|
26104
|
+
}
|
|
26105
|
+
return {
|
|
26106
|
+
issue: { code: "HTML_NO_CHARSET", message: "Missing charset declaration", severity: "warning" },
|
|
26107
|
+
file: htmlPath,
|
|
26108
|
+
before: "<head>",
|
|
26109
|
+
after: '<head>\n <meta charset="UTF-8" />',
|
|
26110
|
+
explanation: "Added UTF-8 charset declaration as first element in head"
|
|
26111
|
+
};
|
|
26112
|
+
}
|
|
26113
|
+
function generateLangFix(context) {
|
|
26114
|
+
const { htmlPath, htmlContent } = context;
|
|
26115
|
+
if (htmlContent.includes("<html") && !htmlContent.includes("lang=")) {
|
|
26116
|
+
return {
|
|
26117
|
+
issue: { code: "HTML_NO_LANG", message: "Missing lang attribute", severity: "warning" },
|
|
26118
|
+
file: htmlPath,
|
|
26119
|
+
before: "<html>",
|
|
26120
|
+
after: '<html lang="en">',
|
|
26121
|
+
explanation: "Added lang attribute for accessibility and SEO"
|
|
26122
|
+
};
|
|
26123
|
+
}
|
|
26124
|
+
return {
|
|
26125
|
+
issue: { code: "HTML_NO_LANG", message: "Missing lang attribute", severity: "warning" },
|
|
26126
|
+
file: htmlPath,
|
|
26127
|
+
before: "<html",
|
|
26128
|
+
after: '<html lang="en"',
|
|
26129
|
+
explanation: "Added lang attribute for accessibility and SEO"
|
|
26130
|
+
};
|
|
26131
|
+
}
|
|
26132
|
+
function generateLlmsTxtFix(context, siteName, url) {
|
|
26133
|
+
const llmsTxt = `# ${siteName}
|
|
26134
|
+
> ${siteName} - A brief description of your product/service
|
|
26135
|
+
|
|
26136
|
+
## About
|
|
26137
|
+
${siteName} is... [Add your description here]
|
|
26138
|
+
|
|
26139
|
+
## Features
|
|
26140
|
+
- Feature 1
|
|
26141
|
+
- Feature 2
|
|
26142
|
+
- Feature 3
|
|
26143
|
+
|
|
26144
|
+
## Links
|
|
26145
|
+
- Homepage: ${url}
|
|
26146
|
+
- Documentation: ${url}/docs
|
|
26147
|
+
- API: ${url}/api
|
|
26148
|
+
|
|
26149
|
+
## Contact
|
|
26150
|
+
- Email: hello@${new URL(url).hostname}
|
|
26151
|
+
`;
|
|
26152
|
+
return {
|
|
26153
|
+
issue: { code: "AI_NO_LLMS_TXT", message: "No llms.txt file for AI crawlers", severity: "info" },
|
|
26154
|
+
file: "public/llms.txt",
|
|
26155
|
+
before: null,
|
|
26156
|
+
after: llmsTxt,
|
|
26157
|
+
explanation: "Created llms.txt to help AI systems understand your site. Customize the content."
|
|
26158
|
+
};
|
|
26159
|
+
}
|
|
25862
26160
|
async function applyFixes(fixes, options) {
|
|
25863
26161
|
const { cwd, dryRun = false } = options;
|
|
25864
26162
|
const applied = [];
|
|
@@ -27657,6 +27955,105 @@ function escapeRegex2(str) {
|
|
|
27657
27955
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
27658
27956
|
}
|
|
27659
27957
|
|
|
27958
|
+
// src/geo/llm-citation-checker.ts
|
|
27959
|
+
var PROVIDER_NAMES = {
|
|
27960
|
+
openai: "ChatGPT",
|
|
27961
|
+
anthropic: "Claude",
|
|
27962
|
+
google: "Gemini",
|
|
27963
|
+
perplexity: "Perplexity"
|
|
27964
|
+
};
|
|
27965
|
+
var DEFAULT_PROVIDERS = ["openai", "anthropic", "google", "perplexity"];
|
|
27966
|
+
function generateRecommendationQueries(brand, industry) {
|
|
27967
|
+
const queries = [
|
|
27968
|
+
`What are the best ${brand.toLowerCase()} alternatives?`,
|
|
27969
|
+
`Can you recommend tools similar to ${brand}?`,
|
|
27970
|
+
`What do you think about ${brand}?`
|
|
27971
|
+
];
|
|
27972
|
+
if (industry) {
|
|
27973
|
+
queries.push(`What are the best ${industry} tools?`);
|
|
27974
|
+
queries.push(`Recommend a good ${industry} solution`);
|
|
27975
|
+
}
|
|
27976
|
+
return queries;
|
|
27977
|
+
}
|
|
27978
|
+
async function checkLLMCitations(brand, domain, options = {}) {
|
|
27979
|
+
const {
|
|
27980
|
+
providers = DEFAULT_PROVIDERS,
|
|
27981
|
+
queries = generateRecommendationQueries(brand),
|
|
27982
|
+
...trackingOptions
|
|
27983
|
+
} = options;
|
|
27984
|
+
const brandConfig = {
|
|
27985
|
+
brandName: brand,
|
|
27986
|
+
domains: [domain],
|
|
27987
|
+
alternativeNames: [brand.toLowerCase(), brand.toUpperCase()]
|
|
27988
|
+
};
|
|
27989
|
+
const query = queries[0];
|
|
27990
|
+
const results = await trackLLMVisibility(
|
|
27991
|
+
{
|
|
27992
|
+
keyword: query,
|
|
27993
|
+
brand: brandConfig,
|
|
27994
|
+
providers
|
|
27995
|
+
},
|
|
27996
|
+
trackingOptions
|
|
27997
|
+
);
|
|
27998
|
+
return results.map((result) => ({
|
|
27999
|
+
provider: result.provider,
|
|
28000
|
+
providerName: PROVIDER_NAMES[result.provider],
|
|
28001
|
+
mentioned: result.mentioned,
|
|
28002
|
+
position: result.position,
|
|
28003
|
+
sentiment: result.sentiment,
|
|
28004
|
+
context: result.contextSnippet || null,
|
|
28005
|
+
score: result.score,
|
|
28006
|
+
error: result.error
|
|
28007
|
+
}));
|
|
28008
|
+
}
|
|
28009
|
+
function calculateAIVisibilityScore(results) {
|
|
28010
|
+
if (results.length === 0) return 0;
|
|
28011
|
+
const mentionedCount = results.filter((r) => r.mentioned && !r.error).length;
|
|
28012
|
+
const validResults = results.filter((r) => !r.error);
|
|
28013
|
+
if (validResults.length === 0) return 0;
|
|
28014
|
+
let score = mentionedCount / validResults.length * 100;
|
|
28015
|
+
const topPositions = results.filter(
|
|
28016
|
+
(r) => r.mentioned && r.position !== null && r.position <= 3
|
|
28017
|
+
);
|
|
28018
|
+
if (topPositions.length > 0) {
|
|
28019
|
+
score = Math.min(100, score + 10);
|
|
28020
|
+
}
|
|
28021
|
+
const positiveResults = results.filter((r) => r.sentiment === "positive");
|
|
28022
|
+
if (positiveResults.length > mentionedCount / 2) {
|
|
28023
|
+
score = Math.min(100, score + 5);
|
|
28024
|
+
}
|
|
28025
|
+
return Math.round(score);
|
|
28026
|
+
}
|
|
28027
|
+
function getAIVisibilitySummary(results) {
|
|
28028
|
+
const score = calculateAIVisibilityScore(results);
|
|
28029
|
+
const mentionedIn = results.filter((r) => r.mentioned).map((r) => r.providerName);
|
|
28030
|
+
const notMentionedIn = results.filter((r) => !r.mentioned && !r.error).map((r) => r.providerName);
|
|
28031
|
+
const positions = results.filter((r) => r.position !== null).map((r) => r.position);
|
|
28032
|
+
const bestPosition = positions.length > 0 ? Math.min(...positions) : null;
|
|
28033
|
+
const sentiments = results.filter((r) => r.sentiment !== null).map((r) => r.sentiment);
|
|
28034
|
+
let overallSentiment = null;
|
|
28035
|
+
if (sentiments.length > 0) {
|
|
28036
|
+
const positiveCount = sentiments.filter((s) => s === "positive").length;
|
|
28037
|
+
const negativeCount = sentiments.filter((s) => s === "negative").length;
|
|
28038
|
+
if (positiveCount > 0 && negativeCount > 0) {
|
|
28039
|
+
overallSentiment = "mixed";
|
|
28040
|
+
} else if (positiveCount > negativeCount) {
|
|
28041
|
+
overallSentiment = "positive";
|
|
28042
|
+
} else if (negativeCount > positiveCount) {
|
|
28043
|
+
overallSentiment = "negative";
|
|
28044
|
+
} else {
|
|
28045
|
+
overallSentiment = "neutral";
|
|
28046
|
+
}
|
|
28047
|
+
}
|
|
28048
|
+
return {
|
|
28049
|
+
score,
|
|
28050
|
+
mentionedIn,
|
|
28051
|
+
notMentionedIn,
|
|
28052
|
+
bestPosition,
|
|
28053
|
+
overallSentiment
|
|
28054
|
+
};
|
|
28055
|
+
}
|
|
28056
|
+
|
|
27660
28057
|
// src/frameworks/index.ts
|
|
27661
28058
|
var frameworks_exports = {};
|
|
27662
28059
|
__export(frameworks_exports, {
|
|
@@ -28411,6 +28808,7 @@ export {
|
|
|
28411
28808
|
applyFixes,
|
|
28412
28809
|
buildGSCApiRequest,
|
|
28413
28810
|
buildGSCRequest,
|
|
28811
|
+
calculateAIVisibilityScore,
|
|
28414
28812
|
calculateBM252 as calculateBM25,
|
|
28415
28813
|
calculateTFIDF as calculateKeywordTFIDF,
|
|
28416
28814
|
calculateNextRun,
|
|
@@ -28425,6 +28823,7 @@ export {
|
|
|
28425
28823
|
checkGitHubCLI,
|
|
28426
28824
|
checkInternalRedirects,
|
|
28427
28825
|
checkJSRenderingRatio,
|
|
28826
|
+
checkLLMCitations,
|
|
28428
28827
|
checkLlmsTxt,
|
|
28429
28828
|
checkMobileResources,
|
|
28430
28829
|
checkPlaintextEmails,
|
|
@@ -28528,6 +28927,7 @@ export {
|
|
|
28528
28927
|
generatePDFReport,
|
|
28529
28928
|
generatePRDescription,
|
|
28530
28929
|
generateReactHelmetSocialMeta,
|
|
28930
|
+
generateRecommendationQueries,
|
|
28531
28931
|
generateRemixMeta,
|
|
28532
28932
|
generateSecretsDoc,
|
|
28533
28933
|
generateSocialMetaFix,
|
|
@@ -28535,6 +28935,7 @@ export {
|
|
|
28535
28935
|
generateUncertaintyQuestions,
|
|
28536
28936
|
generateWizardQuestions,
|
|
28537
28937
|
generateWorkflow,
|
|
28938
|
+
getAIVisibilitySummary,
|
|
28538
28939
|
getAutocompleteSuggestions,
|
|
28539
28940
|
getDateRange,
|
|
28540
28941
|
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);
|