@rankcli/agent-runtime 0.0.1
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/README.md +242 -0
- package/dist/analyzer-2CSWIQGD.mjs +6 -0
- package/dist/chunk-YNZYHEYM.mjs +774 -0
- package/dist/index.d.mts +4012 -0
- package/dist/index.d.ts +4012 -0
- package/dist/index.js +29672 -0
- package/dist/index.mjs +28602 -0
- package/package.json +53 -0
- package/scripts/build-deno.ts +134 -0
- package/src/audit/ai/analyzer.ts +347 -0
- package/src/audit/ai/index.ts +29 -0
- package/src/audit/ai/prompts/content-analysis.ts +271 -0
- package/src/audit/ai/types.ts +179 -0
- package/src/audit/checks/additional-checks.ts +439 -0
- package/src/audit/checks/ai-citation-worthiness.ts +399 -0
- package/src/audit/checks/ai-content-structure.ts +325 -0
- package/src/audit/checks/ai-readiness.ts +339 -0
- package/src/audit/checks/anchor-text.ts +179 -0
- package/src/audit/checks/answer-conciseness.ts +322 -0
- package/src/audit/checks/asset-minification.ts +270 -0
- package/src/audit/checks/bing-optimization.ts +206 -0
- package/src/audit/checks/brand-mention-optimization.ts +349 -0
- package/src/audit/checks/caching-headers.ts +305 -0
- package/src/audit/checks/canonical-advanced.ts +150 -0
- package/src/audit/checks/canonical-domain.ts +196 -0
- package/src/audit/checks/citation-quality.ts +358 -0
- package/src/audit/checks/client-rendering.ts +542 -0
- package/src/audit/checks/color-contrast.ts +342 -0
- package/src/audit/checks/content-freshness.ts +170 -0
- package/src/audit/checks/content-science.ts +589 -0
- package/src/audit/checks/conversion-elements.ts +526 -0
- package/src/audit/checks/crawlability.ts +220 -0
- package/src/audit/checks/directory-listing.ts +172 -0
- package/src/audit/checks/dom-analysis.ts +191 -0
- package/src/audit/checks/dom-size.ts +246 -0
- package/src/audit/checks/duplicate-content.ts +194 -0
- package/src/audit/checks/eeat-signals.ts +990 -0
- package/src/audit/checks/entity-seo.ts +396 -0
- package/src/audit/checks/featured-snippet.ts +473 -0
- package/src/audit/checks/freshness-signals.ts +443 -0
- package/src/audit/checks/funnel-intent.ts +463 -0
- package/src/audit/checks/hreflang.ts +174 -0
- package/src/audit/checks/html-compliance.ts +302 -0
- package/src/audit/checks/image-dimensions.ts +167 -0
- package/src/audit/checks/images.ts +160 -0
- package/src/audit/checks/indexnow.ts +275 -0
- package/src/audit/checks/interactive-tools.ts +475 -0
- package/src/audit/checks/internal-link-graph.ts +436 -0
- package/src/audit/checks/keyword-analysis.ts +239 -0
- package/src/audit/checks/keyword-cannibalization.ts +385 -0
- package/src/audit/checks/keyword-placement.ts +471 -0
- package/src/audit/checks/links.ts +203 -0
- package/src/audit/checks/llms-txt.ts +224 -0
- package/src/audit/checks/local-seo.ts +296 -0
- package/src/audit/checks/mobile.ts +167 -0
- package/src/audit/checks/modern-images.ts +226 -0
- package/src/audit/checks/navboost-signals.ts +395 -0
- package/src/audit/checks/on-page.ts +209 -0
- package/src/audit/checks/page-resources.ts +285 -0
- package/src/audit/checks/pagination.ts +180 -0
- package/src/audit/checks/performance.ts +153 -0
- package/src/audit/checks/platform-presence.ts +580 -0
- package/src/audit/checks/redirect-analysis.ts +153 -0
- package/src/audit/checks/redirect-chain.ts +389 -0
- package/src/audit/checks/resource-hints.ts +420 -0
- package/src/audit/checks/responsive-css.ts +247 -0
- package/src/audit/checks/responsive-images.ts +396 -0
- package/src/audit/checks/review-ecosystem.ts +415 -0
- package/src/audit/checks/robots-validation.ts +373 -0
- package/src/audit/checks/security-headers.ts +172 -0
- package/src/audit/checks/security.ts +144 -0
- package/src/audit/checks/serp-preview.ts +251 -0
- package/src/audit/checks/site-maturity.ts +444 -0
- package/src/audit/checks/social-meta.test.ts +275 -0
- package/src/audit/checks/social-meta.ts +134 -0
- package/src/audit/checks/soft-404.ts +151 -0
- package/src/audit/checks/structured-data.ts +238 -0
- package/src/audit/checks/tech-detection.ts +496 -0
- package/src/audit/checks/topical-clusters.ts +435 -0
- package/src/audit/checks/tracker-bloat.ts +462 -0
- package/src/audit/checks/tracking-verification.test.ts +371 -0
- package/src/audit/checks/tracking-verification.ts +636 -0
- package/src/audit/checks/url-safety.ts +682 -0
- package/src/audit/deno-entry.ts +66 -0
- package/src/audit/discovery/index.ts +15 -0
- package/src/audit/discovery/link-crawler.ts +232 -0
- package/src/audit/discovery/repo-routes.ts +347 -0
- package/src/audit/engine.ts +620 -0
- package/src/audit/fixes/index.ts +209 -0
- package/src/audit/fixes/social-meta-fixes.test.ts +329 -0
- package/src/audit/fixes/social-meta-fixes.ts +463 -0
- package/src/audit/index.ts +74 -0
- package/src/audit/runner.test.ts +299 -0
- package/src/audit/runner.ts +130 -0
- package/src/audit/types.ts +1953 -0
- package/src/content/featured-snippet.ts +367 -0
- package/src/content/generator.test.ts +534 -0
- package/src/content/generator.ts +501 -0
- package/src/content/headline.ts +317 -0
- package/src/content/index.ts +62 -0
- package/src/content/intent.ts +258 -0
- package/src/content/keyword-density.ts +349 -0
- package/src/content/readability.ts +262 -0
- package/src/executor.ts +336 -0
- package/src/fixer.ts +416 -0
- package/src/frameworks/detector.test.ts +248 -0
- package/src/frameworks/detector.ts +371 -0
- package/src/frameworks/index.ts +68 -0
- package/src/frameworks/recipes/angular.yaml +171 -0
- package/src/frameworks/recipes/astro.yaml +206 -0
- package/src/frameworks/recipes/django.yaml +180 -0
- package/src/frameworks/recipes/laravel.yaml +137 -0
- package/src/frameworks/recipes/nextjs.yaml +268 -0
- package/src/frameworks/recipes/nuxt.yaml +175 -0
- package/src/frameworks/recipes/rails.yaml +188 -0
- package/src/frameworks/recipes/react.yaml +202 -0
- package/src/frameworks/recipes/sveltekit.yaml +154 -0
- package/src/frameworks/recipes/vue.yaml +137 -0
- package/src/frameworks/recipes/wordpress.yaml +209 -0
- package/src/frameworks/suggestion-engine.ts +320 -0
- package/src/geo/geo-content.test.ts +305 -0
- package/src/geo/geo-content.ts +266 -0
- package/src/geo/geo-history.test.ts +473 -0
- package/src/geo/geo-history.ts +433 -0
- package/src/geo/geo-tracker.test.ts +359 -0
- package/src/geo/geo-tracker.ts +411 -0
- package/src/geo/index.ts +10 -0
- package/src/git/commit-helper.test.ts +261 -0
- package/src/git/commit-helper.ts +329 -0
- package/src/git/index.ts +12 -0
- package/src/git/pr-helper.test.ts +284 -0
- package/src/git/pr-helper.ts +307 -0
- package/src/index.ts +66 -0
- package/src/keywords/ai-keyword-engine.ts +1062 -0
- package/src/keywords/ai-summarizer.ts +387 -0
- package/src/keywords/ci-mode.ts +555 -0
- package/src/keywords/engine.ts +359 -0
- package/src/keywords/index.ts +151 -0
- package/src/keywords/llm-judge.ts +357 -0
- package/src/keywords/nlp-analysis.ts +706 -0
- package/src/keywords/prioritizer.ts +295 -0
- package/src/keywords/site-crawler.ts +342 -0
- package/src/keywords/sources/autocomplete.ts +139 -0
- package/src/keywords/sources/competitive-search.ts +450 -0
- package/src/keywords/sources/competitor-analysis.ts +374 -0
- package/src/keywords/sources/dataforseo.ts +206 -0
- package/src/keywords/sources/free-sources.ts +294 -0
- package/src/keywords/sources/gsc.ts +123 -0
- package/src/keywords/topic-grouping.ts +327 -0
- package/src/keywords/types.ts +144 -0
- package/src/keywords/wizard.ts +457 -0
- package/src/loader.ts +40 -0
- package/src/reports/index.ts +7 -0
- package/src/reports/report-generator.test.ts +293 -0
- package/src/reports/report-generator.ts +713 -0
- package/src/scheduler/alerts.test.ts +458 -0
- package/src/scheduler/alerts.ts +328 -0
- package/src/scheduler/index.ts +8 -0
- package/src/scheduler/scheduled-audit.test.ts +377 -0
- package/src/scheduler/scheduled-audit.ts +149 -0
- package/src/test/integration-test.ts +325 -0
- package/src/tools/analyzer.ts +373 -0
- package/src/tools/crawl.ts +293 -0
- package/src/tools/files.ts +301 -0
- package/src/tools/h1-fixer.ts +249 -0
- package/src/tools/index.ts +67 -0
- package/src/tracking/github-action.ts +326 -0
- package/src/tracking/google-analytics.ts +265 -0
- package/src/tracking/index.ts +45 -0
- package/src/tracking/report-generator.ts +386 -0
- package/src/tracking/search-console.ts +335 -0
- package/src/types.ts +134 -0
- package/src/utils/http.ts +302 -0
- package/src/wasm-adapter.ts +297 -0
- package/src/wasm-entry.ts +14 -0
- package/tsconfig.json +17 -0
- package/tsup.wasm.config.ts +26 -0
- package/vitest.config.ts +15 -0
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rankcli/agent-runtime",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "RankCLI agent runtime - executes SEO audits and fixes with AI",
|
|
5
|
+
"homepage": "https://rankcli.dev",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.mjs",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"dev": "pnpm build:deno && tsup src/index.ts --watch --onSuccess 'pnpm build:deno'",
|
|
18
|
+
"dev:node": "tsup src/index.ts --watch",
|
|
19
|
+
"dev:deno": "tsx scripts/build-deno.ts --watch",
|
|
20
|
+
"build": "tsup src/index.ts --format cjs,esm --dts && pnpm build:deno",
|
|
21
|
+
"build:deno": "tsx scripts/build-deno.ts",
|
|
22
|
+
"build:wasm": "tsup --config tsup.wasm.config.ts",
|
|
23
|
+
"lint": "eslint src/",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"test:coverage": "vitest run --coverage"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@anthropic-ai/sdk": "^0.52.0",
|
|
30
|
+
"cheerio": "^1.0.0",
|
|
31
|
+
"openai": "^6.16.0",
|
|
32
|
+
"yaml": "^2.3.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^20.0.0",
|
|
36
|
+
"esbuild": "^0.20.0",
|
|
37
|
+
"tsup": "^8.0.0",
|
|
38
|
+
"tsx": "^4.0.0",
|
|
39
|
+
"typescript": "^5.3.0",
|
|
40
|
+
"vitest": "^1.0.0"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"seo",
|
|
44
|
+
"agent",
|
|
45
|
+
"claude",
|
|
46
|
+
"ai"
|
|
47
|
+
],
|
|
48
|
+
"author": "Brian Sam-Bodden",
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18.0.0"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Build script to create a Deno-compatible bundle of the audit engine.
|
|
4
|
+
* This bundle can be used by Supabase Edge Functions.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* pnpm build:deno # One-time build
|
|
8
|
+
* pnpm build:deno --watch # Watch mode for development
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { build, context } from 'esbuild';
|
|
12
|
+
import { writeFileSync, readFileSync, mkdirSync } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
|
|
15
|
+
const OUTPUT_DIR = join(__dirname, '../../saas/supabase/functions/_shared/audit');
|
|
16
|
+
const WATCH_MODE = process.argv.includes('--watch');
|
|
17
|
+
|
|
18
|
+
const buildOptions = {
|
|
19
|
+
entryPoints: [join(__dirname, '../src/audit/deno-entry.ts')],
|
|
20
|
+
bundle: true,
|
|
21
|
+
format: 'esm' as const,
|
|
22
|
+
platform: 'neutral' as const,
|
|
23
|
+
target: 'es2022',
|
|
24
|
+
outfile: join(OUTPUT_DIR, 'engine.bundle.js'),
|
|
25
|
+
external: ['cheerio'], // Will be imported from esm.sh in Deno
|
|
26
|
+
minify: false,
|
|
27
|
+
sourcemap: false,
|
|
28
|
+
define: {
|
|
29
|
+
'process.env.NODE_ENV': '"production"',
|
|
30
|
+
},
|
|
31
|
+
banner: {
|
|
32
|
+
js: `// Auto-generated Deno-compatible audit bundle
|
|
33
|
+
// Do not edit manually - regenerate with: pnpm build:deno
|
|
34
|
+
// Generated: ${new Date().toISOString()}
|
|
35
|
+
|
|
36
|
+
// Polyfill for Node's Buffer if needed
|
|
37
|
+
if (typeof globalThis.Buffer === 'undefined') {
|
|
38
|
+
globalThis.Buffer = {
|
|
39
|
+
from: (str, encoding) => new TextEncoder().encode(str),
|
|
40
|
+
isBuffer: () => false
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
`,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function postProcessBundle() {
|
|
48
|
+
// Replace bare cheerio imports with esm.sh URL
|
|
49
|
+
const bundlePath = join(OUTPUT_DIR, 'engine.bundle.js');
|
|
50
|
+
let content = readFileSync(bundlePath, 'utf-8');
|
|
51
|
+
|
|
52
|
+
// Replace various import patterns for cheerio
|
|
53
|
+
content = content
|
|
54
|
+
.replace(/from\s*["']cheerio["']/g, 'from "https://esm.sh/cheerio@1.0.0"')
|
|
55
|
+
.replace(/import\s*\*\s*as\s*cheerio\s*from\s*["']cheerio["']/g,
|
|
56
|
+
'import * as cheerio from "https://esm.sh/cheerio@1.0.0"')
|
|
57
|
+
.replace(/require\s*\(\s*["']cheerio["']\s*\)/g,
|
|
58
|
+
'await import("https://esm.sh/cheerio@1.0.0")');
|
|
59
|
+
|
|
60
|
+
writeFileSync(bundlePath, content);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function writeWrapper() {
|
|
64
|
+
const wrapper = `// Deno wrapper for audit engine
|
|
65
|
+
// Re-export everything from the bundle
|
|
66
|
+
export * from './engine.bundle.js';
|
|
67
|
+
`;
|
|
68
|
+
writeFileSync(join(OUTPUT_DIR, 'index.ts'), wrapper);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function buildDenoBundle() {
|
|
72
|
+
console.log('Building Deno-compatible audit bundle...');
|
|
73
|
+
|
|
74
|
+
// Ensure output directory exists
|
|
75
|
+
mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
if (WATCH_MODE) {
|
|
79
|
+
// Watch mode using esbuild context
|
|
80
|
+
const ctx = await context({
|
|
81
|
+
...buildOptions,
|
|
82
|
+
plugins: [{
|
|
83
|
+
name: 'rebuild-notify',
|
|
84
|
+
setup(build) {
|
|
85
|
+
build.onEnd(result => {
|
|
86
|
+
if (result.errors.length > 0) {
|
|
87
|
+
console.error(`[${new Date().toLocaleTimeString()}] Deno build failed:`, result.errors);
|
|
88
|
+
} else {
|
|
89
|
+
postProcessBundle();
|
|
90
|
+
writeWrapper();
|
|
91
|
+
console.log(`[${new Date().toLocaleTimeString()}] ✓ Deno bundle rebuilt`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
}],
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await ctx.watch();
|
|
99
|
+
console.log('Watching for changes in audit module...');
|
|
100
|
+
console.log('Press Ctrl+C to stop.\n');
|
|
101
|
+
|
|
102
|
+
// Keep the process running
|
|
103
|
+
process.on('SIGINT', async () => {
|
|
104
|
+
await ctx.dispose();
|
|
105
|
+
console.log('\nStopped watching.');
|
|
106
|
+
process.exit(0);
|
|
107
|
+
});
|
|
108
|
+
} else {
|
|
109
|
+
// One-time build
|
|
110
|
+
const result = await build(buildOptions);
|
|
111
|
+
|
|
112
|
+
if (result.errors.length > 0) {
|
|
113
|
+
console.error('Build errors:', result.errors);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(`✓ Bundle created at ${OUTPUT_DIR}/engine.bundle.js`);
|
|
118
|
+
|
|
119
|
+
postProcessBundle();
|
|
120
|
+
console.log(`✓ Cheerio imports replaced with esm.sh URLs`);
|
|
121
|
+
|
|
122
|
+
writeWrapper();
|
|
123
|
+
console.log(`✓ Wrapper created at ${OUTPUT_DIR}/index.ts`);
|
|
124
|
+
|
|
125
|
+
console.log('\nDeno bundle ready for use in edge functions!');
|
|
126
|
+
console.log('Import with: import { runFullAudit } from "../_shared/audit/index.ts"');
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error('Build failed:', error);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
buildDenoBundle();
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// AI-Powered SEO Analyzer
|
|
2
|
+
// Uses LLMs to provide contextual, intelligent SEO analysis
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
AIConfig,
|
|
6
|
+
AIContentAnalysis,
|
|
7
|
+
AIMetaAnalysis,
|
|
8
|
+
AISchemaAnalysis,
|
|
9
|
+
AIVisibilityAnalysis,
|
|
10
|
+
AIFixPrioritization,
|
|
11
|
+
AIAnalysisResult,
|
|
12
|
+
PageContext,
|
|
13
|
+
} from './types.js';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
CONTENT_ANALYSIS_SYSTEM,
|
|
17
|
+
CONTENT_ANALYSIS_USER,
|
|
18
|
+
META_ANALYSIS_SYSTEM,
|
|
19
|
+
META_ANALYSIS_USER,
|
|
20
|
+
SCHEMA_ANALYSIS_SYSTEM,
|
|
21
|
+
SCHEMA_ANALYSIS_USER,
|
|
22
|
+
AI_VISIBILITY_SYSTEM,
|
|
23
|
+
AI_VISIBILITY_USER,
|
|
24
|
+
FIX_PRIORITIZATION_SYSTEM,
|
|
25
|
+
FIX_PRIORITIZATION_USER,
|
|
26
|
+
} from './prompts/content-analysis.js';
|
|
27
|
+
|
|
28
|
+
export class AIAnalyzer {
|
|
29
|
+
private config: AIConfig;
|
|
30
|
+
private cache: Map<string, { result: unknown; timestamp: number }> = new Map();
|
|
31
|
+
private cacheTimeout = 3600000; // 1 hour
|
|
32
|
+
|
|
33
|
+
constructor(config: AIConfig) {
|
|
34
|
+
this.config = {
|
|
35
|
+
model: 'gpt-4o',
|
|
36
|
+
maxTokens: 4096,
|
|
37
|
+
temperature: 0.3,
|
|
38
|
+
...config,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Call OpenAI API with structured JSON response
|
|
44
|
+
*/
|
|
45
|
+
private async callOpenAI<T>(
|
|
46
|
+
systemPrompt: string,
|
|
47
|
+
userPrompt: string,
|
|
48
|
+
cacheKey?: string
|
|
49
|
+
): Promise<{ result: T; tokensUsed: number; cached: boolean }> {
|
|
50
|
+
// Check cache
|
|
51
|
+
if (cacheKey) {
|
|
52
|
+
const cached = this.cache.get(cacheKey);
|
|
53
|
+
if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
|
|
54
|
+
return { result: cached.result as T, tokensUsed: 0, cached: true };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: {
|
|
61
|
+
'Content-Type': 'application/json',
|
|
62
|
+
Authorization: `Bearer ${this.config.openaiApiKey}`,
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify({
|
|
65
|
+
model: this.config.model,
|
|
66
|
+
messages: [
|
|
67
|
+
{ role: 'system', content: systemPrompt },
|
|
68
|
+
{ role: 'user', content: userPrompt },
|
|
69
|
+
],
|
|
70
|
+
max_tokens: this.config.maxTokens,
|
|
71
|
+
temperature: this.config.temperature,
|
|
72
|
+
response_format: { type: 'json_object' },
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
const error = await response.text();
|
|
78
|
+
throw new Error(`OpenAI API error: ${response.status} - ${error}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const data = await response.json();
|
|
82
|
+
const content = data.choices?.[0]?.message?.content;
|
|
83
|
+
const tokensUsed = data.usage?.total_tokens || 0;
|
|
84
|
+
|
|
85
|
+
if (!content) {
|
|
86
|
+
throw new Error('No content in OpenAI response');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let result: T;
|
|
90
|
+
try {
|
|
91
|
+
result = JSON.parse(content) as T;
|
|
92
|
+
} catch {
|
|
93
|
+
throw new Error(`Failed to parse OpenAI JSON response: ${content}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Cache result
|
|
97
|
+
if (cacheKey) {
|
|
98
|
+
this.cache.set(cacheKey, { result, timestamp: Date.now() });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { result, tokensUsed, cached: false };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Analyze content quality using AI
|
|
106
|
+
*/
|
|
107
|
+
async analyzeContent(context: PageContext): Promise<{
|
|
108
|
+
analysis: AIContentAnalysis;
|
|
109
|
+
tokensUsed: number;
|
|
110
|
+
cached: boolean;
|
|
111
|
+
}> {
|
|
112
|
+
const cacheKey = `content:${context.url}:${context.textContent.length}`;
|
|
113
|
+
|
|
114
|
+
const { result, tokensUsed, cached } = await this.callOpenAI<AIContentAnalysis>(
|
|
115
|
+
CONTENT_ANALYSIS_SYSTEM,
|
|
116
|
+
CONTENT_ANALYSIS_USER({
|
|
117
|
+
url: context.url,
|
|
118
|
+
title: context.title,
|
|
119
|
+
h1: context.h1,
|
|
120
|
+
metaDescription: context.metaDescription,
|
|
121
|
+
textContent: context.textContent,
|
|
122
|
+
wordCount: context.wordCount,
|
|
123
|
+
headings: context.headings,
|
|
124
|
+
}),
|
|
125
|
+
cacheKey
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return { analysis: result, tokensUsed, cached };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Analyze and optimize meta tags using AI
|
|
133
|
+
*/
|
|
134
|
+
async analyzeMeta(context: PageContext): Promise<{
|
|
135
|
+
analysis: AIMetaAnalysis;
|
|
136
|
+
tokensUsed: number;
|
|
137
|
+
cached: boolean;
|
|
138
|
+
}> {
|
|
139
|
+
const cacheKey = `meta:${context.url}:${context.title}:${context.metaDescription}`;
|
|
140
|
+
|
|
141
|
+
const { result, tokensUsed, cached } = await this.callOpenAI<AIMetaAnalysis>(
|
|
142
|
+
META_ANALYSIS_SYSTEM,
|
|
143
|
+
META_ANALYSIS_USER({
|
|
144
|
+
url: context.url,
|
|
145
|
+
title: context.title,
|
|
146
|
+
metaDescription: context.metaDescription,
|
|
147
|
+
h1: context.h1,
|
|
148
|
+
pageType: context.industry,
|
|
149
|
+
industry: context.industry,
|
|
150
|
+
}),
|
|
151
|
+
cacheKey
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
return { analysis: result, tokensUsed, cached };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Analyze and generate structured data using AI
|
|
159
|
+
*/
|
|
160
|
+
async analyzeSchema(context: PageContext): Promise<{
|
|
161
|
+
analysis: AISchemaAnalysis;
|
|
162
|
+
tokensUsed: number;
|
|
163
|
+
cached: boolean;
|
|
164
|
+
}> {
|
|
165
|
+
const cacheKey = `schema:${context.url}`;
|
|
166
|
+
|
|
167
|
+
// Detect page type from URL and content
|
|
168
|
+
let pageType = 'other';
|
|
169
|
+
const urlLower = context.url.toLowerCase();
|
|
170
|
+
const contentLower = context.textContent.toLowerCase();
|
|
171
|
+
|
|
172
|
+
if (urlLower.includes('/blog/') || urlLower.includes('/article/') || urlLower.includes('/post/')) {
|
|
173
|
+
pageType = 'article';
|
|
174
|
+
} else if (urlLower.includes('/product/') || contentLower.includes('add to cart') || contentLower.includes('buy now')) {
|
|
175
|
+
pageType = 'product';
|
|
176
|
+
} else if (urlLower.includes('/faq') || contentLower.includes('frequently asked')) {
|
|
177
|
+
pageType = 'faq';
|
|
178
|
+
} else if (urlLower.includes('/about') || urlLower.includes('/team')) {
|
|
179
|
+
pageType = 'organization';
|
|
180
|
+
} else if (urlLower.includes('/contact')) {
|
|
181
|
+
pageType = 'local-business';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const { result, tokensUsed, cached } = await this.callOpenAI<AISchemaAnalysis>(
|
|
185
|
+
SCHEMA_ANALYSIS_SYSTEM,
|
|
186
|
+
SCHEMA_ANALYSIS_USER({
|
|
187
|
+
url: context.url,
|
|
188
|
+
title: context.title,
|
|
189
|
+
textContent: context.textContent,
|
|
190
|
+
existingSchema: context.schema,
|
|
191
|
+
pageType,
|
|
192
|
+
}),
|
|
193
|
+
cacheKey
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
return { analysis: result, tokensUsed, cached };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Analyze AI/LLM visibility and optimization
|
|
201
|
+
*/
|
|
202
|
+
async analyzeAIVisibility(
|
|
203
|
+
context: PageContext,
|
|
204
|
+
botAccess: { hasLlmsTxt: boolean; llmsTxtContent?: string; blockedBots: string[] }
|
|
205
|
+
): Promise<{
|
|
206
|
+
analysis: AIVisibilityAnalysis;
|
|
207
|
+
tokensUsed: number;
|
|
208
|
+
cached: boolean;
|
|
209
|
+
}> {
|
|
210
|
+
const cacheKey = `aivis:${context.url}`;
|
|
211
|
+
|
|
212
|
+
const { result, tokensUsed, cached } = await this.callOpenAI<AIVisibilityAnalysis>(
|
|
213
|
+
AI_VISIBILITY_SYSTEM,
|
|
214
|
+
AI_VISIBILITY_USER({
|
|
215
|
+
url: context.url,
|
|
216
|
+
title: context.title,
|
|
217
|
+
textContent: context.textContent,
|
|
218
|
+
headings: context.headings,
|
|
219
|
+
hasLlmsTxt: botAccess.hasLlmsTxt,
|
|
220
|
+
llmsTxtContent: botAccess.llmsTxtContent,
|
|
221
|
+
blockedBots: botAccess.blockedBots,
|
|
222
|
+
}),
|
|
223
|
+
cacheKey
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
return { analysis: result, tokensUsed, cached };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Prioritize fixes using AI
|
|
231
|
+
*/
|
|
232
|
+
async prioritizeFixes(
|
|
233
|
+
issues: { code: string; severity: string; title: string; category: string }[],
|
|
234
|
+
context: { url: string; framework?: string }
|
|
235
|
+
): Promise<{
|
|
236
|
+
analysis: AIFixPrioritization;
|
|
237
|
+
tokensUsed: number;
|
|
238
|
+
cached: boolean;
|
|
239
|
+
}> {
|
|
240
|
+
// Don't cache fix prioritization - issues change each audit
|
|
241
|
+
const { result, tokensUsed, cached } = await this.callOpenAI<AIFixPrioritization>(
|
|
242
|
+
FIX_PRIORITIZATION_SYSTEM,
|
|
243
|
+
FIX_PRIORITIZATION_USER({
|
|
244
|
+
issues,
|
|
245
|
+
url: context.url,
|
|
246
|
+
framework: context.framework,
|
|
247
|
+
})
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
return { analysis: result, tokensUsed, cached };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Run full AI analysis on a page
|
|
255
|
+
*/
|
|
256
|
+
async runFullAnalysis(
|
|
257
|
+
context: PageContext,
|
|
258
|
+
options: {
|
|
259
|
+
analyzeContent?: boolean;
|
|
260
|
+
analyzeMeta?: boolean;
|
|
261
|
+
analyzeSchema?: boolean;
|
|
262
|
+
analyzeAIVisibility?: boolean;
|
|
263
|
+
prioritizeFixes?: boolean;
|
|
264
|
+
issues?: { code: string; severity: string; title: string; category: string }[];
|
|
265
|
+
botAccess?: { hasLlmsTxt: boolean; llmsTxtContent?: string; blockedBots: string[] };
|
|
266
|
+
} = {}
|
|
267
|
+
): Promise<AIAnalysisResult> {
|
|
268
|
+
const startTime = Date.now();
|
|
269
|
+
let totalTokens = 0;
|
|
270
|
+
let anyCached = false;
|
|
271
|
+
|
|
272
|
+
const result: AIAnalysisResult = {
|
|
273
|
+
analysisTime: 0,
|
|
274
|
+
tokensUsed: 0,
|
|
275
|
+
modelUsed: this.config.model || 'gpt-4o',
|
|
276
|
+
cached: false,
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// Run analyses in parallel where possible
|
|
280
|
+
const analyses = await Promise.allSettled([
|
|
281
|
+
options.analyzeContent !== false
|
|
282
|
+
? this.analyzeContent(context)
|
|
283
|
+
: Promise.resolve(null),
|
|
284
|
+
options.analyzeMeta !== false
|
|
285
|
+
? this.analyzeMeta(context)
|
|
286
|
+
: Promise.resolve(null),
|
|
287
|
+
options.analyzeSchema !== false
|
|
288
|
+
? this.analyzeSchema(context)
|
|
289
|
+
: Promise.resolve(null),
|
|
290
|
+
options.analyzeAIVisibility !== false && options.botAccess
|
|
291
|
+
? this.analyzeAIVisibility(context, options.botAccess)
|
|
292
|
+
: Promise.resolve(null),
|
|
293
|
+
options.prioritizeFixes && options.issues
|
|
294
|
+
? this.prioritizeFixes(options.issues, { url: context.url, framework: context.framework })
|
|
295
|
+
: Promise.resolve(null),
|
|
296
|
+
]);
|
|
297
|
+
|
|
298
|
+
// Process results
|
|
299
|
+
if (analyses[0].status === 'fulfilled' && analyses[0].value) {
|
|
300
|
+
result.content = analyses[0].value.analysis;
|
|
301
|
+
totalTokens += analyses[0].value.tokensUsed;
|
|
302
|
+
if (analyses[0].value.cached) anyCached = true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (analyses[1].status === 'fulfilled' && analyses[1].value) {
|
|
306
|
+
result.meta = analyses[1].value.analysis;
|
|
307
|
+
totalTokens += analyses[1].value.tokensUsed;
|
|
308
|
+
if (analyses[1].value.cached) anyCached = true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (analyses[2].status === 'fulfilled' && analyses[2].value) {
|
|
312
|
+
result.schema = analyses[2].value.analysis;
|
|
313
|
+
totalTokens += analyses[2].value.tokensUsed;
|
|
314
|
+
if (analyses[2].value.cached) anyCached = true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (analyses[3].status === 'fulfilled' && analyses[3].value) {
|
|
318
|
+
result.aiVisibility = analyses[3].value.analysis;
|
|
319
|
+
totalTokens += analyses[3].value.tokensUsed;
|
|
320
|
+
if (analyses[3].value.cached) anyCached = true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (analyses[4].status === 'fulfilled' && analyses[4].value) {
|
|
324
|
+
result.fixPrioritization = analyses[4].value.analysis;
|
|
325
|
+
totalTokens += analyses[4].value.tokensUsed;
|
|
326
|
+
if (analyses[4].value.cached) anyCached = true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
result.analysisTime = Date.now() - startTime;
|
|
330
|
+
result.tokensUsed = totalTokens;
|
|
331
|
+
result.cached = anyCached;
|
|
332
|
+
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Clear the cache
|
|
338
|
+
*/
|
|
339
|
+
clearCache(): void {
|
|
340
|
+
this.cache.clear();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Export a factory function for creating analyzers
|
|
345
|
+
export function createAIAnalyzer(config: AIConfig): AIAnalyzer {
|
|
346
|
+
return new AIAnalyzer(config);
|
|
347
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// AI-Powered SEO Analysis Module
|
|
2
|
+
// Exports for use in audits and Edge Functions
|
|
3
|
+
|
|
4
|
+
export { AIAnalyzer, createAIAnalyzer } from './analyzer.js';
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
AIConfig,
|
|
8
|
+
AIContentAnalysis,
|
|
9
|
+
AIMetaAnalysis,
|
|
10
|
+
AISchemaAnalysis,
|
|
11
|
+
AILinkAnalysis,
|
|
12
|
+
AIVisibilityAnalysis,
|
|
13
|
+
AIFixPrioritization,
|
|
14
|
+
AIAnalysisResult,
|
|
15
|
+
PageContext,
|
|
16
|
+
} from './types.js';
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
CONTENT_ANALYSIS_SYSTEM,
|
|
20
|
+
CONTENT_ANALYSIS_USER,
|
|
21
|
+
META_ANALYSIS_SYSTEM,
|
|
22
|
+
META_ANALYSIS_USER,
|
|
23
|
+
SCHEMA_ANALYSIS_SYSTEM,
|
|
24
|
+
SCHEMA_ANALYSIS_USER,
|
|
25
|
+
AI_VISIBILITY_SYSTEM,
|
|
26
|
+
AI_VISIBILITY_USER,
|
|
27
|
+
FIX_PRIORITIZATION_SYSTEM,
|
|
28
|
+
FIX_PRIORITIZATION_USER,
|
|
29
|
+
} from './prompts/content-analysis.js';
|