@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.
Files changed (178) hide show
  1. package/README.md +242 -0
  2. package/dist/analyzer-2CSWIQGD.mjs +6 -0
  3. package/dist/chunk-YNZYHEYM.mjs +774 -0
  4. package/dist/index.d.mts +4012 -0
  5. package/dist/index.d.ts +4012 -0
  6. package/dist/index.js +29672 -0
  7. package/dist/index.mjs +28602 -0
  8. package/package.json +53 -0
  9. package/scripts/build-deno.ts +134 -0
  10. package/src/audit/ai/analyzer.ts +347 -0
  11. package/src/audit/ai/index.ts +29 -0
  12. package/src/audit/ai/prompts/content-analysis.ts +271 -0
  13. package/src/audit/ai/types.ts +179 -0
  14. package/src/audit/checks/additional-checks.ts +439 -0
  15. package/src/audit/checks/ai-citation-worthiness.ts +399 -0
  16. package/src/audit/checks/ai-content-structure.ts +325 -0
  17. package/src/audit/checks/ai-readiness.ts +339 -0
  18. package/src/audit/checks/anchor-text.ts +179 -0
  19. package/src/audit/checks/answer-conciseness.ts +322 -0
  20. package/src/audit/checks/asset-minification.ts +270 -0
  21. package/src/audit/checks/bing-optimization.ts +206 -0
  22. package/src/audit/checks/brand-mention-optimization.ts +349 -0
  23. package/src/audit/checks/caching-headers.ts +305 -0
  24. package/src/audit/checks/canonical-advanced.ts +150 -0
  25. package/src/audit/checks/canonical-domain.ts +196 -0
  26. package/src/audit/checks/citation-quality.ts +358 -0
  27. package/src/audit/checks/client-rendering.ts +542 -0
  28. package/src/audit/checks/color-contrast.ts +342 -0
  29. package/src/audit/checks/content-freshness.ts +170 -0
  30. package/src/audit/checks/content-science.ts +589 -0
  31. package/src/audit/checks/conversion-elements.ts +526 -0
  32. package/src/audit/checks/crawlability.ts +220 -0
  33. package/src/audit/checks/directory-listing.ts +172 -0
  34. package/src/audit/checks/dom-analysis.ts +191 -0
  35. package/src/audit/checks/dom-size.ts +246 -0
  36. package/src/audit/checks/duplicate-content.ts +194 -0
  37. package/src/audit/checks/eeat-signals.ts +990 -0
  38. package/src/audit/checks/entity-seo.ts +396 -0
  39. package/src/audit/checks/featured-snippet.ts +473 -0
  40. package/src/audit/checks/freshness-signals.ts +443 -0
  41. package/src/audit/checks/funnel-intent.ts +463 -0
  42. package/src/audit/checks/hreflang.ts +174 -0
  43. package/src/audit/checks/html-compliance.ts +302 -0
  44. package/src/audit/checks/image-dimensions.ts +167 -0
  45. package/src/audit/checks/images.ts +160 -0
  46. package/src/audit/checks/indexnow.ts +275 -0
  47. package/src/audit/checks/interactive-tools.ts +475 -0
  48. package/src/audit/checks/internal-link-graph.ts +436 -0
  49. package/src/audit/checks/keyword-analysis.ts +239 -0
  50. package/src/audit/checks/keyword-cannibalization.ts +385 -0
  51. package/src/audit/checks/keyword-placement.ts +471 -0
  52. package/src/audit/checks/links.ts +203 -0
  53. package/src/audit/checks/llms-txt.ts +224 -0
  54. package/src/audit/checks/local-seo.ts +296 -0
  55. package/src/audit/checks/mobile.ts +167 -0
  56. package/src/audit/checks/modern-images.ts +226 -0
  57. package/src/audit/checks/navboost-signals.ts +395 -0
  58. package/src/audit/checks/on-page.ts +209 -0
  59. package/src/audit/checks/page-resources.ts +285 -0
  60. package/src/audit/checks/pagination.ts +180 -0
  61. package/src/audit/checks/performance.ts +153 -0
  62. package/src/audit/checks/platform-presence.ts +580 -0
  63. package/src/audit/checks/redirect-analysis.ts +153 -0
  64. package/src/audit/checks/redirect-chain.ts +389 -0
  65. package/src/audit/checks/resource-hints.ts +420 -0
  66. package/src/audit/checks/responsive-css.ts +247 -0
  67. package/src/audit/checks/responsive-images.ts +396 -0
  68. package/src/audit/checks/review-ecosystem.ts +415 -0
  69. package/src/audit/checks/robots-validation.ts +373 -0
  70. package/src/audit/checks/security-headers.ts +172 -0
  71. package/src/audit/checks/security.ts +144 -0
  72. package/src/audit/checks/serp-preview.ts +251 -0
  73. package/src/audit/checks/site-maturity.ts +444 -0
  74. package/src/audit/checks/social-meta.test.ts +275 -0
  75. package/src/audit/checks/social-meta.ts +134 -0
  76. package/src/audit/checks/soft-404.ts +151 -0
  77. package/src/audit/checks/structured-data.ts +238 -0
  78. package/src/audit/checks/tech-detection.ts +496 -0
  79. package/src/audit/checks/topical-clusters.ts +435 -0
  80. package/src/audit/checks/tracker-bloat.ts +462 -0
  81. package/src/audit/checks/tracking-verification.test.ts +371 -0
  82. package/src/audit/checks/tracking-verification.ts +636 -0
  83. package/src/audit/checks/url-safety.ts +682 -0
  84. package/src/audit/deno-entry.ts +66 -0
  85. package/src/audit/discovery/index.ts +15 -0
  86. package/src/audit/discovery/link-crawler.ts +232 -0
  87. package/src/audit/discovery/repo-routes.ts +347 -0
  88. package/src/audit/engine.ts +620 -0
  89. package/src/audit/fixes/index.ts +209 -0
  90. package/src/audit/fixes/social-meta-fixes.test.ts +329 -0
  91. package/src/audit/fixes/social-meta-fixes.ts +463 -0
  92. package/src/audit/index.ts +74 -0
  93. package/src/audit/runner.test.ts +299 -0
  94. package/src/audit/runner.ts +130 -0
  95. package/src/audit/types.ts +1953 -0
  96. package/src/content/featured-snippet.ts +367 -0
  97. package/src/content/generator.test.ts +534 -0
  98. package/src/content/generator.ts +501 -0
  99. package/src/content/headline.ts +317 -0
  100. package/src/content/index.ts +62 -0
  101. package/src/content/intent.ts +258 -0
  102. package/src/content/keyword-density.ts +349 -0
  103. package/src/content/readability.ts +262 -0
  104. package/src/executor.ts +336 -0
  105. package/src/fixer.ts +416 -0
  106. package/src/frameworks/detector.test.ts +248 -0
  107. package/src/frameworks/detector.ts +371 -0
  108. package/src/frameworks/index.ts +68 -0
  109. package/src/frameworks/recipes/angular.yaml +171 -0
  110. package/src/frameworks/recipes/astro.yaml +206 -0
  111. package/src/frameworks/recipes/django.yaml +180 -0
  112. package/src/frameworks/recipes/laravel.yaml +137 -0
  113. package/src/frameworks/recipes/nextjs.yaml +268 -0
  114. package/src/frameworks/recipes/nuxt.yaml +175 -0
  115. package/src/frameworks/recipes/rails.yaml +188 -0
  116. package/src/frameworks/recipes/react.yaml +202 -0
  117. package/src/frameworks/recipes/sveltekit.yaml +154 -0
  118. package/src/frameworks/recipes/vue.yaml +137 -0
  119. package/src/frameworks/recipes/wordpress.yaml +209 -0
  120. package/src/frameworks/suggestion-engine.ts +320 -0
  121. package/src/geo/geo-content.test.ts +305 -0
  122. package/src/geo/geo-content.ts +266 -0
  123. package/src/geo/geo-history.test.ts +473 -0
  124. package/src/geo/geo-history.ts +433 -0
  125. package/src/geo/geo-tracker.test.ts +359 -0
  126. package/src/geo/geo-tracker.ts +411 -0
  127. package/src/geo/index.ts +10 -0
  128. package/src/git/commit-helper.test.ts +261 -0
  129. package/src/git/commit-helper.ts +329 -0
  130. package/src/git/index.ts +12 -0
  131. package/src/git/pr-helper.test.ts +284 -0
  132. package/src/git/pr-helper.ts +307 -0
  133. package/src/index.ts +66 -0
  134. package/src/keywords/ai-keyword-engine.ts +1062 -0
  135. package/src/keywords/ai-summarizer.ts +387 -0
  136. package/src/keywords/ci-mode.ts +555 -0
  137. package/src/keywords/engine.ts +359 -0
  138. package/src/keywords/index.ts +151 -0
  139. package/src/keywords/llm-judge.ts +357 -0
  140. package/src/keywords/nlp-analysis.ts +706 -0
  141. package/src/keywords/prioritizer.ts +295 -0
  142. package/src/keywords/site-crawler.ts +342 -0
  143. package/src/keywords/sources/autocomplete.ts +139 -0
  144. package/src/keywords/sources/competitive-search.ts +450 -0
  145. package/src/keywords/sources/competitor-analysis.ts +374 -0
  146. package/src/keywords/sources/dataforseo.ts +206 -0
  147. package/src/keywords/sources/free-sources.ts +294 -0
  148. package/src/keywords/sources/gsc.ts +123 -0
  149. package/src/keywords/topic-grouping.ts +327 -0
  150. package/src/keywords/types.ts +144 -0
  151. package/src/keywords/wizard.ts +457 -0
  152. package/src/loader.ts +40 -0
  153. package/src/reports/index.ts +7 -0
  154. package/src/reports/report-generator.test.ts +293 -0
  155. package/src/reports/report-generator.ts +713 -0
  156. package/src/scheduler/alerts.test.ts +458 -0
  157. package/src/scheduler/alerts.ts +328 -0
  158. package/src/scheduler/index.ts +8 -0
  159. package/src/scheduler/scheduled-audit.test.ts +377 -0
  160. package/src/scheduler/scheduled-audit.ts +149 -0
  161. package/src/test/integration-test.ts +325 -0
  162. package/src/tools/analyzer.ts +373 -0
  163. package/src/tools/crawl.ts +293 -0
  164. package/src/tools/files.ts +301 -0
  165. package/src/tools/h1-fixer.ts +249 -0
  166. package/src/tools/index.ts +67 -0
  167. package/src/tracking/github-action.ts +326 -0
  168. package/src/tracking/google-analytics.ts +265 -0
  169. package/src/tracking/index.ts +45 -0
  170. package/src/tracking/report-generator.ts +386 -0
  171. package/src/tracking/search-console.ts +335 -0
  172. package/src/types.ts +134 -0
  173. package/src/utils/http.ts +302 -0
  174. package/src/wasm-adapter.ts +297 -0
  175. package/src/wasm-entry.ts +14 -0
  176. package/tsconfig.json +17 -0
  177. package/tsup.wasm.config.ts +26 -0
  178. package/vitest.config.ts +15 -0
