@rankcli/agent-runtime 0.0.13 → 0.0.15
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 +215 -1
- package/dist/index.d.ts +215 -1
- package/dist/index.js +847 -90
- package/dist/index.mjs +813 -60
- package/package.json +2 -1
- package/src/audit/checks/agent-experience.ts +108 -0
- package/src/audit/checks/ai-readiness.ts +67 -0
- package/src/audit/checks/client-rendering.ts +10 -10
- package/src/audit/checks/rag-chunk-readiness.test.ts +159 -0
- package/src/audit/checks/rag-chunk-readiness.ts +163 -0
- package/src/audit/checks/security-headers.ts +18 -2
- package/src/audit/engine.ts +23 -10
- package/src/audit/types.ts +24 -0
- package/src/index.ts +3 -0
- package/src/ranking/index.ts +5 -0
- package/src/ranking/serp-client.ts +348 -0
- package/src/ranking/tracker.ts +380 -0
- package/src/ranking/types.ts +123 -0
package/dist/index.mjs
CHANGED
|
@@ -919,6 +919,24 @@ var ISSUE_DEFINITIONS = {
|
|
|
919
919
|
impact: "Your content will not be used for Bard/Gemini AI training (regular search unaffected).",
|
|
920
920
|
howToFix: 'Remove "User-agent: Google-Extended Disallow: /" if you want Google AI visibility.'
|
|
921
921
|
},
|
|
922
|
+
CLOUDFLARE_AI_CRAWLER_GATE_AMBIGUOUS: {
|
|
923
|
+
code: "CLOUDFLARE_AI_CRAWLER_GATE_AMBIGUOUS",
|
|
924
|
+
severity: "warning",
|
|
925
|
+
category: "ai-readiness",
|
|
926
|
+
title: "No explicit AI crawler rules on a Cloudflare-fronted site",
|
|
927
|
+
description: 'This site appears to be served through Cloudflare but robots.txt has no explicit Allow/Disallow rules for AI crawlers. Cloudflare blocks "mixed-use" AI crawlers by default on ad-hosting zones starting September 15, 2026, and is rolling out Pay Per Crawl / Pay Per Use gating beyond that.',
|
|
928
|
+
impact: "Without an explicit rule, whether AI crawlers (and future citation opportunities in ChatGPT, Claude, Perplexity, etc.) can reach this site now depends on Cloudflare account-level bot-management defaults, not on this codebase \u2014 a silent, invisible-to-git failure mode.",
|
|
929
|
+
howToFix: "Add explicit User-agent rules for GPTBot, Claude-Web/ClaudeBot, PerplexityBot, and Google-Extended in robots.txt, and confirm the matching allow/block posture in the Cloudflare dashboard under Bot Management \u2192 AI Crawl Control. If you'd rather charge than block, Cloudflare's Pay Per Crawl (AI Crawl Control \u2192 Payments tab) lets you set a per-crawl price instead of a flat allow/deny \u2014 worth a look if you get meaningful AI-crawler traffic."
|
|
930
|
+
},
|
|
931
|
+
NO_AGENT_EXPERIENCE_SURFACE: {
|
|
932
|
+
code: "NO_AGENT_EXPERIENCE_SURFACE",
|
|
933
|
+
severity: "notice",
|
|
934
|
+
category: "ai-readiness",
|
|
935
|
+
title: "No agent-facing discovery surface found",
|
|
936
|
+
description: "None of the emerging AI-agent discovery conventions were found: llms-full.txt, SKILL.md, a discoverable MCP server, or an OpenAPI spec. This is a genuinely new, low-adoption category as of 2026 \u2014 most sites have none of these yet \u2014 so this is an opportunity, not a compliance failure.",
|
|
937
|
+
impact: "AI agents (not just chatbots answering questions, but agents that browse and act on a user's behalf) increasingly prefer sites that expose machine-readable capabilities over ones that require scraping and guessing at HTML structure. Being an early, discoverable site in this space is a low-competition differentiator right now.",
|
|
938
|
+
howToFix: "Start with whichever fits your site: llms-full.txt (a single Markdown dump of your key content \u2014 measured as fetched roughly 2x more than plain llms.txt), a SKILL.md capability manifest, or an OpenAPI spec at /openapi.json if you already have an API. A discoverable MCP server is the highest-effort, highest-payoff option if your product has one to expose."
|
|
939
|
+
},
|
|
922
940
|
HIGH_JS_RENDERING_RATIO: {
|
|
923
941
|
code: "HIGH_JS_RENDERING_RATIO",
|
|
924
942
|
severity: "warning",
|
|
@@ -3854,6 +3872,61 @@ async function checkInternalRedirects(internalLinks, batchSize = 10) {
|
|
|
3854
3872
|
|
|
3855
3873
|
// src/audit/checks/ai-readiness.ts
|
|
3856
3874
|
import * as cheerio13 from "cheerio";
|
|
3875
|
+
|
|
3876
|
+
// src/audit/checks/agent-experience.ts
|
|
3877
|
+
var SIGNAL_PATHS = [
|
|
3878
|
+
{ path: "/llms-full.txt", description: "Full-site content dump for AI agents \u2014 the fuller sibling of llms.txt, measured as fetched roughly 2x more often" },
|
|
3879
|
+
{ path: "/skill.md", description: "SKILL.md capability manifest (Anthropic Agent Skills convention)" },
|
|
3880
|
+
{ path: "/mcp", description: "MCP (Model Context Protocol) server, discoverable at a conventional path" },
|
|
3881
|
+
{ path: "/.well-known/mcp.json", description: "MCP server descriptor at the .well-known convention" },
|
|
3882
|
+
{ path: "/openapi.json", description: "OpenAPI spec \u2014 lets an agent discover and call your API directly instead of scraping HTML" },
|
|
3883
|
+
{ path: "/.well-known/ai-plugin.json", description: "AI plugin manifest (older but still-referenced convention for agent tool discovery)" }
|
|
3884
|
+
];
|
|
3885
|
+
async function fetchBody(baseUrl, path3) {
|
|
3886
|
+
try {
|
|
3887
|
+
const url = new URL(path3, baseUrl).href;
|
|
3888
|
+
const response = await httpGet(url, {
|
|
3889
|
+
timeout: 8e3,
|
|
3890
|
+
validateStatus: () => true
|
|
3891
|
+
});
|
|
3892
|
+
return { status: response.status, body: String(response.data ?? "") };
|
|
3893
|
+
} catch {
|
|
3894
|
+
return null;
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
async function pathExists(baseUrl, path3, fallbackBody) {
|
|
3898
|
+
const result = await fetchBody(baseUrl, path3);
|
|
3899
|
+
if (!result) return false;
|
|
3900
|
+
if (result.status === 404) return false;
|
|
3901
|
+
if (result.status < 200 || result.status >= 400) return false;
|
|
3902
|
+
if (fallbackBody != null && result.body === fallbackBody) return false;
|
|
3903
|
+
return true;
|
|
3904
|
+
}
|
|
3905
|
+
async function checkAgentExperience(baseUrl) {
|
|
3906
|
+
const issues = [];
|
|
3907
|
+
const probePath = `/__rankcli_ax_probe_${Math.random().toString(36).slice(2)}`;
|
|
3908
|
+
const baseline = await fetchBody(baseUrl, probePath);
|
|
3909
|
+
const fallbackBody = baseline && baseline.status >= 200 && baseline.status < 400 ? baseline.body : null;
|
|
3910
|
+
const signals = await Promise.all(
|
|
3911
|
+
SIGNAL_PATHS.map(async ({ path: path3, description }) => ({
|
|
3912
|
+
path: path3,
|
|
3913
|
+
description,
|
|
3914
|
+
present: await pathExists(baseUrl, path3, fallbackBody)
|
|
3915
|
+
}))
|
|
3916
|
+
);
|
|
3917
|
+
const presentCount = signals.filter((s) => s.present).length;
|
|
3918
|
+
const score = Math.round(presentCount / signals.length * 100);
|
|
3919
|
+
if (presentCount === 0) {
|
|
3920
|
+
issues.push({
|
|
3921
|
+
...ISSUE_DEFINITIONS.NO_AGENT_EXPERIENCE_SURFACE,
|
|
3922
|
+
affectedUrls: [baseUrl],
|
|
3923
|
+
details: { checkedPaths: SIGNAL_PATHS.map((s) => s.path) }
|
|
3924
|
+
});
|
|
3925
|
+
}
|
|
3926
|
+
return { issues, data: { signals, score } };
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3929
|
+
// src/audit/checks/ai-readiness.ts
|
|
3857
3930
|
var AI_BOTS = {
|
|
3858
3931
|
GPTBot: "GPTBot",
|
|
3859
3932
|
"ChatGPT-User": "ChatGPT-User",
|
|
@@ -4074,6 +4147,32 @@ function checkJSRenderingRatio(html, url) {
|
|
|
4074
4147
|
data: { ratio, staticWordCount }
|
|
4075
4148
|
};
|
|
4076
4149
|
}
|
|
4150
|
+
async function checkCloudflareAICrawlerGate(baseUrl, botBlocking) {
|
|
4151
|
+
const issues = [];
|
|
4152
|
+
let behindCloudflare = false;
|
|
4153
|
+
try {
|
|
4154
|
+
const response = await httpGet(baseUrl, {
|
|
4155
|
+
timeout: 1e4,
|
|
4156
|
+
validateStatus: () => true
|
|
4157
|
+
});
|
|
4158
|
+
const server = response.headers["server"] || "";
|
|
4159
|
+
behindCloudflare = server.toLowerCase().includes("cloudflare") || "cf-ray" in response.headers;
|
|
4160
|
+
} catch {
|
|
4161
|
+
behindCloudflare = false;
|
|
4162
|
+
}
|
|
4163
|
+
const hasExplicitAIRules = botBlocking.robotsExists && botBlocking.blockedBots.length > 0;
|
|
4164
|
+
const ambiguous = behindCloudflare && !hasExplicitAIRules;
|
|
4165
|
+
if (ambiguous) {
|
|
4166
|
+
issues.push({
|
|
4167
|
+
...ISSUE_DEFINITIONS.CLOUDFLARE_AI_CRAWLER_GATE_AMBIGUOUS,
|
|
4168
|
+
affectedUrls: [baseUrl]
|
|
4169
|
+
});
|
|
4170
|
+
}
|
|
4171
|
+
return {
|
|
4172
|
+
issues,
|
|
4173
|
+
data: { behindCloudflare, hasExplicitAIRules, ambiguous }
|
|
4174
|
+
};
|
|
4175
|
+
}
|
|
4077
4176
|
async function runAIReadinessChecks(baseUrl, html) {
|
|
4078
4177
|
const allIssues = [];
|
|
4079
4178
|
const llmsResult = await checkLlmsTxt(baseUrl);
|
|
@@ -4082,12 +4181,18 @@ async function runAIReadinessChecks(baseUrl, html) {
|
|
|
4082
4181
|
allIssues.push(...botResult.issues);
|
|
4083
4182
|
const jsResult = checkJSRenderingRatio(html, baseUrl);
|
|
4084
4183
|
allIssues.push(...jsResult.issues);
|
|
4184
|
+
const cfGateResult = await checkCloudflareAICrawlerGate(baseUrl, botResult.data);
|
|
4185
|
+
allIssues.push(...cfGateResult.issues);
|
|
4186
|
+
const agentExperienceResult = await checkAgentExperience(baseUrl);
|
|
4187
|
+
allIssues.push(...agentExperienceResult.issues);
|
|
4085
4188
|
return {
|
|
4086
4189
|
issues: allIssues,
|
|
4087
4190
|
data: {
|
|
4088
4191
|
llmsTxt: llmsResult.data,
|
|
4089
4192
|
botBlocking: botResult.data,
|
|
4090
|
-
jsRenderingRatio: jsResult.data.ratio
|
|
4193
|
+
jsRenderingRatio: jsResult.data.ratio,
|
|
4194
|
+
cloudflareAIGate: cfGateResult.data,
|
|
4195
|
+
agentExperience: agentExperienceResult.data
|
|
4091
4196
|
}
|
|
4092
4197
|
};
|
|
4093
4198
|
}
|
|
@@ -4552,7 +4657,13 @@ async function analyzeSecurityHeaders2(url) {
|
|
|
4552
4657
|
maxRedirects: 5
|
|
4553
4658
|
});
|
|
4554
4659
|
const headers = response.headers;
|
|
4555
|
-
|
|
4660
|
+
let isHttps = false;
|
|
4661
|
+
try {
|
|
4662
|
+
const parsedUrl = new URL(url);
|
|
4663
|
+
isHttps = parsedUrl.protocol === "https:";
|
|
4664
|
+
} catch {
|
|
4665
|
+
isHttps = url.toLowerCase().startsWith("https://");
|
|
4666
|
+
}
|
|
4556
4667
|
const getHeader = (name) => {
|
|
4557
4668
|
const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase());
|
|
4558
4669
|
return key ? headers[key] : null;
|
|
@@ -4624,10 +4735,17 @@ async function analyzeSecurityHeaders2(url) {
|
|
|
4624
4735
|
}
|
|
4625
4736
|
};
|
|
4626
4737
|
} catch (error) {
|
|
4738
|
+
let isHttps = false;
|
|
4739
|
+
try {
|
|
4740
|
+
const parsedUrl = new URL(url);
|
|
4741
|
+
isHttps = parsedUrl.protocol === "https:";
|
|
4742
|
+
} catch {
|
|
4743
|
+
isHttps = url.toLowerCase().startsWith("https://");
|
|
4744
|
+
}
|
|
4627
4745
|
return {
|
|
4628
4746
|
issues,
|
|
4629
4747
|
data: {
|
|
4630
|
-
https:
|
|
4748
|
+
https: isHttps,
|
|
4631
4749
|
headers: {
|
|
4632
4750
|
hsts: null,
|
|
4633
4751
|
csp: null,
|
|
@@ -8661,17 +8779,17 @@ function generateRecommendations2(renderingMethod, hasContentInHTML, charCount,
|
|
|
8661
8779
|
);
|
|
8662
8780
|
if (framework === "React") {
|
|
8663
8781
|
recommendations.push(
|
|
8664
|
-
|
|
8782
|
+
"Quick fix: Use Vike (vike.dev) for SSR/SSG - works with Vite, minimal config needed"
|
|
8665
8783
|
);
|
|
8666
8784
|
recommendations.push(
|
|
8667
|
-
"Alternative:
|
|
8785
|
+
"Alternative: Migrate to Next.js or Remix for built-in SSR/SSG support"
|
|
8668
8786
|
);
|
|
8669
8787
|
} else if (framework === "Vue") {
|
|
8670
8788
|
recommendations.push(
|
|
8671
|
-
"Quick fix:
|
|
8789
|
+
"Quick fix: Use Vike (vike.dev) for SSR/SSG - works with Vite, minimal config needed"
|
|
8672
8790
|
);
|
|
8673
8791
|
recommendations.push(
|
|
8674
|
-
"Alternative:
|
|
8792
|
+
"Alternative: Migrate to Nuxt for built-in SSR/SSG support"
|
|
8675
8793
|
);
|
|
8676
8794
|
} else if (framework === "Angular Universal") {
|
|
8677
8795
|
recommendations.push(
|
|
@@ -8679,7 +8797,7 @@ function generateRecommendations2(renderingMethod, hasContentInHTML, charCount,
|
|
|
8679
8797
|
);
|
|
8680
8798
|
} else {
|
|
8681
8799
|
recommendations.push(
|
|
8682
|
-
"Quick fix: Use
|
|
8800
|
+
"Quick fix: Use Vike (vike.dev) for SSR/SSG with any Vite-based framework"
|
|
8683
8801
|
);
|
|
8684
8802
|
}
|
|
8685
8803
|
recommendations.push(
|
|
@@ -8703,7 +8821,7 @@ function analyzeClientRendering(html, url) {
|
|
|
8703
8821
|
const issues = [];
|
|
8704
8822
|
const analysis = analyzeRendering(html);
|
|
8705
8823
|
if (analysis.renderingMethod === "csr" && analysis.confidence !== "low") {
|
|
8706
|
-
const howToFixByFramework = analysis.frameworkDetected === "React" ?
|
|
8824
|
+
const howToFixByFramework = analysis.frameworkDetected === "React" ? "Use Vike (vike.dev) to add SSR/SSG to your Vite React app: npm install vike vike-react, then follow the setup guide. Alternatively, migrate to Next.js or Remix." : analysis.frameworkDetected === "Vue" ? "Use Vike (vike.dev) to add SSR/SSG to your Vite Vue app: npm install vike vike-vue, then follow the setup guide. Alternatively, migrate to Nuxt." : "Use Vike (vike.dev) for SSR/SSG with Vite-based frameworks, or use a framework with built-in SSR support (Next.js, Nuxt, Remix, SvelteKit).";
|
|
8707
8825
|
issues.push({
|
|
8708
8826
|
code: "CLIENT_SIDE_RENDERING",
|
|
8709
8827
|
severity: "error",
|
|
@@ -8744,7 +8862,7 @@ function analyzeClientRendering(html, url) {
|
|
|
8744
8862
|
title: `${analysis.frameworkDetected} detected without SSR markers`,
|
|
8745
8863
|
description: `This appears to be a ${analysis.frameworkDetected} SPA without server-side rendering enabled.`,
|
|
8746
8864
|
impact: "Single Page Applications without SSR have slower time-to-content for search crawlers.",
|
|
8747
|
-
howToFix: analysis.frameworkDetected === "React" ? "
|
|
8865
|
+
howToFix: analysis.frameworkDetected === "React" ? "Add SSR/SSG using Vike (vike.dev): npm install vike vike-react. Alternative: migrate to Next.js or Remix." : "Add SSR/SSG using Vike (vike.dev): npm install vike vike-vue. Alternative: migrate to Nuxt.",
|
|
8748
8866
|
affectedUrls: [url],
|
|
8749
8867
|
details: {
|
|
8750
8868
|
framework: analysis.frameworkDetected,
|
|
@@ -12521,8 +12639,94 @@ function analyzeAIContentStructure(html, url) {
|
|
|
12521
12639
|
};
|
|
12522
12640
|
}
|
|
12523
12641
|
|
|
12524
|
-
// src/audit/checks/
|
|
12642
|
+
// src/audit/checks/rag-chunk-readiness.ts
|
|
12525
12643
|
import * as cheerio44 from "cheerio";
|
|
12644
|
+
var MIN_GOOD_WORDS = 150;
|
|
12645
|
+
var MAX_GOOD_WORDS = 450;
|
|
12646
|
+
var TOKENS_PER_WORD = 1.33;
|
|
12647
|
+
var DANGLING_OPENERS = /^(this|it|these|that|they|such|those|the former|the latter)\b/i;
|
|
12648
|
+
function classifySize(wordCount) {
|
|
12649
|
+
if (wordCount < MIN_GOOD_WORDS) return "too-short";
|
|
12650
|
+
if (wordCount > MAX_GOOD_WORDS) return "too-long";
|
|
12651
|
+
return "good";
|
|
12652
|
+
}
|
|
12653
|
+
function extractSections($) {
|
|
12654
|
+
const elements = $("h1, h2, h3, p, li, blockquote").toArray();
|
|
12655
|
+
const sections = [];
|
|
12656
|
+
let current = null;
|
|
12657
|
+
for (const el of elements) {
|
|
12658
|
+
const tag = el.tagName?.toLowerCase();
|
|
12659
|
+
if (tag === "h1" || tag === "h2" || tag === "h3") {
|
|
12660
|
+
if (current) sections.push(current);
|
|
12661
|
+
current = { heading: $(el).text().trim(), headingLevel: parseInt(tag.slice(1), 10), text: "" };
|
|
12662
|
+
} else if (current) {
|
|
12663
|
+
current.text += " " + $(el).text();
|
|
12664
|
+
}
|
|
12665
|
+
}
|
|
12666
|
+
if (current) sections.push(current);
|
|
12667
|
+
return sections;
|
|
12668
|
+
}
|
|
12669
|
+
function analyzeRAGChunkReadiness(html, url) {
|
|
12670
|
+
const issues = [];
|
|
12671
|
+
const $ = cheerio44.load(html);
|
|
12672
|
+
$("nav, footer, aside, script, style, noscript, header").remove();
|
|
12673
|
+
const rawSections = extractSections($);
|
|
12674
|
+
const sections = rawSections.map((s) => {
|
|
12675
|
+
const words = s.text.trim().split(/\s+/).filter(Boolean);
|
|
12676
|
+
const wordCount = words.length;
|
|
12677
|
+
const estimatedTokens = Math.round(wordCount * TOKENS_PER_WORD);
|
|
12678
|
+
const firstSentence = s.text.trim().split(/[.!?]/)[0] || "";
|
|
12679
|
+
return {
|
|
12680
|
+
heading: s.heading,
|
|
12681
|
+
headingLevel: s.headingLevel,
|
|
12682
|
+
wordCount,
|
|
12683
|
+
estimatedTokens,
|
|
12684
|
+
sizeQuality: classifySize(wordCount),
|
|
12685
|
+
startsWithDanglingReference: DANGLING_OPENERS.test(firstSentence.trim())
|
|
12686
|
+
};
|
|
12687
|
+
});
|
|
12688
|
+
const totalSections = sections.length;
|
|
12689
|
+
const wellSizedSectionCount = sections.filter((s) => s.sizeQuality === "good").length;
|
|
12690
|
+
const danglingReferenceCount = sections.filter((s) => s.startsWithDanglingReference).length;
|
|
12691
|
+
const chunkReadinessScore = totalSections === 0 ? 0 : Math.round(
|
|
12692
|
+
(wellSizedSectionCount / totalSections * 0.7 + (totalSections - danglingReferenceCount) / totalSections * 0.3) * 100
|
|
12693
|
+
);
|
|
12694
|
+
if (totalSections >= 3 && wellSizedSectionCount / totalSections < 0.5) {
|
|
12695
|
+
const tooLong = sections.filter((s) => s.sizeQuality === "too-long").length;
|
|
12696
|
+
const tooShort = sections.filter((s) => s.sizeQuality === "too-short").length;
|
|
12697
|
+
issues.push({
|
|
12698
|
+
code: "RAG_CHUNK_SIZE_MISMATCH",
|
|
12699
|
+
severity: "notice",
|
|
12700
|
+
category: "ai-readiness",
|
|
12701
|
+
title: "Most sections are poorly sized for AI retrieval chunking",
|
|
12702
|
+
description: `${wellSizedSectionCount} of ${totalSections} sections fall in a well-sized range for RAG retrieval (roughly 150-450 words); ${tooLong} are too long and likely to get split mid-thought, ${tooShort} are too short and likely to get merged with unrelated neighbors.`,
|
|
12703
|
+
impact: "When an AI answer engine retrieves a chunk of this page in response to a query, oversized sections risk being cut off mid-point and undersized ones risk losing standalone context \u2014 both reduce the odds the retrieved chunk reads coherently in an AI-generated answer.",
|
|
12704
|
+
howToFix: "Break up long sections (450+ words) with an additional H2/H3 subheading roughly every 300-400 words. Merge very short sections (under 150 words) into a neighboring section or expand them with enough context to stand alone.",
|
|
12705
|
+
affectedUrls: [url],
|
|
12706
|
+
details: { wellSizedSectionCount, totalSections, tooLong, tooShort }
|
|
12707
|
+
});
|
|
12708
|
+
}
|
|
12709
|
+
if (totalSections >= 3 && danglingReferenceCount / totalSections > 0.3) {
|
|
12710
|
+
issues.push({
|
|
12711
|
+
code: "RAG_CHUNK_DANGLING_REFERENCES",
|
|
12712
|
+
severity: "notice",
|
|
12713
|
+
category: "ai-readiness",
|
|
12714
|
+
title: "Several sections open with a reference to prior context",
|
|
12715
|
+
description: `${danglingReferenceCount} of ${totalSections} sections start with a pronoun or demonstrative ("This...", "It...", "These...") that depends on the previous section to make sense.`,
|
|
12716
|
+
impact: "A RAG system that retrieves one of these sections on its own \u2014 which is exactly how retrieval works, one chunk at a time \u2014 surfaces a sentence whose subject is undefined, reading as broken or confusing in an AI-generated answer.",
|
|
12717
|
+
howToFix: 'Open each section by naming its actual subject instead of referring back to the previous one \u2014 e.g. "This approach reduces cost" becomes "Caching reduces cost."',
|
|
12718
|
+
affectedUrls: [url],
|
|
12719
|
+
details: { danglingReferenceCount, totalSections }
|
|
12720
|
+
});
|
|
12721
|
+
}
|
|
12722
|
+
return {
|
|
12723
|
+
issues,
|
|
12724
|
+
data: { sections, totalSections, wellSizedSectionCount, danglingReferenceCount, chunkReadinessScore }
|
|
12725
|
+
};
|
|
12726
|
+
}
|
|
12727
|
+
|
|
12728
|
+
// src/audit/checks/citation-quality.ts
|
|
12729
|
+
import * as cheerio45 from "cheerio";
|
|
12526
12730
|
var REPUTABLE_SOURCES = {
|
|
12527
12731
|
academic: [
|
|
12528
12732
|
"scholar.google.com",
|
|
@@ -12589,7 +12793,7 @@ var REPUTABLE_SOURCES = {
|
|
|
12589
12793
|
};
|
|
12590
12794
|
function analyzeCitationQuality(html, url) {
|
|
12591
12795
|
const issues = [];
|
|
12592
|
-
const $ =
|
|
12796
|
+
const $ = cheerio45.load(html);
|
|
12593
12797
|
const parsedUrl = new URL(url);
|
|
12594
12798
|
const currentDomain = parsedUrl.hostname;
|
|
12595
12799
|
const $content = $("body").clone();
|
|
@@ -12818,12 +13022,12 @@ function analyzeCitationQuality(html, url) {
|
|
|
12818
13022
|
}
|
|
12819
13023
|
|
|
12820
13024
|
// src/audit/checks/answer-conciseness.ts
|
|
12821
|
-
import * as
|
|
13025
|
+
import * as cheerio46 from "cheerio";
|
|
12822
13026
|
var IDEAL_ANSWER_LENGTH = { min: 40, max: 150 };
|
|
12823
13027
|
var MAX_FIRST_SENTENCE = 200;
|
|
12824
13028
|
function analyzeAnswerConciseness(html, url) {
|
|
12825
13029
|
const issues = [];
|
|
12826
|
-
const $ =
|
|
13030
|
+
const $ = cheerio46.load(html);
|
|
12827
13031
|
$("nav, footer, aside, script, style, noscript, header").remove();
|
|
12828
13032
|
const headings = $("h1, h2, h3, h4, h5, h6");
|
|
12829
13033
|
let totalHeadings = 0;
|
|
@@ -13014,7 +13218,7 @@ function analyzeAnswerConciseness(html, url) {
|
|
|
13014
13218
|
}
|
|
13015
13219
|
|
|
13016
13220
|
// src/audit/checks/brand-mention-optimization.ts
|
|
13017
|
-
import * as
|
|
13221
|
+
import * as cheerio47 from "cheerio";
|
|
13018
13222
|
var BRAND_DEFINITION_PATTERNS = [
|
|
13019
13223
|
/(?:we are|we're|is a|is the|is an)\s+(?:leading|premier|top|best|trusted|innovative|professional)/i,
|
|
13020
13224
|
/(?:our mission|our vision|we help|we provide|we offer|we specialize)/i,
|
|
@@ -13052,7 +13256,7 @@ var REVIEW_PLATFORMS = [
|
|
|
13052
13256
|
];
|
|
13053
13257
|
function analyzeBrandMentionOptimization(html, url) {
|
|
13054
13258
|
const issues = [];
|
|
13055
|
-
const $ =
|
|
13259
|
+
const $ = cheerio47.load(html);
|
|
13056
13260
|
const parsedUrl = new URL(url);
|
|
13057
13261
|
const $content = $("body").clone();
|
|
13058
13262
|
$content.find("nav, footer, script, style, noscript").remove();
|
|
@@ -13244,7 +13448,7 @@ function analyzeBrandMentionOptimization(html, url) {
|
|
|
13244
13448
|
}
|
|
13245
13449
|
|
|
13246
13450
|
// src/audit/checks/ai-citation-worthiness.ts
|
|
13247
|
-
import * as
|
|
13451
|
+
import * as cheerio48 from "cheerio";
|
|
13248
13452
|
var ORIGINAL_DATA_PATTERNS = [
|
|
13249
13453
|
/(?:our data|our research|our study|our analysis|we found|we discovered)/i,
|
|
13250
13454
|
/(?:survey of|surveyed|interviewed|analyzed)\s+\d+/i,
|
|
@@ -13291,7 +13495,7 @@ var STOCK_IMAGE_DOMAINS = [
|
|
|
13291
13495
|
];
|
|
13292
13496
|
function analyzeAICitationWorthiness(html, url) {
|
|
13293
13497
|
const issues = [];
|
|
13294
|
-
const $ =
|
|
13498
|
+
const $ = cheerio48.load(html);
|
|
13295
13499
|
const $content = $("body").clone();
|
|
13296
13500
|
$content.find("nav, footer, script, style, noscript, aside").remove();
|
|
13297
13501
|
const bodyText = $content.text();
|
|
@@ -13516,7 +13720,7 @@ function analyzeAICitationWorthiness(html, url) {
|
|
|
13516
13720
|
}
|
|
13517
13721
|
|
|
13518
13722
|
// src/audit/checks/review-ecosystem.ts
|
|
13519
|
-
import * as
|
|
13723
|
+
import * as cheerio49 from "cheerio";
|
|
13520
13724
|
var REVIEW_PLATFORMS2 = {
|
|
13521
13725
|
general: [
|
|
13522
13726
|
{ name: "Google Business", domain: "google.com/maps", aliases: ["goo.gl/maps", "maps.google"] },
|
|
@@ -13576,7 +13780,7 @@ var TRUST_BADGE_PATTERNS2 = [
|
|
|
13576
13780
|
];
|
|
13577
13781
|
function analyzeReviewEcosystem(html, url) {
|
|
13578
13782
|
const issues = [];
|
|
13579
|
-
const $ =
|
|
13783
|
+
const $ = cheerio49.load(html);
|
|
13580
13784
|
const allLinks = $("a[href]").map((_, a) => $(a).attr("href") || "").get();
|
|
13581
13785
|
const allLinksLower = allLinks.map((l) => l.toLowerCase());
|
|
13582
13786
|
const linkedPlatforms = [];
|
|
@@ -14258,10 +14462,10 @@ function analyzeStructure(content) {
|
|
|
14258
14462
|
}
|
|
14259
14463
|
|
|
14260
14464
|
// src/audit/checks/html-compliance.ts
|
|
14261
|
-
import * as
|
|
14465
|
+
import * as cheerio50 from "cheerio";
|
|
14262
14466
|
async function analyzeHtmlCompliance(html, url, headers) {
|
|
14263
14467
|
const issues = [];
|
|
14264
|
-
const $ =
|
|
14468
|
+
const $ = cheerio50.load(html);
|
|
14265
14469
|
const parsedUrl = new URL(url);
|
|
14266
14470
|
const doctypeMatch = html.match(/<!DOCTYPE\s+([^>]+)>/i);
|
|
14267
14471
|
const hasDoctype = doctypeMatch !== null;
|
|
@@ -14825,7 +15029,7 @@ function getAssetType(url) {
|
|
|
14825
15029
|
}
|
|
14826
15030
|
|
|
14827
15031
|
// src/audit/checks/dom-size.ts
|
|
14828
|
-
import * as
|
|
15032
|
+
import * as cheerio51 from "cheerio";
|
|
14829
15033
|
var THRESHOLDS = {
|
|
14830
15034
|
totalElements: {
|
|
14831
15035
|
warning: 1500,
|
|
@@ -14842,7 +15046,7 @@ var THRESHOLDS = {
|
|
|
14842
15046
|
};
|
|
14843
15047
|
function analyzeDomSize(html, url) {
|
|
14844
15048
|
const issues = [];
|
|
14845
|
-
const $ =
|
|
15049
|
+
const $ = cheerio51.load(html);
|
|
14846
15050
|
const allElements = $("*");
|
|
14847
15051
|
const totalElements = allElements.length;
|
|
14848
15052
|
let maxDepth = 0;
|
|
@@ -14998,10 +15202,10 @@ function getDomReductionSuggestions(breakdown) {
|
|
|
14998
15202
|
}
|
|
14999
15203
|
|
|
15000
15204
|
// src/audit/checks/image-dimensions.ts
|
|
15001
|
-
import * as
|
|
15205
|
+
import * as cheerio52 from "cheerio";
|
|
15002
15206
|
function analyzeImageDimensions(html, url) {
|
|
15003
15207
|
const issues = [];
|
|
15004
|
-
const $ =
|
|
15208
|
+
const $ = cheerio52.load(html);
|
|
15005
15209
|
const images = $("img");
|
|
15006
15210
|
const totalImages = images.length;
|
|
15007
15211
|
let withDimensions = 0;
|
|
@@ -15112,7 +15316,7 @@ function truncateSrc(src) {
|
|
|
15112
15316
|
}
|
|
15113
15317
|
|
|
15114
15318
|
// src/audit/checks/color-contrast.ts
|
|
15115
|
-
import * as
|
|
15319
|
+
import * as cheerio53 from "cheerio";
|
|
15116
15320
|
var KNOWN_LOW_CONTRAST_PAIRS = [
|
|
15117
15321
|
{ fg: "#999999", bg: "#ffffff", ratio: 2.85 },
|
|
15118
15322
|
{ fg: "#888888", bg: "#ffffff", ratio: 3.54 },
|
|
@@ -15147,7 +15351,7 @@ var NAMED_COLORS = {
|
|
|
15147
15351
|
};
|
|
15148
15352
|
function analyzeColorContrast(html, url) {
|
|
15149
15353
|
const issues = [];
|
|
15150
|
-
const $ =
|
|
15354
|
+
const $ = cheerio53.load(html);
|
|
15151
15355
|
const potentialIssues = [];
|
|
15152
15356
|
let elementsAnalyzed = 0;
|
|
15153
15357
|
let passedChecks = 0;
|
|
@@ -15345,7 +15549,7 @@ function calculateContrastRatio(fg, bg) {
|
|
|
15345
15549
|
}
|
|
15346
15550
|
|
|
15347
15551
|
// src/audit/checks/asset-minification.ts
|
|
15348
|
-
import * as
|
|
15552
|
+
import * as cheerio54 from "cheerio";
|
|
15349
15553
|
function isMinified(content, type) {
|
|
15350
15554
|
const lines = content.split("\n");
|
|
15351
15555
|
const totalLines = lines.length;
|
|
@@ -15373,7 +15577,7 @@ function isMinified(content, type) {
|
|
|
15373
15577
|
};
|
|
15374
15578
|
}
|
|
15375
15579
|
function extractAssetUrls(html, baseUrl) {
|
|
15376
|
-
const $ =
|
|
15580
|
+
const $ = cheerio54.load(html);
|
|
15377
15581
|
const base = new URL(baseUrl);
|
|
15378
15582
|
const css = [];
|
|
15379
15583
|
const js = [];
|
|
@@ -15519,10 +15723,10 @@ async function analyzeAssetMinification(html, url) {
|
|
|
15519
15723
|
}
|
|
15520
15724
|
|
|
15521
15725
|
// src/audit/checks/page-resources.ts
|
|
15522
|
-
import * as
|
|
15726
|
+
import * as cheerio55 from "cheerio";
|
|
15523
15727
|
function analyzePageResources(html, url) {
|
|
15524
15728
|
const issues = [];
|
|
15525
|
-
const $ =
|
|
15729
|
+
const $ = cheerio55.load(html);
|
|
15526
15730
|
const baseUrl = new URL(url);
|
|
15527
15731
|
const baseHostname = baseUrl.hostname;
|
|
15528
15732
|
const stylesheets = [];
|
|
@@ -15722,7 +15926,7 @@ function analyzePageResources(html, url) {
|
|
|
15722
15926
|
}
|
|
15723
15927
|
|
|
15724
15928
|
// src/audit/checks/responsive-css.ts
|
|
15725
|
-
import * as
|
|
15929
|
+
import * as cheerio56 from "cheerio";
|
|
15726
15930
|
function extractMediaQueries(css) {
|
|
15727
15931
|
const mediaQueries = [];
|
|
15728
15932
|
const regex = /@media\s*([^{]+)/g;
|
|
@@ -15772,7 +15976,7 @@ function classifyBreakpoints(mediaQueries) {
|
|
|
15772
15976
|
}
|
|
15773
15977
|
async function analyzeResponsiveCss(html, url) {
|
|
15774
15978
|
const issues = [];
|
|
15775
|
-
const $ =
|
|
15979
|
+
const $ = cheerio56.load(html);
|
|
15776
15980
|
const baseUrl = new URL(url);
|
|
15777
15981
|
const viewportMeta = $('meta[name="viewport"]').attr("content");
|
|
15778
15982
|
const hasViewport = !!viewportMeta;
|
|
@@ -16509,10 +16713,10 @@ var urlSafetyDatabase = {
|
|
|
16509
16713
|
};
|
|
16510
16714
|
|
|
16511
16715
|
// src/audit/checks/tracking-verification.ts
|
|
16512
|
-
import * as
|
|
16716
|
+
import * as cheerio57 from "cheerio";
|
|
16513
16717
|
function analyzeTrackingVerification(html, url) {
|
|
16514
16718
|
const issues = [];
|
|
16515
|
-
const $ =
|
|
16719
|
+
const $ = cheerio57.load(html);
|
|
16516
16720
|
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);
|
|
16517
16721
|
const hasGa4 = !!ga4Match;
|
|
16518
16722
|
const ga4MeasurementId = ga4Match ? ga4Match[1] : void 0;
|
|
@@ -16646,10 +16850,10 @@ async function runFullAudit(options) {
|
|
|
16646
16850
|
const runExtendedChecks = tier !== "free";
|
|
16647
16851
|
const parsedUrl = new URL(url);
|
|
16648
16852
|
const domain = parsedUrl.hostname;
|
|
16649
|
-
console.
|
|
16853
|
+
console.error(`
|
|
16650
16854
|
\u{1F50D} Running comprehensive SEO audit on ${url}...
|
|
16651
16855
|
`);
|
|
16652
|
-
console.
|
|
16856
|
+
console.error("\u{1F4CB} Phase 1: Crawlability checks + page fetch (parallel)...");
|
|
16653
16857
|
const [crawlabilityResult, fetchResult] = await Promise.all([
|
|
16654
16858
|
runCrawlabilityChecks(url).catch((err) => {
|
|
16655
16859
|
console.error("Crawlability check failed:", err);
|
|
@@ -16659,26 +16863,32 @@ async function runFullAudit(options) {
|
|
|
16659
16863
|
timeout: 3e4,
|
|
16660
16864
|
validateStatus: () => true
|
|
16661
16865
|
}).catch((err) => {
|
|
16866
|
+
console.error("Main page fetch failed:", err instanceof Error ? { message: err.message, cause: err.cause } : err);
|
|
16662
16867
|
return { error: err, data: "", headers: {} };
|
|
16663
16868
|
})
|
|
16664
16869
|
]);
|
|
16665
16870
|
allIssues.push(...crawlabilityResult);
|
|
16666
16871
|
if ("error" in fetchResult) {
|
|
16872
|
+
const err = fetchResult.error;
|
|
16873
|
+
const errorMsg = err instanceof Error ? err.message : "Unknown error";
|
|
16874
|
+
const errorCause = err instanceof Error && err.cause ? ` (${String(err.cause)})` : "";
|
|
16667
16875
|
allIssues.push({
|
|
16668
16876
|
code: "FETCH_ERROR",
|
|
16669
16877
|
severity: "error",
|
|
16670
|
-
category: "
|
|
16671
|
-
|
|
16672
|
-
|
|
16673
|
-
|
|
16674
|
-
|
|
16675
|
-
|
|
16878
|
+
category: "indexability",
|
|
16879
|
+
// Not crawlability - robots/sitemap checks may have succeeded
|
|
16880
|
+
title: "Page fetch failed",
|
|
16881
|
+
description: `Could not load page content: ${errorMsg}${errorCause}. Only robots.txt and sitemap checks were performed.`,
|
|
16882
|
+
impact: "Cannot perform full SEO audit without page HTML content.",
|
|
16883
|
+
howToFix: "Verify the URL is accessible, the server is responding, and there are no firewall/geo blocks.",
|
|
16884
|
+
affectedUrls: [url],
|
|
16885
|
+
details: { error: errorMsg, cause: errorCause || void 0 }
|
|
16676
16886
|
});
|
|
16677
16887
|
return createReport(url, domain, allIssues, pages);
|
|
16678
16888
|
}
|
|
16679
16889
|
const html = fetchResult.data;
|
|
16680
16890
|
const headers = fetchResult.headers;
|
|
16681
|
-
console.
|
|
16891
|
+
console.error(`\u{1F4DD} Phase 2: Running synchronous HTML checks (tier: ${tier}, limit: ${checksLimit})...`);
|
|
16682
16892
|
const onPageResult = analyzeOnPage(html, url);
|
|
16683
16893
|
const structuredDataResult = analyzeStructuredData2(html, url);
|
|
16684
16894
|
const mobileResult = analyzeMobile(html, url);
|
|
@@ -16718,6 +16928,7 @@ async function runFullAudit(options) {
|
|
|
16718
16928
|
const entityResult = runPremiumChecks ? analyzeEntitySEO(html, url) : { issues: [], data: {} };
|
|
16719
16929
|
const qdfFreshnessResult = runPremiumChecks ? analyzeFreshnessSignals(html, url) : { issues: [], data: {} };
|
|
16720
16930
|
const aiContentStructureResult = runPremiumChecks ? analyzeAIContentStructure(html, url) : { issues: [], data: {} };
|
|
16931
|
+
const ragChunkReadinessResult = runPremiumChecks ? analyzeRAGChunkReadiness(html, url) : { issues: [], data: {} };
|
|
16721
16932
|
const citationQualityResult = runPremiumChecks ? analyzeCitationQuality(html, url) : { issues: [], data: {} };
|
|
16722
16933
|
const answerConcisenessResult = runPremiumChecks ? analyzeAnswerConciseness(html, url) : { issues: [], data: {} };
|
|
16723
16934
|
const brandMentionResult = runPremiumChecks ? analyzeBrandMentionOptimization(html, url) : { issues: [], data: {} };
|
|
@@ -16756,6 +16967,7 @@ async function runFullAudit(options) {
|
|
|
16756
16967
|
...entityResult.issues,
|
|
16757
16968
|
...qdfFreshnessResult.issues,
|
|
16758
16969
|
...aiContentStructureResult.issues,
|
|
16970
|
+
...ragChunkReadinessResult.issues,
|
|
16759
16971
|
...citationQualityResult.issues,
|
|
16760
16972
|
...answerConcisenessResult.issues,
|
|
16761
16973
|
...brandMentionResult.issues,
|
|
@@ -16779,7 +16991,7 @@ async function runFullAudit(options) {
|
|
|
16779
16991
|
};
|
|
16780
16992
|
}
|
|
16781
16993
|
}
|
|
16782
|
-
console.
|
|
16994
|
+
console.error("\u{1F517} Phase 3: Running async checks (parallel)...");
|
|
16783
16995
|
const safeAsync = async (name, fn, timeoutMs = 1e4) => {
|
|
16784
16996
|
try {
|
|
16785
16997
|
const resultPromise = fn();
|
|
@@ -16892,7 +17104,7 @@ async function runFullAudit(options) {
|
|
|
16892
17104
|
loadTime: perfData.loadTime,
|
|
16893
17105
|
issues: allIssues.map((i) => i.code)
|
|
16894
17106
|
});
|
|
16895
|
-
console.
|
|
17107
|
+
console.error("\n\u2705 Audit complete!\n");
|
|
16896
17108
|
return createReport(url, domain, allIssues, pages);
|
|
16897
17109
|
}
|
|
16898
17110
|
function createReport(url, domain, issues, pages) {
|
|
@@ -17053,10 +17265,10 @@ function groupIssuesByCategory(issues) {
|
|
|
17053
17265
|
}
|
|
17054
17266
|
|
|
17055
17267
|
// src/audit/checks/duplicate-content.ts
|
|
17056
|
-
import * as
|
|
17268
|
+
import * as cheerio58 from "cheerio";
|
|
17057
17269
|
import { createHash } from "crypto";
|
|
17058
17270
|
function extractContentHash(html, url) {
|
|
17059
|
-
const $ =
|
|
17271
|
+
const $ = cheerio58.load(html);
|
|
17060
17272
|
$("script, style, nav, header, footer, aside, .nav, .header, .footer, .sidebar").remove();
|
|
17061
17273
|
const title = $("title").text().trim();
|
|
17062
17274
|
const bodyText = $("body").text().replace(/\s+/g, " ").trim();
|
|
@@ -18284,7 +18496,7 @@ var PRIORITY_WEIGHTS = {
|
|
|
18284
18496
|
};
|
|
18285
18497
|
|
|
18286
18498
|
// src/keywords/engine.ts
|
|
18287
|
-
import * as
|
|
18499
|
+
import * as cheerio60 from "cheerio";
|
|
18288
18500
|
|
|
18289
18501
|
// src/keywords/prioritizer.ts
|
|
18290
18502
|
function prioritizeKeywords(keywords, siteProfile, existingMeta) {
|
|
@@ -18578,7 +18790,7 @@ function enrichKeywordsWithEstimates(keywords) {
|
|
|
18578
18790
|
}
|
|
18579
18791
|
|
|
18580
18792
|
// src/keywords/sources/free-sources.ts
|
|
18581
|
-
import * as
|
|
18793
|
+
import * as cheerio59 from "cheerio";
|
|
18582
18794
|
var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
|
|
18583
18795
|
async function getPeopleAlsoAsk(query) {
|
|
18584
18796
|
try {
|
|
@@ -18587,7 +18799,7 @@ async function getPeopleAlsoAsk(query) {
|
|
|
18587
18799
|
headers: { "User-Agent": USER_AGENT },
|
|
18588
18800
|
timeout: 1e4
|
|
18589
18801
|
});
|
|
18590
|
-
const $ =
|
|
18802
|
+
const $ = cheerio59.load(response.data);
|
|
18591
18803
|
const questions = [];
|
|
18592
18804
|
$("[data-sgrd]").each((_, el) => {
|
|
18593
18805
|
const text = $(el).text().trim();
|
|
@@ -18613,7 +18825,7 @@ async function getRelatedSearches(query) {
|
|
|
18613
18825
|
headers: { "User-Agent": USER_AGENT },
|
|
18614
18826
|
timeout: 1e4
|
|
18615
18827
|
});
|
|
18616
|
-
const $ =
|
|
18828
|
+
const $ = cheerio59.load(response.data);
|
|
18617
18829
|
const related = [];
|
|
18618
18830
|
$("div[data-ved] a").each((_, el) => {
|
|
18619
18831
|
const href = $(el).attr("href");
|
|
@@ -18703,7 +18915,7 @@ async function analyzeCompetitorTitles(query) {
|
|
|
18703
18915
|
headers: { "User-Agent": USER_AGENT },
|
|
18704
18916
|
timeout: 1e4
|
|
18705
18917
|
});
|
|
18706
|
-
const $ =
|
|
18918
|
+
const $ = cheerio59.load(response.data);
|
|
18707
18919
|
const titles = [];
|
|
18708
18920
|
const keywords = /* @__PURE__ */ new Set();
|
|
18709
18921
|
$("h3").each((_, el) => {
|
|
@@ -19035,7 +19247,7 @@ async function fetchPageMeta(url) {
|
|
|
19035
19247
|
const response = await httpGet(url, {
|
|
19036
19248
|
timeout: 1e4
|
|
19037
19249
|
});
|
|
19038
|
-
const $ =
|
|
19250
|
+
const $ = cheerio60.load(response.data);
|
|
19039
19251
|
return {
|
|
19040
19252
|
url,
|
|
19041
19253
|
title: $("title").text().trim() || void 0,
|
|
@@ -19166,7 +19378,7 @@ async function extractSeedKeywords(url) {
|
|
|
19166
19378
|
const response = await httpGet(url, {
|
|
19167
19379
|
timeout: 1e4
|
|
19168
19380
|
});
|
|
19169
|
-
const $ =
|
|
19381
|
+
const $ = cheerio60.load(response.data);
|
|
19170
19382
|
const seeds = /* @__PURE__ */ new Set();
|
|
19171
19383
|
const title = $("title").text().toLowerCase();
|
|
19172
19384
|
const titleWords = title.split(/[\s\-|:]+/).filter((w) => w.length > 3);
|
|
@@ -19188,7 +19400,7 @@ async function extractSeedKeywords(url) {
|
|
|
19188
19400
|
}
|
|
19189
19401
|
|
|
19190
19402
|
// src/keywords/site-crawler.ts
|
|
19191
|
-
import * as
|
|
19403
|
+
import * as cheerio61 from "cheerio";
|
|
19192
19404
|
var EXCLUDED_PATHS = [
|
|
19193
19405
|
"/cdn-cgi/",
|
|
19194
19406
|
"/wp-admin/",
|
|
@@ -19300,7 +19512,7 @@ async function crawlPage(url, timeout) {
|
|
|
19300
19512
|
validateStatus: (status) => status === 200
|
|
19301
19513
|
});
|
|
19302
19514
|
const html = response.data;
|
|
19303
|
-
const $ =
|
|
19515
|
+
const $ = cheerio61.load(html);
|
|
19304
19516
|
$('script, style, noscript, iframe, nav, footer, header, aside, [role="navigation"]').remove();
|
|
19305
19517
|
const title = $("title").text().trim();
|
|
19306
19518
|
const description = $('meta[name="description"]').attr("content")?.trim() || "";
|
|
@@ -21957,7 +22169,7 @@ function getDateRange(days = 28) {
|
|
|
21957
22169
|
}
|
|
21958
22170
|
|
|
21959
22171
|
// src/keywords/sources/competitor-analysis.ts
|
|
21960
|
-
import * as
|
|
22172
|
+
import * as cheerio62 from "cheerio";
|
|
21961
22173
|
var USER_AGENT2 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
|
|
21962
22174
|
async function discoverCompetitorKeywords(yourDomain, seedKeywords, competitors) {
|
|
21963
22175
|
const yourKeywords = /* @__PURE__ */ new Set();
|
|
@@ -22054,7 +22266,7 @@ async function analyzeSERP(query) {
|
|
|
22054
22266
|
headers: { "User-Agent": USER_AGENT2 },
|
|
22055
22267
|
timeout: 1e4
|
|
22056
22268
|
});
|
|
22057
|
-
const $ =
|
|
22269
|
+
const $ = cheerio62.load(response.data);
|
|
22058
22270
|
const results = [];
|
|
22059
22271
|
const relatedSearches = [];
|
|
22060
22272
|
const peopleAlsoAsk = [];
|
|
@@ -33755,6 +33967,544 @@ function getAIVisibilitySummary(results) {
|
|
|
33755
33967
|
};
|
|
33756
33968
|
}
|
|
33757
33969
|
|
|
33970
|
+
// src/ranking/types.ts
|
|
33971
|
+
var TIER_LIMITS = {
|
|
33972
|
+
free: {
|
|
33973
|
+
maxKeywords: 10,
|
|
33974
|
+
checksPerDay: 1,
|
|
33975
|
+
serpFeatures: false,
|
|
33976
|
+
competitorTracking: false,
|
|
33977
|
+
historyDays: 7
|
|
33978
|
+
},
|
|
33979
|
+
solo: {
|
|
33980
|
+
maxKeywords: 100,
|
|
33981
|
+
checksPerDay: 1,
|
|
33982
|
+
serpFeatures: true,
|
|
33983
|
+
competitorTracking: false,
|
|
33984
|
+
historyDays: 30
|
|
33985
|
+
},
|
|
33986
|
+
pro: {
|
|
33987
|
+
maxKeywords: 500,
|
|
33988
|
+
checksPerDay: 2,
|
|
33989
|
+
serpFeatures: true,
|
|
33990
|
+
competitorTracking: true,
|
|
33991
|
+
historyDays: 90
|
|
33992
|
+
},
|
|
33993
|
+
agency: {
|
|
33994
|
+
maxKeywords: 2e3,
|
|
33995
|
+
checksPerDay: 4,
|
|
33996
|
+
serpFeatures: true,
|
|
33997
|
+
competitorTracking: true,
|
|
33998
|
+
historyDays: 365
|
|
33999
|
+
}
|
|
34000
|
+
};
|
|
34001
|
+
|
|
34002
|
+
// src/ranking/serp-client.ts
|
|
34003
|
+
var SerpClient = class {
|
|
34004
|
+
config;
|
|
34005
|
+
constructor(config) {
|
|
34006
|
+
this.config = config;
|
|
34007
|
+
}
|
|
34008
|
+
/**
|
|
34009
|
+
* Check ranking for a single keyword
|
|
34010
|
+
*/
|
|
34011
|
+
async checkRank(options) {
|
|
34012
|
+
const results = [];
|
|
34013
|
+
for (const keyword of options.keywords) {
|
|
34014
|
+
try {
|
|
34015
|
+
const result = await this.checkSingleKeyword({
|
|
34016
|
+
...options,
|
|
34017
|
+
keyword
|
|
34018
|
+
});
|
|
34019
|
+
results.push(result);
|
|
34020
|
+
} catch (error) {
|
|
34021
|
+
console.error(`Error checking rank for "${keyword}":`, error);
|
|
34022
|
+
results.push({
|
|
34023
|
+
keyword,
|
|
34024
|
+
position: null,
|
|
34025
|
+
url: null,
|
|
34026
|
+
serpFeatures: [],
|
|
34027
|
+
topResults: [],
|
|
34028
|
+
checkedAt: /* @__PURE__ */ new Date()
|
|
34029
|
+
});
|
|
34030
|
+
}
|
|
34031
|
+
}
|
|
34032
|
+
return results;
|
|
34033
|
+
}
|
|
34034
|
+
async checkSingleKeyword(options) {
|
|
34035
|
+
switch (this.config.provider) {
|
|
34036
|
+
case "valueserp":
|
|
34037
|
+
return this.checkViaValueSerp(options);
|
|
34038
|
+
case "serpapi":
|
|
34039
|
+
return this.checkViaSerpApi(options);
|
|
34040
|
+
case "direct":
|
|
34041
|
+
default:
|
|
34042
|
+
return this.checkViaDirect(options);
|
|
34043
|
+
}
|
|
34044
|
+
}
|
|
34045
|
+
/**
|
|
34046
|
+
* ValueSERP API implementation
|
|
34047
|
+
* Docs: https://www.valueserp.com/docs
|
|
34048
|
+
*/
|
|
34049
|
+
async checkViaValueSerp(options) {
|
|
34050
|
+
if (!this.config.apiKey) {
|
|
34051
|
+
throw new Error("ValueSERP API key required");
|
|
34052
|
+
}
|
|
34053
|
+
const params = new URLSearchParams({
|
|
34054
|
+
api_key: this.config.apiKey,
|
|
34055
|
+
q: options.keyword,
|
|
34056
|
+
location: options.country || "United States",
|
|
34057
|
+
google_domain: options.country === "US" ? "google.com" : `google.${options.country?.toLowerCase() || "com"}`,
|
|
34058
|
+
gl: options.country || "us",
|
|
34059
|
+
hl: options.language || "en",
|
|
34060
|
+
device: options.device || "desktop",
|
|
34061
|
+
num: "100"
|
|
34062
|
+
// Get top 100 results
|
|
34063
|
+
});
|
|
34064
|
+
const response = await fetch(`https://api.valueserp.com/search?${params}`);
|
|
34065
|
+
if (!response.ok) {
|
|
34066
|
+
throw new Error(`ValueSERP API error: ${response.status}`);
|
|
34067
|
+
}
|
|
34068
|
+
const data = await response.json();
|
|
34069
|
+
return this.parseValueSerpResponse(data, options.domain, options.keyword);
|
|
34070
|
+
}
|
|
34071
|
+
/**
|
|
34072
|
+
* SerpAPI implementation (alternative)
|
|
34073
|
+
* Docs: https://serpapi.com/search-api
|
|
34074
|
+
*/
|
|
34075
|
+
async checkViaSerpApi(options) {
|
|
34076
|
+
if (!this.config.apiKey) {
|
|
34077
|
+
throw new Error("SerpAPI key required");
|
|
34078
|
+
}
|
|
34079
|
+
const params = new URLSearchParams({
|
|
34080
|
+
api_key: this.config.apiKey,
|
|
34081
|
+
q: options.keyword,
|
|
34082
|
+
location: options.country === "US" ? "United States" : options.country || "United States",
|
|
34083
|
+
gl: options.country?.toLowerCase() || "us",
|
|
34084
|
+
hl: options.language || "en",
|
|
34085
|
+
device: options.device || "desktop",
|
|
34086
|
+
num: "100"
|
|
34087
|
+
});
|
|
34088
|
+
const response = await fetch(`https://serpapi.com/search?${params}`);
|
|
34089
|
+
if (!response.ok) {
|
|
34090
|
+
throw new Error(`SerpAPI error: ${response.status}`);
|
|
34091
|
+
}
|
|
34092
|
+
const data = await response.json();
|
|
34093
|
+
return this.parseSerpApiResponse(data, options.domain, options.keyword);
|
|
34094
|
+
}
|
|
34095
|
+
/**
|
|
34096
|
+
* Direct scraping fallback (rate-limited, use with caution)
|
|
34097
|
+
*/
|
|
34098
|
+
async checkViaDirect(options) {
|
|
34099
|
+
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(options.keyword)}&num=100`;
|
|
34100
|
+
const headers = {
|
|
34101
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
34102
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
34103
|
+
"Accept-Language": options.language || "en-US,en;q=0.9"
|
|
34104
|
+
};
|
|
34105
|
+
const response = await fetch(searchUrl, { headers });
|
|
34106
|
+
if (!response.ok) {
|
|
34107
|
+
throw new Error(`Direct search failed: ${response.status}`);
|
|
34108
|
+
}
|
|
34109
|
+
const html = await response.text();
|
|
34110
|
+
return this.parseDirectSearchResults(html, options.domain, options.keyword);
|
|
34111
|
+
}
|
|
34112
|
+
parseValueSerpResponse(data, domain, keyword) {
|
|
34113
|
+
const topResults = [];
|
|
34114
|
+
const serpFeatures = [];
|
|
34115
|
+
let position = null;
|
|
34116
|
+
let url = null;
|
|
34117
|
+
if (data.organic_results) {
|
|
34118
|
+
for (let i = 0; i < data.organic_results.length; i++) {
|
|
34119
|
+
const result = data.organic_results[i];
|
|
34120
|
+
const resultDomain = this.extractDomain(result.link);
|
|
34121
|
+
topResults.push({
|
|
34122
|
+
position: i + 1,
|
|
34123
|
+
url: result.link,
|
|
34124
|
+
domain: resultDomain,
|
|
34125
|
+
title: result.title
|
|
34126
|
+
});
|
|
34127
|
+
if (this.domainMatches(resultDomain, domain) && position === null) {
|
|
34128
|
+
position = i + 1;
|
|
34129
|
+
url = result.link;
|
|
34130
|
+
}
|
|
34131
|
+
}
|
|
34132
|
+
}
|
|
34133
|
+
if (data.answer_box) {
|
|
34134
|
+
serpFeatures.push({
|
|
34135
|
+
type: "featured_snippet",
|
|
34136
|
+
position: 0,
|
|
34137
|
+
hasOwnSite: this.domainMatches(this.extractDomain(data.answer_box.link || ""), domain)
|
|
34138
|
+
});
|
|
34139
|
+
}
|
|
34140
|
+
if (data.people_also_ask) {
|
|
34141
|
+
serpFeatures.push({
|
|
34142
|
+
type: "people_also_ask",
|
|
34143
|
+
hasOwnSite: data.people_also_ask.some(
|
|
34144
|
+
(paa) => this.domainMatches(this.extractDomain(paa.link || ""), domain)
|
|
34145
|
+
)
|
|
34146
|
+
});
|
|
34147
|
+
}
|
|
34148
|
+
if (data.local_results) {
|
|
34149
|
+
serpFeatures.push({ type: "local_pack" });
|
|
34150
|
+
}
|
|
34151
|
+
if (data.knowledge_graph) {
|
|
34152
|
+
serpFeatures.push({ type: "knowledge_panel" });
|
|
34153
|
+
}
|
|
34154
|
+
return {
|
|
34155
|
+
keyword,
|
|
34156
|
+
position,
|
|
34157
|
+
url,
|
|
34158
|
+
serpFeatures,
|
|
34159
|
+
topResults: topResults.slice(0, 10),
|
|
34160
|
+
checkedAt: /* @__PURE__ */ new Date()
|
|
34161
|
+
};
|
|
34162
|
+
}
|
|
34163
|
+
parseSerpApiResponse(data, domain, keyword) {
|
|
34164
|
+
const topResults = [];
|
|
34165
|
+
const serpFeatures = [];
|
|
34166
|
+
let position = null;
|
|
34167
|
+
let url = null;
|
|
34168
|
+
if (data.organic_results) {
|
|
34169
|
+
for (let i = 0; i < data.organic_results.length; i++) {
|
|
34170
|
+
const result = data.organic_results[i];
|
|
34171
|
+
const resultDomain = this.extractDomain(result.link);
|
|
34172
|
+
topResults.push({
|
|
34173
|
+
position: result.position || i + 1,
|
|
34174
|
+
url: result.link,
|
|
34175
|
+
domain: resultDomain,
|
|
34176
|
+
title: result.title
|
|
34177
|
+
});
|
|
34178
|
+
if (this.domainMatches(resultDomain, domain) && position === null) {
|
|
34179
|
+
position = result.position || i + 1;
|
|
34180
|
+
url = result.link;
|
|
34181
|
+
}
|
|
34182
|
+
}
|
|
34183
|
+
}
|
|
34184
|
+
if (data.answer_box) {
|
|
34185
|
+
serpFeatures.push({
|
|
34186
|
+
type: "featured_snippet",
|
|
34187
|
+
position: 0
|
|
34188
|
+
});
|
|
34189
|
+
}
|
|
34190
|
+
if (data.related_questions) {
|
|
34191
|
+
serpFeatures.push({ type: "people_also_ask" });
|
|
34192
|
+
}
|
|
34193
|
+
return {
|
|
34194
|
+
keyword,
|
|
34195
|
+
position,
|
|
34196
|
+
url,
|
|
34197
|
+
serpFeatures,
|
|
34198
|
+
topResults: topResults.slice(0, 10),
|
|
34199
|
+
checkedAt: /* @__PURE__ */ new Date()
|
|
34200
|
+
};
|
|
34201
|
+
}
|
|
34202
|
+
parseDirectSearchResults(html, domain, keyword) {
|
|
34203
|
+
const topResults = [];
|
|
34204
|
+
let position = null;
|
|
34205
|
+
let url = null;
|
|
34206
|
+
const linkRegex = /<a[^>]+href="\/url\?q=([^"&]+)/g;
|
|
34207
|
+
let match;
|
|
34208
|
+
let index = 0;
|
|
34209
|
+
while ((match = linkRegex.exec(html)) !== null && index < 100) {
|
|
34210
|
+
try {
|
|
34211
|
+
const decodedUrl = decodeURIComponent(match[1]);
|
|
34212
|
+
const resultDomain = this.extractDomain(decodedUrl);
|
|
34213
|
+
if (resultDomain.includes("google.com") || resultDomain.includes("gstatic.com")) {
|
|
34214
|
+
continue;
|
|
34215
|
+
}
|
|
34216
|
+
index++;
|
|
34217
|
+
topResults.push({
|
|
34218
|
+
position: index,
|
|
34219
|
+
url: decodedUrl,
|
|
34220
|
+
domain: resultDomain
|
|
34221
|
+
});
|
|
34222
|
+
if (this.domainMatches(resultDomain, domain) && position === null) {
|
|
34223
|
+
position = index;
|
|
34224
|
+
url = decodedUrl;
|
|
34225
|
+
}
|
|
34226
|
+
} catch {
|
|
34227
|
+
}
|
|
34228
|
+
}
|
|
34229
|
+
return {
|
|
34230
|
+
keyword,
|
|
34231
|
+
position,
|
|
34232
|
+
url,
|
|
34233
|
+
serpFeatures: [],
|
|
34234
|
+
// Direct scraping doesn't easily extract SERP features
|
|
34235
|
+
topResults: topResults.slice(0, 10),
|
|
34236
|
+
checkedAt: /* @__PURE__ */ new Date()
|
|
34237
|
+
};
|
|
34238
|
+
}
|
|
34239
|
+
extractDomain(url) {
|
|
34240
|
+
try {
|
|
34241
|
+
const parsed = new URL(url);
|
|
34242
|
+
return parsed.hostname.replace(/^www\./, "");
|
|
34243
|
+
} catch {
|
|
34244
|
+
return "";
|
|
34245
|
+
}
|
|
34246
|
+
}
|
|
34247
|
+
domainMatches(resultDomain, targetDomain) {
|
|
34248
|
+
const normalizedResult = resultDomain.toLowerCase().replace(/^www\./, "");
|
|
34249
|
+
const normalizedTarget = targetDomain.toLowerCase().replace(/^www\./, "");
|
|
34250
|
+
return normalizedResult === normalizedTarget || normalizedResult.endsWith(`.${normalizedTarget}`);
|
|
34251
|
+
}
|
|
34252
|
+
};
|
|
34253
|
+
|
|
34254
|
+
// src/ranking/tracker.ts
|
|
34255
|
+
var RankTracker = class {
|
|
34256
|
+
supabase;
|
|
34257
|
+
serpClient;
|
|
34258
|
+
constructor(config) {
|
|
34259
|
+
this.supabase = config.supabase;
|
|
34260
|
+
this.serpClient = new SerpClient(config.serpConfig);
|
|
34261
|
+
}
|
|
34262
|
+
/**
|
|
34263
|
+
* Add keywords to track for a project
|
|
34264
|
+
*/
|
|
34265
|
+
async addKeywords(projectId, keywords, options) {
|
|
34266
|
+
const keywordRecords = keywords.map((keyword) => ({
|
|
34267
|
+
project_id: projectId,
|
|
34268
|
+
keyword: keyword.toLowerCase().trim(),
|
|
34269
|
+
search_engine: options?.searchEngine || "google",
|
|
34270
|
+
country: options?.country || "US",
|
|
34271
|
+
language: options?.language || "en",
|
|
34272
|
+
track_url: options?.trackUrl,
|
|
34273
|
+
is_active: true
|
|
34274
|
+
}));
|
|
34275
|
+
const { data, error } = await this.supabase.from("keywords").upsert(keywordRecords, {
|
|
34276
|
+
onConflict: "project_id,keyword",
|
|
34277
|
+
ignoreDuplicates: false
|
|
34278
|
+
}).select();
|
|
34279
|
+
if (error) {
|
|
34280
|
+
throw new Error(`Failed to add keywords: ${error.message}`);
|
|
34281
|
+
}
|
|
34282
|
+
return (data || []).map(this.mapKeyword);
|
|
34283
|
+
}
|
|
34284
|
+
/**
|
|
34285
|
+
* Remove keywords from tracking
|
|
34286
|
+
*/
|
|
34287
|
+
async removeKeywords(projectId, keywords) {
|
|
34288
|
+
const normalizedKeywords = keywords.map((k) => k.toLowerCase().trim());
|
|
34289
|
+
const { error } = await this.supabase.from("keywords").update({ is_active: false }).eq("project_id", projectId).in("keyword", normalizedKeywords);
|
|
34290
|
+
if (error) {
|
|
34291
|
+
throw new Error(`Failed to remove keywords: ${error.message}`);
|
|
34292
|
+
}
|
|
34293
|
+
}
|
|
34294
|
+
/**
|
|
34295
|
+
* Get all tracked keywords for a project
|
|
34296
|
+
*/
|
|
34297
|
+
async getKeywords(projectId, includeInactive = false) {
|
|
34298
|
+
let query = this.supabase.from("keywords").select("*").eq("project_id", projectId);
|
|
34299
|
+
if (!includeInactive) {
|
|
34300
|
+
query = query.eq("is_active", true);
|
|
34301
|
+
}
|
|
34302
|
+
const { data, error } = await query;
|
|
34303
|
+
if (error) {
|
|
34304
|
+
throw new Error(`Failed to get keywords: ${error.message}`);
|
|
34305
|
+
}
|
|
34306
|
+
return (data || []).map(this.mapKeyword);
|
|
34307
|
+
}
|
|
34308
|
+
/**
|
|
34309
|
+
* Check rankings for all active keywords in a project
|
|
34310
|
+
*/
|
|
34311
|
+
async checkRankings(projectId, domain) {
|
|
34312
|
+
const keywords = await this.getKeywords(projectId);
|
|
34313
|
+
if (keywords.length === 0) {
|
|
34314
|
+
return [];
|
|
34315
|
+
}
|
|
34316
|
+
const groups = this.groupKeywords(keywords);
|
|
34317
|
+
const results = [];
|
|
34318
|
+
for (const group of groups) {
|
|
34319
|
+
const checkResults = await this.serpClient.checkRank({
|
|
34320
|
+
keywords: group.keywords.map((k) => k.keyword),
|
|
34321
|
+
domain,
|
|
34322
|
+
searchEngine: group.searchEngine,
|
|
34323
|
+
country: group.country,
|
|
34324
|
+
language: group.language
|
|
34325
|
+
});
|
|
34326
|
+
for (const result of checkResults) {
|
|
34327
|
+
const keyword = group.keywords.find((k) => k.keyword === result.keyword);
|
|
34328
|
+
if (!keyword) continue;
|
|
34329
|
+
await this.saveRanking(keyword.id, result);
|
|
34330
|
+
results.push({
|
|
34331
|
+
keywordId: keyword.id,
|
|
34332
|
+
keyword: result.keyword,
|
|
34333
|
+
position: result.position,
|
|
34334
|
+
url: result.url,
|
|
34335
|
+
serpFeatures: result.serpFeatures,
|
|
34336
|
+
competitorUrls: result.topResults,
|
|
34337
|
+
checkedAt: result.checkedAt
|
|
34338
|
+
});
|
|
34339
|
+
}
|
|
34340
|
+
}
|
|
34341
|
+
await this.supabase.from("projects").update({ last_rank_check_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", projectId);
|
|
34342
|
+
return results;
|
|
34343
|
+
}
|
|
34344
|
+
/**
|
|
34345
|
+
* Save a ranking result to the database
|
|
34346
|
+
*/
|
|
34347
|
+
async saveRanking(keywordId, result) {
|
|
34348
|
+
const { data: currentKeyword } = await this.supabase.from("keywords").select("current_position, best_position").eq("id", keywordId).single();
|
|
34349
|
+
const previousPosition = currentKeyword?.current_position;
|
|
34350
|
+
const bestPosition = currentKeyword?.best_position;
|
|
34351
|
+
const newBestPosition = result.position !== null && (bestPosition === null || result.position < bestPosition) ? result.position : bestPosition;
|
|
34352
|
+
const { error: updateError } = await this.supabase.from("keywords").update({
|
|
34353
|
+
previous_position: previousPosition,
|
|
34354
|
+
current_position: result.position,
|
|
34355
|
+
best_position: newBestPosition,
|
|
34356
|
+
last_checked: result.checkedAt.toISOString()
|
|
34357
|
+
}).eq("id", keywordId);
|
|
34358
|
+
if (updateError) {
|
|
34359
|
+
console.error(`Failed to save ranking for ${result.keyword}:`, updateError);
|
|
34360
|
+
}
|
|
34361
|
+
}
|
|
34362
|
+
/**
|
|
34363
|
+
* Get ranking history for a keyword
|
|
34364
|
+
* Note: Full history requires keyword_ranking_history table with keyword_ranking_id
|
|
34365
|
+
* For now, returns current state as single history entry
|
|
34366
|
+
*/
|
|
34367
|
+
async getHistory(keywordId, _days = 30) {
|
|
34368
|
+
const { data, error } = await this.supabase.from("keywords").select("current_position, target_url, last_checked").eq("id", keywordId).single();
|
|
34369
|
+
if (error || !data) {
|
|
34370
|
+
return [];
|
|
34371
|
+
}
|
|
34372
|
+
return [{
|
|
34373
|
+
position: data.current_position,
|
|
34374
|
+
url: data.target_url,
|
|
34375
|
+
serpFeatures: [],
|
|
34376
|
+
recordedAt: data.last_checked ? new Date(data.last_checked) : /* @__PURE__ */ new Date()
|
|
34377
|
+
}];
|
|
34378
|
+
}
|
|
34379
|
+
/**
|
|
34380
|
+
* Get keyword trends for a project
|
|
34381
|
+
*/
|
|
34382
|
+
async getTrends(projectId) {
|
|
34383
|
+
const { data, error } = await this.supabase.from("keyword_rank_trends").select("*").eq("project_id", projectId);
|
|
34384
|
+
if (error) {
|
|
34385
|
+
return this.calculateTrends(projectId);
|
|
34386
|
+
}
|
|
34387
|
+
return (data || []).map((row) => ({
|
|
34388
|
+
keywordId: row.keyword_id,
|
|
34389
|
+
keyword: row.keyword,
|
|
34390
|
+
currentPosition: row.current_position,
|
|
34391
|
+
bestPosition: row.best_position,
|
|
34392
|
+
positionChange: row.position_change || 0,
|
|
34393
|
+
avgPosition: row.avg_position || 0,
|
|
34394
|
+
dataPoints: row.data_points || 0
|
|
34395
|
+
}));
|
|
34396
|
+
}
|
|
34397
|
+
/**
|
|
34398
|
+
* Manual trend calculation fallback
|
|
34399
|
+
*/
|
|
34400
|
+
async calculateTrends(projectId) {
|
|
34401
|
+
const keywords = await this.getKeywords(projectId);
|
|
34402
|
+
const trends = [];
|
|
34403
|
+
for (const keyword of keywords) {
|
|
34404
|
+
const history = await this.getHistory(keyword.id, 30);
|
|
34405
|
+
if (history.length === 0) {
|
|
34406
|
+
trends.push({
|
|
34407
|
+
keywordId: keyword.id,
|
|
34408
|
+
keyword: keyword.keyword,
|
|
34409
|
+
currentPosition: keyword.currentPosition,
|
|
34410
|
+
bestPosition: keyword.bestPosition,
|
|
34411
|
+
positionChange: 0,
|
|
34412
|
+
avgPosition: keyword.currentPosition || 0,
|
|
34413
|
+
dataPoints: 0
|
|
34414
|
+
});
|
|
34415
|
+
continue;
|
|
34416
|
+
}
|
|
34417
|
+
const positions = history.filter((h) => h.position !== null).map((h) => h.position);
|
|
34418
|
+
const avgPosition = positions.length > 0 ? positions.reduce((a, b) => a + b, 0) / positions.length : 0;
|
|
34419
|
+
const positionChange = keyword.previousPosition && keyword.currentPosition ? keyword.previousPosition - keyword.currentPosition : 0;
|
|
34420
|
+
trends.push({
|
|
34421
|
+
keywordId: keyword.id,
|
|
34422
|
+
keyword: keyword.keyword,
|
|
34423
|
+
currentPosition: keyword.currentPosition,
|
|
34424
|
+
bestPosition: keyword.bestPosition,
|
|
34425
|
+
positionChange,
|
|
34426
|
+
avgPosition,
|
|
34427
|
+
dataPoints: history.length
|
|
34428
|
+
});
|
|
34429
|
+
}
|
|
34430
|
+
return trends;
|
|
34431
|
+
}
|
|
34432
|
+
/**
|
|
34433
|
+
* Export ranking data as CSV
|
|
34434
|
+
*/
|
|
34435
|
+
async exportCSV(projectId, days = 30) {
|
|
34436
|
+
const keywords = await this.getKeywords(projectId);
|
|
34437
|
+
const rows = ["Keyword,Current Position,Best Position,Last Checked,Trend"];
|
|
34438
|
+
for (const keyword of keywords) {
|
|
34439
|
+
const history = await this.getHistory(keyword.id, days);
|
|
34440
|
+
const trend = this.calculatePositionTrend(history);
|
|
34441
|
+
rows.push([
|
|
34442
|
+
`"${keyword.keyword}"`,
|
|
34443
|
+
keyword.currentPosition?.toString() || "N/A",
|
|
34444
|
+
keyword.bestPosition?.toString() || "N/A",
|
|
34445
|
+
keyword.lastChecked?.toISOString() || "Never",
|
|
34446
|
+
trend
|
|
34447
|
+
].join(","));
|
|
34448
|
+
}
|
|
34449
|
+
return rows.join("\n");
|
|
34450
|
+
}
|
|
34451
|
+
/**
|
|
34452
|
+
* Calculate position trend from history
|
|
34453
|
+
*/
|
|
34454
|
+
calculatePositionTrend(history) {
|
|
34455
|
+
if (history.length < 2) return "\u2192";
|
|
34456
|
+
const recent = history[0]?.position;
|
|
34457
|
+
const older = history[history.length - 1]?.position;
|
|
34458
|
+
if (recent === null || older === null) return "\u2192";
|
|
34459
|
+
if (recent < older) return "\u2191";
|
|
34460
|
+
if (recent > older) return "\u2193";
|
|
34461
|
+
return "\u2192";
|
|
34462
|
+
}
|
|
34463
|
+
/**
|
|
34464
|
+
* Group keywords by search engine and country
|
|
34465
|
+
*/
|
|
34466
|
+
groupKeywords(keywords) {
|
|
34467
|
+
const groups = /* @__PURE__ */ new Map();
|
|
34468
|
+
for (const keyword of keywords) {
|
|
34469
|
+
const key = `${keyword.searchEngine}:${keyword.country}:${keyword.language}`;
|
|
34470
|
+
if (!groups.has(key)) {
|
|
34471
|
+
groups.set(key, []);
|
|
34472
|
+
}
|
|
34473
|
+
groups.get(key).push(keyword);
|
|
34474
|
+
}
|
|
34475
|
+
return Array.from(groups.entries()).map(([key, keywords2]) => {
|
|
34476
|
+
const [searchEngine, country, language] = key.split(":");
|
|
34477
|
+
return {
|
|
34478
|
+
searchEngine,
|
|
34479
|
+
country,
|
|
34480
|
+
language,
|
|
34481
|
+
keywords: keywords2
|
|
34482
|
+
};
|
|
34483
|
+
});
|
|
34484
|
+
}
|
|
34485
|
+
/**
|
|
34486
|
+
* Map database row to TrackedKeyword
|
|
34487
|
+
*/
|
|
34488
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34489
|
+
mapKeyword(row) {
|
|
34490
|
+
return {
|
|
34491
|
+
id: row.id,
|
|
34492
|
+
projectId: row.project_id,
|
|
34493
|
+
keyword: row.keyword,
|
|
34494
|
+
searchEngine: row.search_engine || "google",
|
|
34495
|
+
country: row.country || "US",
|
|
34496
|
+
language: row.language || "en",
|
|
34497
|
+
currentPosition: row.current_position,
|
|
34498
|
+
previousPosition: row.previous_position,
|
|
34499
|
+
bestPosition: row.best_position,
|
|
34500
|
+
trackUrl: row.track_url,
|
|
34501
|
+
isActive: row.is_active,
|
|
34502
|
+
lastChecked: row.last_checked ? new Date(row.last_checked) : null,
|
|
34503
|
+
createdAt: new Date(row.created_at)
|
|
34504
|
+
};
|
|
34505
|
+
}
|
|
34506
|
+
};
|
|
34507
|
+
|
|
33758
34508
|
// src/analyzers/index.ts
|
|
33759
34509
|
var analyzers_exports = {};
|
|
33760
34510
|
__export(analyzers_exports, {
|
|
@@ -34937,9 +35687,12 @@ export {
|
|
|
34937
35687
|
LOCATION_CODES,
|
|
34938
35688
|
OG_IMAGE_SPECS,
|
|
34939
35689
|
PRIORITY_WEIGHTS,
|
|
35690
|
+
RankTracker,
|
|
34940
35691
|
SEO_SCOPES,
|
|
34941
35692
|
SITE_PROFILE_QUESTIONS,
|
|
34942
35693
|
Schemas,
|
|
35694
|
+
SerpClient,
|
|
35695
|
+
TIER_LIMITS,
|
|
34943
35696
|
addTrackingResult,
|
|
34944
35697
|
analyzeAnchorText,
|
|
34945
35698
|
analyzeCanonicalAdvanced,
|