@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
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import { httpGet } from '../utils/http.js';
|
|
2
|
+
import type { AuditIssue, AuditReport, HealthScore, PageAudit } from './types.js';
|
|
3
|
+
import { runCrawlabilityChecks } from './checks/crawlability.js';
|
|
4
|
+
import { analyzeOnPage } from './checks/on-page.js';
|
|
5
|
+
import { analyzeLinks } from './checks/links.js';
|
|
6
|
+
import { analyzeImages } from './checks/images.js';
|
|
7
|
+
import { analyzePerformance, fetchCoreWebVitals } from './checks/performance.js';
|
|
8
|
+
import { analyzeSecurity, checkCertificate } from './checks/security.js';
|
|
9
|
+
import { analyzeStructuredData, suggestSchemaTypes } from './checks/structured-data.js';
|
|
10
|
+
import { analyzeMobile, checkMobileResources } from './checks/mobile.js';
|
|
11
|
+
// Advanced checks
|
|
12
|
+
import { analyzeHreflang } from './checks/hreflang.js';
|
|
13
|
+
import { analyzeSocialMeta } from './checks/social-meta.js';
|
|
14
|
+
import { detectSoft404 } from './checks/soft-404.js';
|
|
15
|
+
import { analyzeAnchorText } from './checks/anchor-text.js';
|
|
16
|
+
import { analyzeCanonicalAdvanced } from './checks/canonical-advanced.js';
|
|
17
|
+
import { analyzePagination } from './checks/pagination.js';
|
|
18
|
+
import { analyzeRedirects } from './checks/redirect-analysis.js';
|
|
19
|
+
// New competitor-parity checks
|
|
20
|
+
import { runAIReadinessChecks } from './checks/ai-readiness.js';
|
|
21
|
+
import { analyzeSERPPreview } from './checks/serp-preview.js';
|
|
22
|
+
import { analyzeLocalSEO } from './checks/local-seo.js';
|
|
23
|
+
import { analyzeSecurityHeaders } from './checks/security-headers.js';
|
|
24
|
+
import { analyzeContentFreshness } from './checks/content-freshness.js';
|
|
25
|
+
import { analyzeDOMStructure } from './checks/dom-analysis.js';
|
|
26
|
+
import { analyzeModernImages } from './checks/modern-images.js';
|
|
27
|
+
import { detectTechnologies } from './checks/tech-detection.js';
|
|
28
|
+
import { analyzeKeywords } from './checks/keyword-analysis.js';
|
|
29
|
+
import { runAdditionalChecks } from './checks/additional-checks.js';
|
|
30
|
+
// Advanced checks from SEO research
|
|
31
|
+
import { analyzeFeaturedSnippet } from './checks/featured-snippet.js';
|
|
32
|
+
import { analyzeInternalLinkGraph } from './checks/internal-link-graph.js';
|
|
33
|
+
import { analyzeResourceHints } from './checks/resource-hints.js';
|
|
34
|
+
import { analyzeEEATSignals } from './checks/eeat-signals.js';
|
|
35
|
+
import { analyzeIndexNow } from './checks/indexnow.js';
|
|
36
|
+
import { analyzeContentScience } from './checks/content-science.js';
|
|
37
|
+
import { analyzeSiteMaturity } from './checks/site-maturity.js';
|
|
38
|
+
import { analyzeKeywordCannibalizationSinglePage } from './checks/keyword-cannibalization.js';
|
|
39
|
+
// New checks from Technical SEO for Developers & Best SEO Strategies 2025
|
|
40
|
+
import { analyzeTrackerBloat } from './checks/tracker-bloat.js';
|
|
41
|
+
import { analyzeClientRendering } from './checks/client-rendering.js';
|
|
42
|
+
import { analyzeRedirectChain } from './checks/redirect-chain.js';
|
|
43
|
+
import { analyzeResponsiveImages } from './checks/responsive-images.js';
|
|
44
|
+
// New checks from 2026 SEO transcripts (Nathan Gotch & Neil Patel)
|
|
45
|
+
import { analyzeConversionElements } from './checks/conversion-elements.js';
|
|
46
|
+
import { analyzeKeywordPlacement } from './checks/keyword-placement.js';
|
|
47
|
+
import { analyzeTopicalClusters } from './checks/topical-clusters.js';
|
|
48
|
+
import { analyzePlatformPresence } from './checks/platform-presence.js';
|
|
49
|
+
// Advanced SEO Tips 2026 & React SEO
|
|
50
|
+
import { analyzeInteractiveTools } from './checks/interactive-tools.js';
|
|
51
|
+
import { analyzeFunnelIntent } from './checks/funnel-intent.js';
|
|
52
|
+
// Cutting-edge differentiating checks (Google leak, Entity SEO, QDF)
|
|
53
|
+
import { analyzeNavBoostSignals } from './checks/navboost-signals.js';
|
|
54
|
+
import { analyzeEntitySEO } from './checks/entity-seo.js';
|
|
55
|
+
import { analyzeFreshnessSignals } from './checks/freshness-signals.js';
|
|
56
|
+
// AI Search Optimization checks (2026 - ChatGPT, AI Overviews)
|
|
57
|
+
import { analyzeBingOptimization } from './checks/bing-optimization.js';
|
|
58
|
+
import { analyzeAIContentStructure } from './checks/ai-content-structure.js';
|
|
59
|
+
import { analyzeCitationQuality } from './checks/citation-quality.js';
|
|
60
|
+
import { analyzeAnswerConciseness } from './checks/answer-conciseness.js';
|
|
61
|
+
// AI SEO Skills checks (brand narrative, citation worthiness, review ecosystem)
|
|
62
|
+
import { analyzeBrandMentionOptimization } from './checks/brand-mention-optimization.js';
|
|
63
|
+
import { analyzeAICitationWorthiness } from './checks/ai-citation-worthiness.js';
|
|
64
|
+
import { analyzeReviewEcosystem } from './checks/review-ecosystem.js';
|
|
65
|
+
// Competitor-parity checks (from missing-checks.md analysis)
|
|
66
|
+
import { validateRobotsTxt } from './checks/robots-validation.js';
|
|
67
|
+
import { analyzeLlmsTxt } from './checks/llms-txt.js';
|
|
68
|
+
import { analyzeHtmlCompliance } from './checks/html-compliance.js';
|
|
69
|
+
import { analyzeCanonicalDomain } from './checks/canonical-domain.js';
|
|
70
|
+
import { analyzeCachingHeaders } from './checks/caching-headers.js';
|
|
71
|
+
import { analyzeDomSize } from './checks/dom-size.js';
|
|
72
|
+
import { analyzeImageDimensions } from './checks/image-dimensions.js';
|
|
73
|
+
import { analyzeColorContrast } from './checks/color-contrast.js';
|
|
74
|
+
// New checks from Rank Math analysis
|
|
75
|
+
import { analyzeAssetMinification } from './checks/asset-minification.js';
|
|
76
|
+
import { analyzePageResources } from './checks/page-resources.js';
|
|
77
|
+
import { analyzeResponsiveCss } from './checks/responsive-css.js';
|
|
78
|
+
import { analyzeDirectoryListing } from './checks/directory-listing.js';
|
|
79
|
+
import { analyzeUrlSafety } from './checks/url-safety.js';
|
|
80
|
+
import { analyzeTrackingVerification } from './checks/tracking-verification.js';
|
|
81
|
+
|
|
82
|
+
export interface AuditOptions {
|
|
83
|
+
url: string;
|
|
84
|
+
checkBrokenLinks?: boolean;
|
|
85
|
+
checkHreflangUrls?: boolean;
|
|
86
|
+
checkCanonicalChain?: boolean;
|
|
87
|
+
maxPages?: number;
|
|
88
|
+
cruxApiKey?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Maximum number of checks to run. Used for tier enforcement:
|
|
91
|
+
* - 50: Free unregistered users (core checks only)
|
|
92
|
+
* - 100: Free registered users (core + some premium checks)
|
|
93
|
+
* - 280+: Paid users (all checks)
|
|
94
|
+
*/
|
|
95
|
+
checksLimit?: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function runFullAudit(options: AuditOptions): Promise<AuditReport> {
|
|
99
|
+
const { url, checkBrokenLinks = false, checkHreflangUrls = false, checkCanonicalChain = false, cruxApiKey, checksLimit = 280 } = options;
|
|
100
|
+
const allIssues: AuditIssue[] = [];
|
|
101
|
+
const pages: PageAudit[] = [];
|
|
102
|
+
|
|
103
|
+
// Determine tier based on checksLimit
|
|
104
|
+
// Free unregistered: 50, Free registered: 100, Paid: 280+
|
|
105
|
+
const tier = checksLimit <= 50 ? 'free' : checksLimit <= 100 ? 'registered' : 'paid';
|
|
106
|
+
const runPremiumChecks = tier === 'paid';
|
|
107
|
+
const runExtendedChecks = tier !== 'free'; // registered + paid
|
|
108
|
+
|
|
109
|
+
const parsedUrl = new URL(url);
|
|
110
|
+
const domain = parsedUrl.hostname;
|
|
111
|
+
|
|
112
|
+
console.log(`\nđ Running comprehensive SEO audit on ${url}...\n`);
|
|
113
|
+
|
|
114
|
+
// ========== PHASE 1: CRAWLABILITY + FETCH (PARALLEL) ==========
|
|
115
|
+
console.log('đ Phase 1: Crawlability checks + page fetch (parallel)...');
|
|
116
|
+
|
|
117
|
+
const [crawlabilityResult, fetchResult] = await Promise.all([
|
|
118
|
+
runCrawlabilityChecks(url).catch(err => {
|
|
119
|
+
console.error('Crawlability check failed:', err);
|
|
120
|
+
return [] as AuditIssue[];
|
|
121
|
+
}),
|
|
122
|
+
httpGet<string>(url, {
|
|
123
|
+
timeout: 30000,
|
|
124
|
+
validateStatus: () => true,
|
|
125
|
+
}).catch(err => {
|
|
126
|
+
return { error: err, data: '', headers: {} as Record<string, string> };
|
|
127
|
+
}),
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
allIssues.push(...crawlabilityResult);
|
|
131
|
+
|
|
132
|
+
// Check if fetch failed
|
|
133
|
+
if ('error' in fetchResult) {
|
|
134
|
+
allIssues.push({
|
|
135
|
+
code: 'FETCH_ERROR',
|
|
136
|
+
severity: 'error',
|
|
137
|
+
category: 'crawlability',
|
|
138
|
+
title: 'Failed to fetch page',
|
|
139
|
+
description: `Could not fetch the page: ${fetchResult.error instanceof Error ? fetchResult.error.message : 'Unknown error'}`,
|
|
140
|
+
impact: 'Cannot perform full audit without page content.',
|
|
141
|
+
howToFix: 'Ensure the URL is accessible and the server is responding.',
|
|
142
|
+
affectedUrls: [url],
|
|
143
|
+
});
|
|
144
|
+
return createReport(url, domain, allIssues, pages);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const html = fetchResult.data;
|
|
148
|
+
const headers = fetchResult.headers;
|
|
149
|
+
|
|
150
|
+
// ========== PHASE 2: SYNCHRONOUS HTML CHECKS (PARALLEL) ==========
|
|
151
|
+
console.log(`đ Phase 2: Running synchronous HTML checks (tier: ${tier}, limit: ${checksLimit})...`);
|
|
152
|
+
|
|
153
|
+
// ===== CORE CHECKS (all tiers) =====
|
|
154
|
+
const onPageResult = analyzeOnPage(html, url);
|
|
155
|
+
const structuredDataResult = analyzeStructuredData(html, url);
|
|
156
|
+
const mobileResult = analyzeMobile(html, url);
|
|
157
|
+
const mobileResourceIssues = checkMobileResources(html, url);
|
|
158
|
+
const socialResult = analyzeSocialMeta(html, url);
|
|
159
|
+
const soft404Result = detectSoft404(html, url, 200);
|
|
160
|
+
const serpResult = analyzeSERPPreview(html, url);
|
|
161
|
+
const domResult = analyzeDOMStructure(html, url);
|
|
162
|
+
const techResult = detectTechnologies(html, url, headers);
|
|
163
|
+
const keywordResult = analyzeKeywords(html, url);
|
|
164
|
+
// Pagination depends on onPageData.canonical
|
|
165
|
+
const paginationResult = analyzePagination(html, url, onPageResult.data.canonical);
|
|
166
|
+
// URL Safety check (local hash database - like Google Safe Browsing)
|
|
167
|
+
const urlSafetyResult = analyzeUrlSafety(url);
|
|
168
|
+
// Competitor-parity checks (sync) - important for all users
|
|
169
|
+
const domSizeResult = analyzeDomSize(html, url);
|
|
170
|
+
const imageDimensionsResult = analyzeImageDimensions(html, url);
|
|
171
|
+
// Tracking & verification checks (GA4, GSC, Bing, Schema.org)
|
|
172
|
+
const trackingResult = analyzeTrackingVerification(html, url);
|
|
173
|
+
|
|
174
|
+
// ===== EXTENDED CHECKS (registered + paid tiers) =====
|
|
175
|
+
const anchorResult = runExtendedChecks ? analyzeAnchorText(html, url) : { issues: [], data: {} };
|
|
176
|
+
const localSeoResult = runExtendedChecks ? analyzeLocalSEO(html, url) : { issues: [], data: {} };
|
|
177
|
+
const modernImageResult = runExtendedChecks ? analyzeModernImages(html, url) : { issues: [], data: {} };
|
|
178
|
+
const resourceHintResult = runExtendedChecks ? analyzeResourceHints(html, url) : { issues: [], data: {} };
|
|
179
|
+
const trackerResult = runExtendedChecks ? analyzeTrackerBloat(html, url) : { issues: [], data: {} };
|
|
180
|
+
const csrResult = runExtendedChecks ? analyzeClientRendering(html, url) : { issues: [], data: {} };
|
|
181
|
+
const responsiveImgResult = runExtendedChecks ? analyzeResponsiveImages(html, url) : { issues: [], data: {} };
|
|
182
|
+
const colorContrastResult = runExtendedChecks ? analyzeColorContrast(html, url) : { issues: [], data: {} };
|
|
183
|
+
const pageResourcesResult = runExtendedChecks ? analyzePageResources(html, url) : { issues: [], data: {} };
|
|
184
|
+
|
|
185
|
+
// ===== PREMIUM CHECKS (paid tier only) =====
|
|
186
|
+
const snippetResult = runPremiumChecks ? analyzeFeaturedSnippet(html, url) : { issues: [], data: {} };
|
|
187
|
+
const linkGraphResult = runPremiumChecks ? analyzeInternalLinkGraph(html, url) : { issues: [], data: {} };
|
|
188
|
+
const eeatResult = runPremiumChecks ? analyzeEEATSignals(html, url) : { issues: [], data: {} };
|
|
189
|
+
const contentScienceResult = runPremiumChecks ? analyzeContentScience(html, url) : { issues: [], data: {} };
|
|
190
|
+
const cannibalizationResult = runPremiumChecks ? analyzeKeywordCannibalizationSinglePage(html, url) : { issues: [], data: {} };
|
|
191
|
+
const conversionResult = runPremiumChecks ? analyzeConversionElements(html, url) : { issues: [], data: {} };
|
|
192
|
+
const keywordPlacementResult = runPremiumChecks ? analyzeKeywordPlacement(html, url) : { issues: [], data: {} };
|
|
193
|
+
const clusterResult = runPremiumChecks ? analyzeTopicalClusters(html, url) : { issues: [], data: {} };
|
|
194
|
+
const platformResult = runPremiumChecks ? analyzePlatformPresence(html, url) : { issues: [], data: {} };
|
|
195
|
+
const toolResult = runPremiumChecks ? analyzeInteractiveTools(html, url) : { issues: [], data: {} };
|
|
196
|
+
const funnelResult = runPremiumChecks ? analyzeFunnelIntent(html, url) : { issues: [], data: {} };
|
|
197
|
+
const navboostResult = runPremiumChecks ? analyzeNavBoostSignals(html, url) : { issues: [], data: {} };
|
|
198
|
+
const entityResult = runPremiumChecks ? analyzeEntitySEO(html, url) : { issues: [], data: {} };
|
|
199
|
+
const qdfFreshnessResult = runPremiumChecks ? analyzeFreshnessSignals(html, url) : { issues: [], data: {} };
|
|
200
|
+
// AI Search Optimization (2026) - Premium only
|
|
201
|
+
const aiContentStructureResult = runPremiumChecks ? analyzeAIContentStructure(html, url) : { issues: [], data: {} };
|
|
202
|
+
const citationQualityResult = runPremiumChecks ? analyzeCitationQuality(html, url) : { issues: [], data: {} };
|
|
203
|
+
const answerConcisenessResult = runPremiumChecks ? analyzeAnswerConciseness(html, url) : { issues: [], data: {} };
|
|
204
|
+
// AI SEO Skills - Premium only
|
|
205
|
+
const brandMentionResult = runPremiumChecks ? analyzeBrandMentionOptimization(html, url) : { issues: [], data: {} };
|
|
206
|
+
const citationWorthinessResult = runPremiumChecks ? analyzeAICitationWorthiness(html, url) : { issues: [], data: {} };
|
|
207
|
+
const reviewEcosystemResult = runPremiumChecks ? analyzeReviewEcosystem(html, url) : { issues: [], data: {} };
|
|
208
|
+
|
|
209
|
+
// Collect all sync check issues
|
|
210
|
+
allIssues.push(
|
|
211
|
+
...onPageResult.issues,
|
|
212
|
+
...structuredDataResult.issues,
|
|
213
|
+
...mobileResult.issues,
|
|
214
|
+
...mobileResourceIssues,
|
|
215
|
+
...socialResult.issues,
|
|
216
|
+
...soft404Result.issues,
|
|
217
|
+
...anchorResult.issues,
|
|
218
|
+
...serpResult.issues,
|
|
219
|
+
...localSeoResult.issues,
|
|
220
|
+
...domResult.issues,
|
|
221
|
+
...modernImageResult.issues,
|
|
222
|
+
...techResult.issues,
|
|
223
|
+
...keywordResult.issues,
|
|
224
|
+
...snippetResult.issues,
|
|
225
|
+
...linkGraphResult.issues,
|
|
226
|
+
...resourceHintResult.issues,
|
|
227
|
+
...eeatResult.issues,
|
|
228
|
+
...contentScienceResult.issues,
|
|
229
|
+
...cannibalizationResult.issues,
|
|
230
|
+
...trackerResult.issues,
|
|
231
|
+
...csrResult.issues,
|
|
232
|
+
...responsiveImgResult.issues,
|
|
233
|
+
...conversionResult.issues,
|
|
234
|
+
...keywordPlacementResult.issues,
|
|
235
|
+
...clusterResult.issues,
|
|
236
|
+
...platformResult.issues,
|
|
237
|
+
...toolResult.issues,
|
|
238
|
+
...funnelResult.issues,
|
|
239
|
+
...navboostResult.issues,
|
|
240
|
+
...entityResult.issues,
|
|
241
|
+
...qdfFreshnessResult.issues,
|
|
242
|
+
...aiContentStructureResult.issues,
|
|
243
|
+
...citationQualityResult.issues,
|
|
244
|
+
...answerConcisenessResult.issues,
|
|
245
|
+
...brandMentionResult.issues,
|
|
246
|
+
...citationWorthinessResult.issues,
|
|
247
|
+
...reviewEcosystemResult.issues,
|
|
248
|
+
...domSizeResult.issues,
|
|
249
|
+
...imageDimensionsResult.issues,
|
|
250
|
+
...colorContrastResult.issues,
|
|
251
|
+
...pageResourcesResult.issues,
|
|
252
|
+
...urlSafetyResult.issues,
|
|
253
|
+
...paginationResult.issues,
|
|
254
|
+
...trackingResult.issues,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// Suggest schema types if none found
|
|
258
|
+
if (!structuredDataResult.data.hasSchema) {
|
|
259
|
+
const suggestions = suggestSchemaTypes(html, url);
|
|
260
|
+
if (suggestions.length > 0 && structuredDataResult.issues.length > 0) {
|
|
261
|
+
const lastSchemaIssue = structuredDataResult.issues[structuredDataResult.issues.length - 1];
|
|
262
|
+
lastSchemaIssue.details = {
|
|
263
|
+
...lastSchemaIssue.details,
|
|
264
|
+
suggestedTypes: suggestions,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ========== PHASE 3: ASYNC CHECKS (PARALLEL) ==========
|
|
270
|
+
console.log('đ Phase 3: Running async checks (parallel)...');
|
|
271
|
+
|
|
272
|
+
// Helper to safely run async checks with error handling and timeout
|
|
273
|
+
const safeAsync = async <T>(
|
|
274
|
+
name: string,
|
|
275
|
+
fn: () => Promise<{ issues: AuditIssue[]; data: T }>,
|
|
276
|
+
timeoutMs: number = 10000, // 10 second default timeout per check
|
|
277
|
+
): Promise<AuditIssue[]> => {
|
|
278
|
+
try {
|
|
279
|
+
const resultPromise = fn();
|
|
280
|
+
const timeoutPromise = new Promise<never>((_, reject) =>
|
|
281
|
+
setTimeout(() => reject(new Error(`${name} timed out`)), timeoutMs)
|
|
282
|
+
);
|
|
283
|
+
const result = await Promise.race([resultPromise, timeoutPromise]);
|
|
284
|
+
return result.issues;
|
|
285
|
+
} catch (err) {
|
|
286
|
+
console.error(`${name} failed:`, err);
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// Helper for async checks that only return issues
|
|
292
|
+
const safeAsyncIssues = async (
|
|
293
|
+
name: string,
|
|
294
|
+
fn: () => Promise<AuditIssue[]>,
|
|
295
|
+
timeoutMs: number = 10000,
|
|
296
|
+
): Promise<AuditIssue[]> => {
|
|
297
|
+
try {
|
|
298
|
+
const resultPromise = fn();
|
|
299
|
+
const timeoutPromise = new Promise<never>((_, reject) =>
|
|
300
|
+
setTimeout(() => reject(new Error(`${name} timed out`)), timeoutMs)
|
|
301
|
+
);
|
|
302
|
+
return await Promise.race([resultPromise, timeoutPromise]);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
console.error(`${name} failed:`, err);
|
|
305
|
+
return [];
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// Run performance separately to capture data for page audit
|
|
310
|
+
let perfData = { loadTime: 0 };
|
|
311
|
+
const performancePromise = analyzePerformance(url)
|
|
312
|
+
.then(result => {
|
|
313
|
+
perfData = result.data;
|
|
314
|
+
return result.issues;
|
|
315
|
+
})
|
|
316
|
+
.catch(err => {
|
|
317
|
+
console.error('Performance check failed:', err);
|
|
318
|
+
return [] as AuditIssue[];
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Run all async checks in parallel (respecting tier limits)
|
|
322
|
+
const asyncChecks: Promise<AuditIssue[]>[] = [];
|
|
323
|
+
|
|
324
|
+
// ===== CORE ASYNC CHECKS (all tiers) =====
|
|
325
|
+
asyncChecks.push(safeAsync('Links', () => analyzeLinks(html, url, checkBrokenLinks)));
|
|
326
|
+
asyncChecks.push(safeAsync('Images', () => analyzeImages(html, url, checkBrokenLinks)));
|
|
327
|
+
asyncChecks.push(performancePromise);
|
|
328
|
+
asyncChecks.push(safeAsync('Security', () => analyzeSecurity(html, url, headers)));
|
|
329
|
+
asyncChecks.push(safeAsyncIssues('Certificate', async () => (await checkCertificate(url)).issues));
|
|
330
|
+
asyncChecks.push(safeAsync('Redirects', () => analyzeRedirects(url)));
|
|
331
|
+
asyncChecks.push(safeAsync('Robots Validation', () => validateRobotsTxt(url)));
|
|
332
|
+
asyncChecks.push(safeAsync('Canonical Domain', () => analyzeCanonicalDomain(url)));
|
|
333
|
+
|
|
334
|
+
// ===== EXTENDED ASYNC CHECKS (registered + paid tiers) =====
|
|
335
|
+
if (runExtendedChecks) {
|
|
336
|
+
asyncChecks.push(safeAsync('Hreflang', () => analyzeHreflang(html, url, { validateUrls: checkHreflangUrls })));
|
|
337
|
+
asyncChecks.push(safeAsync('Canonical Advanced', () => analyzeCanonicalAdvanced(html, url, { checkChain: checkCanonicalChain })));
|
|
338
|
+
asyncChecks.push(safeAsync('Security Headers', () => analyzeSecurityHeaders(url)));
|
|
339
|
+
asyncChecks.push(safeAsync('Caching Headers', () => analyzeCachingHeaders(url, headers)));
|
|
340
|
+
asyncChecks.push(safeAsync('HTML Compliance', () => analyzeHtmlCompliance(html, url, headers)));
|
|
341
|
+
asyncChecks.push(safeAsync('Redirect Chain', () => analyzeRedirectChain(url)));
|
|
342
|
+
asyncChecks.push(safeAsync('Asset Minification', () => analyzeAssetMinification(html, url)));
|
|
343
|
+
asyncChecks.push(safeAsync('Responsive CSS', () => analyzeResponsiveCss(html, url)));
|
|
344
|
+
asyncChecks.push(safeAsync('Directory Listing', () => analyzeDirectoryListing(url)));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ===== PREMIUM ASYNC CHECKS (paid tier only) =====
|
|
348
|
+
if (runPremiumChecks) {
|
|
349
|
+
asyncChecks.push(safeAsync('AI Readiness', () => runAIReadinessChecks(url, html)));
|
|
350
|
+
asyncChecks.push(safeAsync('Content Freshness', () => analyzeContentFreshness(url, html)));
|
|
351
|
+
asyncChecks.push(safeAsync('Additional Checks', () => runAdditionalChecks(url, html)));
|
|
352
|
+
asyncChecks.push(safeAsync('IndexNow', () => analyzeIndexNow(html, url, headers)));
|
|
353
|
+
asyncChecks.push(safeAsync('Site Maturity', () => analyzeSiteMaturity(html, url)));
|
|
354
|
+
asyncChecks.push(safeAsync('Bing Optimization', () => analyzeBingOptimization(html, url)));
|
|
355
|
+
asyncChecks.push(safeAsync('LLMs.txt', () => analyzeLlmsTxt(url)));
|
|
356
|
+
// Core Web Vitals (optional, premium only)
|
|
357
|
+
if (cruxApiKey) {
|
|
358
|
+
asyncChecks.push(safeAsyncIssues('Core Web Vitals', async () => {
|
|
359
|
+
const cwv = await fetchCoreWebVitals(url, cruxApiKey);
|
|
360
|
+
const issues: AuditIssue[] = [];
|
|
361
|
+
if (cwv) {
|
|
362
|
+
if (cwv.lcp?.rating === 'poor') {
|
|
363
|
+
issues.push({
|
|
364
|
+
code: 'LCP_POOR',
|
|
365
|
+
severity: 'error',
|
|
366
|
+
category: 'performance',
|
|
367
|
+
title: 'Poor Largest Contentful Paint (LCP)',
|
|
368
|
+
description: `LCP is ${cwv.lcp.value}ms (should be under 2500ms)`,
|
|
369
|
+
impact: 'Direct Core Web Vitals ranking factor.',
|
|
370
|
+
howToFix: 'Optimize hero images, preload critical resources, improve server response time.',
|
|
371
|
+
affectedUrls: [url],
|
|
372
|
+
details: { lcp: cwv.lcp },
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
if (cwv.cls?.rating === 'poor') {
|
|
376
|
+
issues.push({
|
|
377
|
+
code: 'CLS_POOR',
|
|
378
|
+
severity: 'error',
|
|
379
|
+
category: 'performance',
|
|
380
|
+
title: 'Poor Cumulative Layout Shift (CLS)',
|
|
381
|
+
description: `CLS is ${cwv.cls.value} (should be under 0.1)`,
|
|
382
|
+
impact: 'Direct Core Web Vitals ranking factor.',
|
|
383
|
+
howToFix: 'Set image dimensions, avoid inserting content above existing content.',
|
|
384
|
+
affectedUrls: [url],
|
|
385
|
+
details: { cls: cwv.cls },
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return issues;
|
|
390
|
+
}));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const asyncResults = await Promise.all(asyncChecks);
|
|
395
|
+
|
|
396
|
+
// Collect all async issues
|
|
397
|
+
for (const issues of asyncResults) {
|
|
398
|
+
allIssues.push(...issues);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ========== CREATE PAGE AUDIT ==========
|
|
402
|
+
pages.push({
|
|
403
|
+
url,
|
|
404
|
+
statusCode: 200,
|
|
405
|
+
title: onPageResult.data.title,
|
|
406
|
+
description: onPageResult.data.description,
|
|
407
|
+
canonical: onPageResult.data.canonical,
|
|
408
|
+
h1: onPageResult.data.h1s,
|
|
409
|
+
wordCount: onPageResult.data.wordCount,
|
|
410
|
+
loadTime: perfData.loadTime,
|
|
411
|
+
issues: allIssues.map(i => i.code),
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
console.log('\nâ
Audit complete!\n');
|
|
415
|
+
|
|
416
|
+
return createReport(url, domain, allIssues, pages);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function createReport(url: string, domain: string, issues: AuditIssue[], pages: PageAudit[]): AuditReport {
|
|
420
|
+
// Calculate health score
|
|
421
|
+
const healthScore = calculateHealthScore(issues);
|
|
422
|
+
|
|
423
|
+
// Count issues by severity
|
|
424
|
+
const errors = issues.filter(i => i.severity === 'error').length;
|
|
425
|
+
const warnings = issues.filter(i => i.severity === 'warning').length;
|
|
426
|
+
const notices = issues.filter(i => i.severity === 'notice').length;
|
|
427
|
+
|
|
428
|
+
// Calculate passed checks (rough estimate based on possible checks)
|
|
429
|
+
const totalPossibleChecks = 310; // Approximate number of checks (expanded with Rank Math parity + URL safety checks)
|
|
430
|
+
const passed = Math.max(0, totalPossibleChecks - errors - warnings);
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
url,
|
|
434
|
+
domain,
|
|
435
|
+
timestamp: new Date().toISOString(),
|
|
436
|
+
crawlStats: {
|
|
437
|
+
totalUrls: 1,
|
|
438
|
+
crawledUrls: 1,
|
|
439
|
+
errorUrls: 0,
|
|
440
|
+
redirectUrls: 0,
|
|
441
|
+
blockedUrls: 0,
|
|
442
|
+
},
|
|
443
|
+
healthScore,
|
|
444
|
+
issues,
|
|
445
|
+
pages,
|
|
446
|
+
summary: {
|
|
447
|
+
errors,
|
|
448
|
+
warnings,
|
|
449
|
+
notices,
|
|
450
|
+
passed,
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function calculateHealthScore(issues: AuditIssue[]): HealthScore {
|
|
456
|
+
// Weight issues by severity
|
|
457
|
+
const weights = { error: 10, warning: 3, notice: 1 };
|
|
458
|
+
|
|
459
|
+
// Calculate deductions per category
|
|
460
|
+
const categoryDeductions: Record<string, number> = {
|
|
461
|
+
crawlability: 0,
|
|
462
|
+
indexability: 0,
|
|
463
|
+
'on-page': 0,
|
|
464
|
+
content: 0,
|
|
465
|
+
links: 0,
|
|
466
|
+
images: 0,
|
|
467
|
+
'structured-data': 0,
|
|
468
|
+
performance: 0,
|
|
469
|
+
security: 0,
|
|
470
|
+
mobile: 0,
|
|
471
|
+
international: 0,
|
|
472
|
+
'ai-readiness': 0,
|
|
473
|
+
'social': 0,
|
|
474
|
+
'local-seo': 0,
|
|
475
|
+
'accessibility': 0,
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
for (const issue of issues) {
|
|
479
|
+
const deduction = weights[issue.severity];
|
|
480
|
+
categoryDeductions[issue.category] = (categoryDeductions[issue.category] || 0) + deduction;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Calculate scores (100 - deductions, min 0)
|
|
484
|
+
const crawlability = Math.max(0, 100 - categoryDeductions.crawlability * 5);
|
|
485
|
+
const indexability = Math.max(0, 100 - categoryDeductions.indexability * 5);
|
|
486
|
+
const onPage = Math.max(0, 100 - categoryDeductions['on-page'] * 3);
|
|
487
|
+
const content = Math.max(0, 100 - categoryDeductions.content * 3);
|
|
488
|
+
const links = Math.max(0, 100 - categoryDeductions.links * 3);
|
|
489
|
+
const performance = Math.max(0, 100 - categoryDeductions.performance * 4);
|
|
490
|
+
const security = Math.max(0, 100 - categoryDeductions.security * 5);
|
|
491
|
+
const aiReadiness = Math.max(0, 100 - categoryDeductions['ai-readiness'] * 3);
|
|
492
|
+
const social = Math.max(0, 100 - categoryDeductions.social * 2);
|
|
493
|
+
const localSeo = Math.max(0, 100 - categoryDeductions['local-seo'] * 2);
|
|
494
|
+
const accessibility = Math.max(0, 100 - categoryDeductions.accessibility * 4);
|
|
495
|
+
|
|
496
|
+
// Overall score is weighted average
|
|
497
|
+
const overall = Math.round(
|
|
498
|
+
(crawlability * 0.11 +
|
|
499
|
+
indexability * 0.11 +
|
|
500
|
+
onPage * 0.16 +
|
|
501
|
+
content * 0.07 +
|
|
502
|
+
links * 0.07 +
|
|
503
|
+
performance * 0.11 +
|
|
504
|
+
security * 0.11 +
|
|
505
|
+
aiReadiness * 0.06 +
|
|
506
|
+
social * 0.05 +
|
|
507
|
+
localSeo * 0.05 +
|
|
508
|
+
accessibility * 0.10)
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
overall,
|
|
513
|
+
crawlability,
|
|
514
|
+
indexability,
|
|
515
|
+
onPage,
|
|
516
|
+
content,
|
|
517
|
+
links,
|
|
518
|
+
performance,
|
|
519
|
+
security,
|
|
520
|
+
aiReadiness,
|
|
521
|
+
social,
|
|
522
|
+
localSeo,
|
|
523
|
+
accessibility,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Format the report for console output
|
|
528
|
+
export function formatReport(report: AuditReport): string {
|
|
529
|
+
const lines: string[] = [];
|
|
530
|
+
|
|
531
|
+
lines.push('');
|
|
532
|
+
lines.push('â'.repeat(70));
|
|
533
|
+
lines.push(' SEO AUDIT REPORT');
|
|
534
|
+
lines.push('â'.repeat(70));
|
|
535
|
+
lines.push(` URL: ${report.url}`);
|
|
536
|
+
lines.push(` Domain: ${report.domain}`);
|
|
537
|
+
lines.push(` Time: ${report.timestamp}`);
|
|
538
|
+
lines.push('â'.repeat(70));
|
|
539
|
+
lines.push('');
|
|
540
|
+
|
|
541
|
+
// Health Score
|
|
542
|
+
lines.push('đ HEALTH SCORE');
|
|
543
|
+
lines.push('â'.repeat(70));
|
|
544
|
+
lines.push(` Overall: ${report.healthScore.overall}/100 ${getScoreEmoji(report.healthScore.overall)}`);
|
|
545
|
+
lines.push('');
|
|
546
|
+
lines.push(' Category Scores:');
|
|
547
|
+
lines.push(` Crawlability: ${report.healthScore.crawlability}/100`);
|
|
548
|
+
lines.push(` Indexability: ${report.healthScore.indexability}/100`);
|
|
549
|
+
lines.push(` On-Page SEO: ${report.healthScore.onPage}/100`);
|
|
550
|
+
lines.push(` Content: ${report.healthScore.content}/100`);
|
|
551
|
+
lines.push(` Links: ${report.healthScore.links}/100`);
|
|
552
|
+
lines.push(` Performance: ${report.healthScore.performance}/100`);
|
|
553
|
+
lines.push(` Security: ${report.healthScore.security}/100`);
|
|
554
|
+
lines.push(` AI Readiness: ${report.healthScore.aiReadiness}/100`);
|
|
555
|
+
lines.push(` Social: ${report.healthScore.social}/100`);
|
|
556
|
+
lines.push(` Local SEO: ${report.healthScore.localSeo}/100`);
|
|
557
|
+
lines.push(` Accessibility: ${report.healthScore.accessibility}/100`);
|
|
558
|
+
lines.push('');
|
|
559
|
+
|
|
560
|
+
// Summary
|
|
561
|
+
lines.push('đ SUMMARY');
|
|
562
|
+
lines.push('â'.repeat(70));
|
|
563
|
+
lines.push(` â Errors: ${report.summary.errors}`);
|
|
564
|
+
lines.push(` â ī¸ Warnings: ${report.summary.warnings}`);
|
|
565
|
+
lines.push(` âšī¸ Notices: ${report.summary.notices}`);
|
|
566
|
+
lines.push(` â
Passed: ${report.summary.passed}`);
|
|
567
|
+
lines.push('');
|
|
568
|
+
|
|
569
|
+
// Issues by category
|
|
570
|
+
const issuesByCategory = groupIssuesByCategory(report.issues);
|
|
571
|
+
|
|
572
|
+
for (const [category, issues] of Object.entries(issuesByCategory)) {
|
|
573
|
+
if (issues.length === 0) continue;
|
|
574
|
+
|
|
575
|
+
lines.push(`đ ${category.toUpperCase()}`);
|
|
576
|
+
lines.push('â'.repeat(70));
|
|
577
|
+
|
|
578
|
+
for (const issue of issues) {
|
|
579
|
+
const icon = issue.severity === 'error' ? 'â' : issue.severity === 'warning' ? 'â ī¸' : 'âšī¸';
|
|
580
|
+
lines.push(` ${icon} ${issue.title}`);
|
|
581
|
+
lines.push(` ${issue.description}`);
|
|
582
|
+
if (issue.howToFix) {
|
|
583
|
+
lines.push(` â ${issue.howToFix}`);
|
|
584
|
+
}
|
|
585
|
+
lines.push('');
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
lines.push('â'.repeat(70));
|
|
590
|
+
|
|
591
|
+
return lines.join('\n');
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function getScoreEmoji(score: number): string {
|
|
595
|
+
if (score >= 90) return 'đĸ Excellent';
|
|
596
|
+
if (score >= 70) return 'đĄ Good';
|
|
597
|
+
if (score >= 50) return 'đ Needs Work';
|
|
598
|
+
return 'đ´ Poor';
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function groupIssuesByCategory(issues: AuditIssue[]): Record<string, AuditIssue[]> {
|
|
602
|
+
const grouped: Record<string, AuditIssue[]> = {};
|
|
603
|
+
|
|
604
|
+
for (const issue of issues) {
|
|
605
|
+
if (!grouped[issue.category]) {
|
|
606
|
+
grouped[issue.category] = [];
|
|
607
|
+
}
|
|
608
|
+
grouped[issue.category].push(issue);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Sort by severity within each category
|
|
612
|
+
for (const category of Object.keys(grouped)) {
|
|
613
|
+
grouped[category].sort((a, b) => {
|
|
614
|
+
const order = { error: 0, warning: 1, notice: 2 };
|
|
615
|
+
return order[a.severity] - order[b.severity];
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return grouped;
|
|
620
|
+
}
|