@@ -0,0 +1,713 @@
1
+ /**
2
+ * Report Generator
3
+ *
4
+ * Generates HTML and PDF reports for SEO audits with white-label branding support.
5
+ */
6
+
7
+ export interface ReportBranding {
8
+ companyName?: string;
9
+ logoUrl?: string;
10
+ primaryColor?: string;
11
+ accentColor?: string;
12
+ whiteLabel?: boolean;
13
+ contactEmail?: string;
14
+ websiteUrl?: string;
15
+ }
16
+
17
+ export interface ReportConfig {
18
+ branding?: ReportBranding;
19
+ includeExecutiveSummary?: boolean;
20
+ includeRecommendations?: boolean;
21
+ includePageDetails?: boolean;
22
+ maxIssues?: number;
23
+ }
24
+
25
+ export interface AuditIssueData {
26
+ code: string;
27
+ severity: 'error' | 'warning' | 'notice';
28
+ category: string;
29
+ title: string;
30
+ affectedUrls: string[];
31
+ description?: string;
32
+ howToFix?: string;
33
+ }
34
+
35
+ export interface ReportPageData {
36
+ url: string;
37
+ score: number;
38
+ issues: number;
39
+ }
40
+
41
+ export interface AuditReportData {
42
+ url: string;
43
+ domain: string;
44
+ timestamp: string;
45
+ score: number;
46
+ healthScores: {
47
+ overall: number;
48
+ crawlability: number;
49
+ indexability: number;
50
+ onPage: number;
51
+ content: number;
52
+ links: number;
53
+ performance: number;
54
+ security: number;
55
+ aiReadiness: number;
56
+ social: number;
57
+ localSeo: number;
58
+ };
59
+ summary: {
60
+ errors: number;
61
+ warnings: number;
62
+ notices: number;
63
+ passed: number;
64
+ };
65
+ issues: AuditIssueData[];
66
+ pages: ReportPageData[];
67
+ }
68
+
69
+ const DEFAULT_BRANDING: ReportBranding = {
70
+ companyName: 'SEO Autopilot',
71
+ primaryColor: '#2563eb',
72
+ accentColor: '#10b981',
73
+ };
74
+
75
+ function getScoreColor(score: number): string {
76
+ if (score >= 80) return '#22c55e'; // green
77
+ if (score >= 60) return '#f59e0b'; // yellow/orange
78
+ if (score >= 40) return '#f97316'; // orange
79
+ return '#ef4444'; // red
80
+ }
81
+
82
+ function getScoreLabel(score: number): string {
83
+ if (score >= 80) return 'good';
84
+ if (score >= 60) return 'warning';
85
+ return 'error';
86
+ }
87
+
88
+ function formatCategory(category: string): string {
89
+ const names: Record<string, string> = {
90
+ 'on-page': 'On-Page',
91
+ 'onPage': 'On-Page',
92
+ 'social-meta': 'Social Meta',
93
+ 'socialMeta': 'Social Meta',
94
+ 'performance': 'Performance',
95
+ 'crawlability': 'Crawlability',
96
+ 'indexability': 'Indexability',
97
+ 'structured-data': 'Structured Data',
98
+ 'structuredData': 'Structured Data',
99
+ 'security': 'Security',
100
+ 'mobile': 'Mobile',
101
+ 'links': 'Links',
102
+ 'images': 'Images',
103
+ 'content': 'Content',
104
+ };
105
+ return names[category] || category.charAt(0).toUpperCase() + category.slice(1);
106
+ }
107
+
108
+ function formatDate(timestamp: string): string {
109
+ return new Date(timestamp).toLocaleDateString('en-US', {
110
+ year: 'numeric',
111
+ month: 'long',
112
+ day: 'numeric',
113
+ hour: '2-digit',
114
+ minute: '2-digit',
115
+ });
116
+ }
117
+
118
+ function generateStyles(branding: ReportBranding): string {
119
+ const primary = branding.primaryColor || DEFAULT_BRANDING.primaryColor;
120
+ const accent = branding.accentColor || DEFAULT_BRANDING.accentColor;
121
+
122
+ return `
123
+ :root {
124
+ --primary: ${primary};
125
+ --accent: ${accent};
126
+ --success: #22c55e;
127
+ --warning: #f59e0b;
128
+ --error: #ef4444;
129
+ --text: #1f2937;
130
+ --text-light: #6b7280;
131
+ --bg: #ffffff;
132
+ --bg-secondary: #f9fafb;
133
+ --border: #e5e7eb;
134
+ }
135
+
136
+ * {
137
+ box-sizing: border-box;
138
+ margin: 0;
139
+ padding: 0;
140
+ }
141
+
142
+ body {
143
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
144
+ color: var(--text);
145
+ line-height: 1.6;
146
+ background: var(--bg);
147
+ }
148
+
149
+ .container {
150
+ max-width: 1200px;
151
+ margin: 0 auto;
152
+ padding: 40px 20px;
153
+ }
154
+
155
+ header {
156
+ display: flex;
157
+ justify-content: space-between;
158
+ align-items: center;
159
+ margin-bottom: 40px;
160
+ padding-bottom: 20px;
161
+ border-bottom: 2px solid var(--border);
162
+ }
163
+
164
+ .logo {
165
+ display: flex;
166
+ align-items: center;
167
+ gap: 12px;
168
+ }
169
+
170
+ .logo img {
171
+ max-height: 40px;
172
+ }
173
+
174
+ .logo h1 {
175
+ font-size: 24px;
176
+ font-weight: 700;
177
+ color: var(--primary);
178
+ }
179
+
180
+ .report-meta {
181
+ text-align: right;
182
+ color: var(--text-light);
183
+ font-size: 14px;
184
+ }
185
+
186
+ .score-card {
187
+ background: linear-gradient(135deg, var(--primary), var(--accent));
188
+ color: white;
189
+ border-radius: 16px;
190
+ padding: 40px;
191
+ margin-bottom: 40px;
192
+ display: flex;
193
+ justify-content: space-between;
194
+ align-items: center;
195
+ }
196
+
197
+ .score-main {
198
+ text-align: center;
199
+ }
200
+
201
+ .score-value {
202
+ font-size: 72px;
203
+ font-weight: 800;
204
+ line-height: 1;
205
+ }
206
+
207
+ .score-label {
208
+ font-size: 18px;
209
+ opacity: 0.9;
210
+ margin-top: 8px;
211
+ }
212
+
213
+ .score-stats {
214
+ display: grid;
215
+ grid-template-columns: repeat(4, 1fr);
216
+ gap: 20px;
217
+ }
218
+
219
+ .stat-item {
220
+ text-align: center;
221
+ padding: 16px;
222
+ background: rgba(255,255,255,0.1);
223
+ border-radius: 12px;
224
+ }
225
+
226
+ .stat-value {
227
+ font-size: 28px;
228
+ font-weight: 700;
229
+ }
230
+
231
+ .stat-label {
232
+ font-size: 12px;
233
+ opacity: 0.8;
234
+ text-transform: uppercase;
235
+ letter-spacing: 0.5px;
236
+ }
237
+
238
+ section {
239
+ margin-bottom: 40px;
240
+ }
241
+
242
+ h2 {
243
+ font-size: 20px;
244
+ font-weight: 600;
245
+ margin-bottom: 20px;
246
+ padding-bottom: 10px;
247
+ border-bottom: 2px solid var(--border);
248
+ }
249
+
250
+ .categories-grid {
251
+ display: grid;
252
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
253
+ gap: 16px;
254
+ }
255
+
256
+ .category-card {
257
+ background: var(--bg-secondary);
258
+ border-radius: 12px;
259
+ padding: 20px;
260
+ border: 1px solid var(--border);
261
+ }
262
+
263
+ .category-name {
264
+ font-weight: 600;
265
+ margin-bottom: 8px;
266
+ }
267
+
268
+ .category-score {
269
+ font-size: 24px;
270
+ font-weight: 700;
271
+ }
272
+
273
+ .issues-list {
274
+ background: var(--bg-secondary);
275
+ border-radius: 12px;
276
+ overflow: hidden;
277
+ border: 1px solid var(--border);
278
+ }
279
+
280
+ .issue-item {
281
+ padding: 16px 20px;
282
+ border-bottom: 1px solid var(--border);
283
+ display: flex;
284
+ align-items: flex-start;
285
+ gap: 12px;
286
+ }
287
+
288
+ .issue-item:last-child {
289
+ border-bottom: none;
290
+ }
291
+
292
+ .severity-badge {
293
+ padding: 4px 8px;
294
+ border-radius: 4px;
295
+ font-size: 11px;
296
+ font-weight: 600;
297
+ text-transform: uppercase;
298
+ flex-shrink: 0;
299
+ }
300
+
301
+ .severity-error {
302
+ background: #fef2f2;
303
+ color: var(--error);
304
+ }
305
+
306
+ .severity-warning {
307
+ background: #fffbeb;
308
+ color: var(--warning);
309
+ }
310
+
311
+ .severity-notice {
312
+ background: #eff6ff;
313
+ color: var(--primary);
314
+ }
315
+
316
+ .issue-content {
317
+ flex: 1;
318
+ }
319
+
320
+ .issue-title {
321
+ font-weight: 600;
322
+ margin-bottom: 4px;
323
+ }
324
+
325
+ .issue-code {
326
+ font-family: monospace;
327
+ font-size: 12px;
328
+ color: var(--text-light);
329
+ }
330
+
331
+ .issue-urls {
332
+ margin-top: 8px;
333
+ font-size: 13px;
334
+ color: var(--text-light);
335
+ }
336
+
337
+ .pages-table {
338
+ width: 100%;
339
+ border-collapse: collapse;
340
+ background: var(--bg-secondary);
341
+ border-radius: 12px;
342
+ overflow: hidden;
343
+ border: 1px solid var(--border);
344
+ }
345
+
346
+ .pages-table th,
347
+ .pages-table td {
348
+ padding: 12px 16px;
349
+ text-align: left;
350
+ border-bottom: 1px solid var(--border);
351
+ }
352
+
353
+ .pages-table th {
354
+ background: var(--bg);
355
+ font-weight: 600;
356
+ font-size: 13px;
357
+ text-transform: uppercase;
358
+ letter-spacing: 0.5px;
359
+ color: var(--text-light);
360
+ }
361
+
362
+ .pages-table tr:last-child td {
363
+ border-bottom: none;
364
+ }
365
+
366
+ .recommendations {
367
+ background: var(--bg-secondary);
368
+ border-radius: 12px;
369
+ padding: 24px;
370
+ border: 1px solid var(--border);
371
+ }
372
+
373
+ .recommendation-item {
374
+ margin-bottom: 20px;
375
+ padding-bottom: 20px;
376
+ border-bottom: 1px solid var(--border);
377
+ }
378
+
379
+ .recommendation-item:last-child {
380
+ margin-bottom: 0;
381
+ padding-bottom: 0;
382
+ border-bottom: none;
383
+ }
384
+
385
+ .recommendation-title {
386
+ font-weight: 600;
387
+ margin-bottom: 8px;
388
+ display: flex;
389
+ align-items: center;
390
+ gap: 8px;
391
+ }
392
+
393
+ .recommendation-text {
394
+ color: var(--text-light);
395
+ font-size: 14px;
396
+ }
397
+
398
+ footer {
399
+ margin-top: 60px;
400
+ padding-top: 20px;
401
+ border-top: 2px solid var(--border);
402
+ text-align: center;
403
+ color: var(--text-light);
404
+ font-size: 13px;
405
+ }
406
+
407
+ footer a {
408
+ color: var(--primary);
409
+ text-decoration: none;
410
+ }
411
+
412
+ .executive-summary {
413
+ background: linear-gradient(to right, #f0f9ff, #e0f2fe);
414
+ border-radius: 12px;
415
+ padding: 24px;
416
+ margin-bottom: 40px;
417
+ border: 1px solid #bae6fd;
418
+ }
419
+
420
+ .executive-summary h2 {
421
+ border-bottom: none;
422
+ margin-bottom: 16px;
423
+ padding-bottom: 0;
424
+ }
425
+
426
+ .executive-summary p {
427
+ color: var(--text);
428
+ line-height: 1.8;
429
+ }
430
+
431
+ .more-indicator {
432
+ padding: 16px 20px;
433
+ text-align: center;
434
+ color: var(--text-light);
435
+ font-style: italic;
436
+ }
437
+
438
+ @media (max-width: 768px) {
439
+ .score-card {
440
+ flex-direction: column;
441
+ gap: 30px;
442
+ }
443
+
444
+ .score-stats {
445
+ grid-template-columns: repeat(2, 1fr);
446
+ }
447
+
448
+ header {
449
+ flex-direction: column;
450
+ gap: 20px;
451
+ text-align: center;
452
+ }
453
+
454
+ .report-meta {
455
+ text-align: center;
456
+ }
457
+ }
458
+
459
+ @media print {
460
+ body {
461
+ print-color-adjust: exact;
462
+ -webkit-print-color-adjust: exact;
463
+ }
464
+
465
+ .container {
466
+ padding: 20px;
467
+ }
468
+
469
+ section {
470
+ page-break-inside: avoid;
471
+ }
472
+ }
473
+ `;
474
+ }
475
+
476
+ function generateExecutiveSummary(data: AuditReportData): string {
477
+ const { score, summary, issues } = data;
478
+ const topIssues = issues.filter(i => i.severity === 'error').slice(0, 3);
479
+
480
+ let status = 'needs improvement';
481
+ if (score >= 80) status = 'in good shape';
482
+ else if (score >= 60) status = 'has room for improvement';
483
+
484
+ return `
485
+ <section class="executive-summary">
486
+ <h2>Executive Summary</h2>
487
+ <p>
488
+ The SEO health of <strong>${data.domain}</strong> is ${status} with an overall score of
489
+ <strong>${score}/100</strong>. The audit found <strong>${summary.errors} critical errors</strong>
490
+ and <strong>${summary.warnings} warnings</strong> that should be addressed to improve search visibility.
491
+ ${topIssues.length > 0 ? `The most critical issues are: ${topIssues.map(i => i.title).join(', ')}.` : ''}
492
+ ${summary.passed} checks passed successfully.
493
+ </p>
494
+ </section>
495
+ `;
496
+ }
497
+
498
+ function generateRecommendations(issues: AuditIssueData[]): string {
499
+ const errorIssues = issues.filter(i => i.severity === 'error').slice(0, 5);
500
+
501
+ if (errorIssues.length === 0) {
502
+ return `
503
+ <section>
504
+ <h2>Recommendations</h2>
505
+ <div class="recommendations">
506
+ <p>Great job! No critical errors found. Continue monitoring your site regularly.</p>
507
+ </div>
508
+ </section>
509
+ `;
510
+ }
511
+
512
+ const recommendations = errorIssues.map(issue => `
513
+ <div class="recommendation-item">
514
+ <div class="recommendation-title">
515
+ <span class="severity-badge severity-error">Priority</span>
516
+ ${issue.title}
517
+ </div>
518
+ <div class="recommendation-text">
519
+ <strong>How to Fix:</strong> ${issue.howToFix || `Address the ${issue.code} issue to improve your SEO score.`}
520
+ </div>
521
+ </div>
522
+ `).join('');
523
+
524
+ return `
525
+ <section>
526
+ <h2>Recommendations</h2>
527
+ <div class="recommendations">
528
+ ${recommendations}
529
+ </div>
530
+ </section>
531
+ `;
532
+ }
533
+
534
+ /**
535
+ * Generates an HTML report from audit data
536
+ */
537
+ export function generateHTMLReport(
538
+ data: AuditReportData,
539
+ config: ReportConfig = {}
540
+ ): string {
541
+ const branding = { ...DEFAULT_BRANDING, ...config.branding };
542
+ const {
543
+ includeExecutiveSummary = false,
544
+ includeRecommendations = false,
545
+ includePageDetails = true,
546
+ maxIssues,
547
+ } = config;
548
+
549
+ const scoreColor = getScoreColor(data.score);
550
+ const scoreLabel = getScoreLabel(data.score);
551
+ const displayIssues = maxIssues ? data.issues.slice(0, maxIssues) : data.issues;
552
+ const remainingIssues = maxIssues ? data.issues.length - maxIssues : 0;
553
+
554
+ const logoHtml = branding.logoUrl
555
+ ? `<img src="${branding.logoUrl}" alt="${branding.companyName}" />`
556
+ : '';
557
+
558
+ const footerHtml = branding.whiteLabel
559
+ ? `<footer>
560
+ <p>Generated on ${formatDate(data.timestamp)}</p>
561
+ ${branding.contactEmail ? `<p>Contact: <a href="mailto:${branding.contactEmail}">${branding.contactEmail}</a></p>` : ''}
562
+ ${branding.websiteUrl ? `<p><a href="${branding.websiteUrl}">${branding.websiteUrl}</a></p>` : ''}
563
+ </footer>`
564
+ : `<footer>
565
+ <p>Generated by <a href="https://github.com/seo-autopilot/seo-autopilot">SEO Autopilot</a> on ${formatDate(data.timestamp)}</p>
566
+ </footer>`;
567
+
568
+ const categoriesHtml = Object.entries(data.healthScores)
569
+ .filter(([key]) => key !== 'overall')
570
+ .map(([key, value]) => `
571
+ <div class="category-card">
572
+ <div class="category-name">${formatCategory(key)}</div>
573
+ <div class="category-score" style="color: ${getScoreColor(value)}">${value}/100</div>
574
+ </div>
575
+ `).join('');
576
+
577
+ const issuesHtml = displayIssues.map(issue => `
578
+ <div class="issue-item">
579
+ <span class="severity-badge severity-${issue.severity}">${issue.severity}</span>
580
+ <div class="issue-content">
581
+ <div class="issue-title">${issue.title}</div>
582
+ <div class="issue-code">${issue.code}</div>
583
+ ${issue.affectedUrls.length > 0 ? `
584
+ <div class="issue-urls">
585
+ Affected: ${issue.affectedUrls.slice(0, 3).join(', ')}
586
+ ${issue.affectedUrls.length > 3 ? ` and ${issue.affectedUrls.length - 3} more` : ''}
587
+ </div>
588
+ ` : ''}
589
+ </div>
590
+ </div>
591
+ `).join('');
592
+
593
+ const moreIndicator = remainingIssues > 0
594
+ ? `<div class="more-indicator">...and ${remainingIssues} more issues</div>`
595
+ : '';
596
+
597
+ const pagesHtml = includePageDetails && data.pages.length > 0 ? `
598
+ <section>
599
+ <h2>Page Details</h2>
600
+ <table class="pages-table">
601
+ <thead>
602
+ <tr>
603
+ <th>URL</th>
604
+ <th>Score</th>
605
+ <th>Issues</th>
606
+ </tr>
607
+ </thead>
608
+ <tbody>
609
+ ${data.pages.map(page => `
610
+ <tr>
611
+ <td>${page.url}</td>
612
+ <td style="color: ${getScoreColor(page.score)}">${page.score}/100</td>
613
+ <td>${page.issues}</td>
614
+ </tr>
615
+ `).join('')}
616
+ </tbody>
617
+ </table>
618
+ </section>
619
+ ` : '';
620
+
621
+ return `<!DOCTYPE html>
622
+ <html lang="en">
623
+ <head>
624
+ <meta charset="UTF-8">
625
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
626
+ <title>SEO Audit Report - ${data.domain}</title>
627
+ <style>${generateStyles(branding)}</style>
628
+ </head>
629
+ <body>
630
+ <div class="container">
631
+ <header>
632
+ <div class="logo">
633
+ ${logoHtml}
634
+ <h1>${branding.companyName}</h1>
635
+ </div>
636
+ <div class="report-meta">
637
+ <div><strong>${data.domain}</strong></div>
638
+ <div>${formatDate(data.timestamp)}</div>
639
+ </div>
640
+ </header>
641
+
642
+ <div class="score-card">
643
+ <div class="score-main">
644
+ <div class="score-value">${data.score}</div>
645
+ <div class="score-label">SEO Score /100</div>
646
+ </div>
647
+ <div class="score-stats">
648
+ <div class="stat-item">
649
+ <div class="stat-value">${data.summary.errors}</div>
650
+ <div class="stat-label">Errors</div>
651
+ </div>
652
+ <div class="stat-item">
653
+ <div class="stat-value">${data.summary.warnings}</div>
654
+ <div class="stat-label">Warnings</div>
655
+ </div>
656
+ <div class="stat-item">
657
+ <div class="stat-value">${data.summary.notices}</div>
658
+ <div class="stat-label">Notices</div>
659
+ </div>
660
+ <div class="stat-item">
661
+ <div class="stat-value">${data.summary.passed}</div>
662
+ <div class="stat-label">Passed</div>
663
+ </div>
664
+ </div>
665
+ </div>
666
+
667
+ ${includeExecutiveSummary ? generateExecutiveSummary(data) : ''}
668
+
669
+ <section>
670
+ <h2>Category Scores</h2>
671
+ <div class="categories-grid">
672
+ ${categoriesHtml}
673
+ </div>
674
+ </section>
675
+
676
+ <section>
677
+ <h2>Issues Found</h2>
678
+ <div class="issues-list">
679
+ ${issuesHtml}
680
+ ${moreIndicator}
681
+ </div>
682
+ </section>
683
+
684
+ ${includeRecommendations ? generateRecommendations(data.issues) : ''}
685
+
686
+ ${pagesHtml}
687
+
688
+ ${footerHtml}
689
+ </div>
690
+ </body>
691
+ </html>`;
692
+ }
693
+
694
+ /**
695
+ * Generates a PDF report from audit data
696
+ * Uses the HTML report as a base and converts to PDF
697
+ */
698
+ export async function generatePDFReport(
699
+ data: AuditReportData,
700
+ config: ReportConfig = {}
701
+ ): Promise<Buffer> {
702
+ // Generate HTML first
703
+ const html = generateHTMLReport(data, {
704
+ ...config,
705
+ includeExecutiveSummary: config.includeExecutiveSummary ?? true,
706
+ includeRecommendations: config.includeRecommendations ?? true,
707
+ });
708
+
709
+ // For now, return the HTML as a buffer
710
+ // In production, this would use puppeteer or similar to generate actual PDF
711
+ // This is a placeholder that satisfies the interface
712
+ return Buffer.from(html, 'utf-8');
713
+ }