s3db.js 18.0.11-next.1534f717 → 18.0.11-next.e8e71b5b

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 (190) hide show
  1. package/dist/clients/recker-http-handler.js +56 -8
  2. package/dist/clients/recker-http-handler.js.map +1 -1
  3. package/dist/concerns/high-performance-inserter.js +6 -34
  4. package/dist/concerns/high-performance-inserter.js.map +1 -1
  5. package/dist/concerns/id/alphabets.js +150 -0
  6. package/dist/concerns/id/alphabets.js.map +1 -0
  7. package/dist/concerns/id/entropy.js +243 -0
  8. package/dist/concerns/id/entropy.js.map +1 -0
  9. package/dist/concerns/id/generators/nanoid.js +74 -0
  10. package/dist/concerns/id/generators/nanoid.js.map +1 -0
  11. package/dist/concerns/id/generators/sid.js +73 -0
  12. package/dist/concerns/id/generators/sid.js.map +1 -0
  13. package/dist/concerns/id/generators/ulid.js +208 -0
  14. package/dist/concerns/id/generators/ulid.js.map +1 -0
  15. package/dist/concerns/id/generators/uuid-v7.js +150 -0
  16. package/dist/concerns/id/generators/uuid-v7.js.map +1 -0
  17. package/dist/concerns/id/index.js +74 -0
  18. package/dist/concerns/id/index.js.map +1 -0
  19. package/dist/concerns/plugin-storage.js +114 -0
  20. package/dist/concerns/plugin-storage.js.map +1 -1
  21. package/dist/concerns/s3-errors.js +72 -0
  22. package/dist/concerns/s3-errors.js.map +1 -0
  23. package/dist/concerns/s3-key.js +54 -0
  24. package/dist/concerns/s3-key.js.map +1 -0
  25. package/dist/concerns/safe-merge.js +47 -0
  26. package/dist/concerns/safe-merge.js.map +1 -0
  27. package/dist/core/resource-config-validator.js +12 -2
  28. package/dist/core/resource-config-validator.js.map +1 -1
  29. package/dist/core/resource-partitions.class.js +12 -1
  30. package/dist/core/resource-partitions.class.js.map +1 -1
  31. package/dist/core/resource-persistence.class.js +41 -12
  32. package/dist/core/resource-persistence.class.js.map +1 -1
  33. package/dist/core/resource-query.class.js +21 -47
  34. package/dist/core/resource-query.class.js.map +1 -1
  35. package/dist/database/database-connection.class.js +3 -6
  36. package/dist/database/database-connection.class.js.map +1 -1
  37. package/dist/database/database-plugins.class.js +7 -13
  38. package/dist/database/database-plugins.class.js.map +1 -1
  39. package/dist/plugins/concerns/s3-mutex.class.js +155 -0
  40. package/dist/plugins/concerns/s3-mutex.class.js.map +1 -0
  41. package/dist/plugins/eventual-consistency/consolidation.js +4 -7
  42. package/dist/plugins/eventual-consistency/consolidation.js.map +1 -1
  43. package/dist/plugins/eventual-consistency/garbage-collection.js +3 -6
  44. package/dist/plugins/eventual-consistency/garbage-collection.js.map +1 -1
  45. package/dist/plugins/queue-consumer.plugin.js +10 -16
  46. package/dist/plugins/queue-consumer.plugin.js.map +1 -1
  47. package/dist/plugins/recon/managers/scheduler-manager.js +3 -5
  48. package/dist/plugins/recon/managers/scheduler-manager.js.map +1 -1
  49. package/dist/plugins/recon/stages/recker-asn-stage.js +279 -0
  50. package/dist/plugins/recon/stages/recker-asn-stage.js.map +1 -0
  51. package/dist/plugins/recon/stages/recker-dns-stage.js +227 -0
  52. package/dist/plugins/recon/stages/recker-dns-stage.js.map +1 -0
  53. package/dist/plugins/recon/stages/recker-scrape-stage.js +369 -0
  54. package/dist/plugins/recon/stages/recker-scrape-stage.js.map +1 -0
  55. package/dist/plugins/replicator.plugin.js +13 -31
  56. package/dist/plugins/replicator.plugin.js.map +1 -1
  57. package/dist/plugins/replicators/base-replicator.class.js +10 -23
  58. package/dist/plugins/replicators/base-replicator.class.js.map +1 -1
  59. package/dist/plugins/spider/recker-link-discoverer.js +544 -0
  60. package/dist/plugins/spider/recker-link-discoverer.js.map +1 -0
  61. package/dist/plugins/spider/recker-llms-validator.js +334 -0
  62. package/dist/plugins/spider/recker-llms-validator.js.map +1 -0
  63. package/dist/plugins/spider/recker-robots-validator.js +336 -0
  64. package/dist/plugins/spider/recker-robots-validator.js.map +1 -0
  65. package/dist/plugins/spider/recker-security-adapter.js +325 -0
  66. package/dist/plugins/spider/recker-security-adapter.js.map +1 -0
  67. package/dist/plugins/spider/recker-seo-adapter.js +399 -0
  68. package/dist/plugins/spider/recker-seo-adapter.js.map +1 -0
  69. package/dist/plugins/spider/recker-sitemap-validator.js +406 -0
  70. package/dist/plugins/spider/recker-sitemap-validator.js.map +1 -0
  71. package/dist/resource.class.js +2 -0
  72. package/dist/resource.class.js.map +1 -1
  73. package/dist/s3db.cjs +444 -219
  74. package/dist/s3db.cjs.map +1 -1
  75. package/dist/s3db.es.js +445 -220
  76. package/dist/s3db.es.js.map +1 -1
  77. package/dist/stream/resource-reader.class.js +5 -7
  78. package/dist/stream/resource-reader.class.js.map +1 -1
  79. package/dist/stream/resource-writer.class.js +5 -7
  80. package/dist/stream/resource-writer.class.js.map +1 -1
  81. package/dist/tasks/tasks-pool.class.js +31 -0
  82. package/dist/tasks/tasks-pool.class.js.map +1 -1
  83. package/dist/types/clients/recker-http-handler.d.ts +1 -0
  84. package/dist/types/clients/recker-http-handler.d.ts.map +1 -1
  85. package/dist/types/clients/types.d.ts +14 -0
  86. package/dist/types/clients/types.d.ts.map +1 -1
  87. package/dist/types/concerns/high-performance-inserter.d.ts.map +1 -1
  88. package/dist/types/concerns/id/alphabets.d.ts +125 -0
  89. package/dist/types/concerns/id/alphabets.d.ts.map +1 -0
  90. package/dist/types/concerns/id/entropy.d.ts +84 -0
  91. package/dist/types/concerns/id/entropy.d.ts.map +1 -0
  92. package/dist/types/concerns/id/generators/nanoid.d.ts +46 -0
  93. package/dist/types/concerns/id/generators/nanoid.d.ts.map +1 -0
  94. package/dist/types/concerns/id/generators/sid.d.ts +45 -0
  95. package/dist/types/concerns/id/generators/sid.d.ts.map +1 -0
  96. package/dist/types/concerns/id/generators/ulid.d.ts +71 -0
  97. package/dist/types/concerns/id/generators/ulid.d.ts.map +1 -0
  98. package/dist/types/concerns/id/generators/uuid-v7.d.ts +60 -0
  99. package/dist/types/concerns/id/generators/uuid-v7.d.ts.map +1 -0
  100. package/dist/types/concerns/id/index.d.ts +51 -0
  101. package/dist/types/concerns/id/index.d.ts.map +1 -0
  102. package/dist/types/concerns/plugin-storage.d.ts +25 -0
  103. package/dist/types/concerns/plugin-storage.d.ts.map +1 -1
  104. package/dist/types/concerns/s3-errors.d.ts +20 -0
  105. package/dist/types/concerns/s3-errors.d.ts.map +1 -0
  106. package/dist/types/concerns/s3-key.d.ts +30 -0
  107. package/dist/types/concerns/s3-key.d.ts.map +1 -0
  108. package/dist/types/concerns/safe-merge.d.ts +22 -0
  109. package/dist/types/concerns/safe-merge.d.ts.map +1 -0
  110. package/dist/types/core/resource-config-validator.d.ts.map +1 -1
  111. package/dist/types/core/resource-partitions.class.d.ts.map +1 -1
  112. package/dist/types/core/resource-persistence.class.d.ts.map +1 -1
  113. package/dist/types/core/resource-query.class.d.ts.map +1 -1
  114. package/dist/types/database/database-connection.class.d.ts.map +1 -1
  115. package/dist/types/database/database-plugins.class.d.ts.map +1 -1
  116. package/dist/types/plugins/concerns/s3-mutex.class.d.ts +30 -0
  117. package/dist/types/plugins/concerns/s3-mutex.class.d.ts.map +1 -0
  118. package/dist/types/plugins/eventual-consistency/consolidation.d.ts.map +1 -1
  119. package/dist/types/plugins/eventual-consistency/garbage-collection.d.ts.map +1 -1
  120. package/dist/types/plugins/queue-consumer.plugin.d.ts.map +1 -1
  121. package/dist/types/plugins/recon/managers/scheduler-manager.d.ts.map +1 -1
  122. package/dist/types/plugins/recon/stages/recker-asn-stage.d.ts +90 -0
  123. package/dist/types/plugins/recon/stages/recker-asn-stage.d.ts.map +1 -0
  124. package/dist/types/plugins/recon/stages/recker-dns-stage.d.ts +125 -0
  125. package/dist/types/plugins/recon/stages/recker-dns-stage.d.ts.map +1 -0
  126. package/dist/types/plugins/recon/stages/recker-scrape-stage.d.ts +96 -0
  127. package/dist/types/plugins/recon/stages/recker-scrape-stage.d.ts.map +1 -0
  128. package/dist/types/plugins/replicator.plugin.d.ts.map +1 -1
  129. package/dist/types/plugins/replicators/base-replicator.class.d.ts.map +1 -1
  130. package/dist/types/plugins/spider/recker-link-discoverer.d.ts +54 -0
  131. package/dist/types/plugins/spider/recker-link-discoverer.d.ts.map +1 -0
  132. package/dist/types/plugins/spider/recker-llms-validator.d.ts +105 -0
  133. package/dist/types/plugins/spider/recker-llms-validator.d.ts.map +1 -0
  134. package/dist/types/plugins/spider/recker-robots-validator.d.ts +92 -0
  135. package/dist/types/plugins/spider/recker-robots-validator.d.ts.map +1 -0
  136. package/dist/types/plugins/spider/recker-security-adapter.d.ts +83 -0
  137. package/dist/types/plugins/spider/recker-security-adapter.d.ts.map +1 -0
  138. package/dist/types/plugins/spider/recker-seo-adapter.d.ts +187 -0
  139. package/dist/types/plugins/spider/recker-seo-adapter.d.ts.map +1 -0
  140. package/dist/types/plugins/spider/recker-sitemap-validator.d.ts +121 -0
  141. package/dist/types/plugins/spider/recker-sitemap-validator.d.ts.map +1 -0
  142. package/dist/types/resource.class.d.ts.map +1 -1
  143. package/dist/types/stream/resource-reader.class.d.ts.map +1 -1
  144. package/dist/types/stream/resource-writer.class.d.ts.map +1 -1
  145. package/dist/types/tasks/tasks-pool.class.d.ts +23 -0
  146. package/dist/types/tasks/tasks-pool.class.d.ts.map +1 -1
  147. package/mcp/prompts/index.ts +275 -0
  148. package/mcp/resources/index.ts +322 -0
  149. package/mcp/tools/plugins.ts +1137 -0
  150. package/mcp/tools/streams.ts +340 -0
  151. package/package.json +20 -22
  152. package/src/clients/recker-http-handler.ts +74 -8
  153. package/src/clients/types.ts +14 -0
  154. package/src/concerns/high-performance-inserter.ts +18 -57
  155. package/src/concerns/id/alphabets.ts +175 -0
  156. package/src/concerns/id/entropy.ts +286 -0
  157. package/src/concerns/id/generators/sid.ts +90 -0
  158. package/src/concerns/id/generators/ulid.ts +249 -0
  159. package/src/concerns/id/generators/uuid-v7.ts +179 -0
  160. package/src/concerns/id/index.ts +167 -0
  161. package/src/concerns/plugin-storage.ts +144 -0
  162. package/src/concerns/s3-errors.ts +97 -0
  163. package/src/concerns/s3-key.ts +62 -0
  164. package/src/concerns/safe-merge.ts +60 -0
  165. package/src/core/resource-config-validator.ts +9 -2
  166. package/src/core/resource-partitions.class.ts +14 -1
  167. package/src/core/resource-persistence.class.ts +47 -13
  168. package/src/core/resource-query.class.ts +21 -46
  169. package/src/database/database-connection.class.ts +7 -6
  170. package/src/database/database-plugins.class.ts +15 -13
  171. package/src/plugins/concerns/s3-mutex.class.ts +228 -0
  172. package/src/plugins/eventual-consistency/consolidation.ts +8 -7
  173. package/src/plugins/eventual-consistency/garbage-collection.ts +7 -6
  174. package/src/plugins/queue-consumer.plugin.ts +21 -19
  175. package/src/plugins/recon/managers/scheduler-manager.ts +7 -5
  176. package/src/plugins/recon/stages/recker-asn-stage.ts +385 -0
  177. package/src/plugins/recon/stages/recker-dns-stage.ts +360 -0
  178. package/src/plugins/recon/stages/recker-scrape-stage.ts +509 -0
  179. package/src/plugins/replicator.plugin.ts +41 -35
  180. package/src/plugins/replicators/base-replicator.class.ts +17 -23
  181. package/src/plugins/spider/recker-link-discoverer.ts +645 -0
  182. package/src/plugins/spider/recker-llms-validator.ts +500 -0
  183. package/src/plugins/spider/recker-robots-validator.ts +473 -0
  184. package/src/plugins/spider/recker-security-adapter.ts +489 -0
  185. package/src/plugins/spider/recker-seo-adapter.ts +605 -0
  186. package/src/plugins/spider/recker-sitemap-validator.ts +621 -0
  187. package/src/resource.class.ts +2 -0
  188. package/src/stream/resource-reader.class.ts +10 -8
  189. package/src/stream/resource-writer.class.ts +10 -8
  190. package/src/tasks/tasks-pool.class.ts +46 -0
@@ -0,0 +1,605 @@
1
+ import type {
2
+ AnalysisResult,
3
+ SEOAnalyzerConfig,
4
+ SEOScore,
5
+ OnPageSEOAnalysis,
6
+ AccessibilityAnalysis,
7
+ InternalLinksAnalysis,
8
+ KeywordOptimizationAnalysis,
9
+ AssetsAnalysis
10
+ } from './seo-analyzer.js';
11
+
12
+ type ReckerSeoReport = {
13
+ url: string;
14
+ timestamp: Date;
15
+ grade: string;
16
+ score: number;
17
+ summary: {
18
+ totalChecks: number;
19
+ passed: number;
20
+ warnings: number;
21
+ errors: number;
22
+ infos: number;
23
+ passRate: number;
24
+ issuesByCategory: Record<string, { passed: number; warnings: number; errors: number }>;
25
+ topIssues: Array<{ name: string; message: string; category: string; severity: 'error' | 'warning' }>;
26
+ quickWins: string[];
27
+ vitals: {
28
+ htmlSize?: number;
29
+ domElements?: number;
30
+ ttfb?: number;
31
+ totalTime?: number;
32
+ wordCount: number;
33
+ totalWordCount?: number;
34
+ readingTime: number;
35
+ imageCount: number;
36
+ linkCount: number;
37
+ };
38
+ completeness: {
39
+ meta: number;
40
+ social: number;
41
+ technical: number;
42
+ content: number;
43
+ images: number;
44
+ links: number;
45
+ };
46
+ };
47
+ checks: Array<{
48
+ name: string;
49
+ category: string;
50
+ status: 'pass' | 'warn' | 'fail' | 'info';
51
+ message: string;
52
+ value?: string | number;
53
+ recommendation?: string;
54
+ }>;
55
+ title?: { text: string; length: number };
56
+ metaDescription?: { text: string; length: number };
57
+ openGraph?: {
58
+ title?: string;
59
+ description?: string;
60
+ image?: string;
61
+ url?: string;
62
+ type?: string;
63
+ siteName?: string;
64
+ };
65
+ twitterCard?: {
66
+ card?: string;
67
+ title?: string;
68
+ description?: string;
69
+ image?: string;
70
+ site?: string;
71
+ };
72
+ structuredData: {
73
+ count: number;
74
+ types: string[];
75
+ items: Record<string, unknown>[];
76
+ };
77
+ content: {
78
+ wordCount: number;
79
+ totalWordCount?: number;
80
+ characterCount: number;
81
+ sentenceCount: number;
82
+ paragraphCount: number;
83
+ readingTimeMinutes: number;
84
+ avgWordsPerSentence: number;
85
+ avgParagraphLength: number;
86
+ listCount: number;
87
+ strongTagCount: number;
88
+ emTagCount: number;
89
+ fleschReadingEase?: number;
90
+ hasQuestionHeadings?: boolean;
91
+ };
92
+ headings: {
93
+ structure: Array<{ level: number; text: string; count: number }>;
94
+ h1Count: number;
95
+ hasProperHierarchy: boolean;
96
+ issues: string[];
97
+ };
98
+ keywords: {
99
+ primary: string | null;
100
+ secondary: string[];
101
+ density: Record<string, number>;
102
+ };
103
+ links: {
104
+ total: number;
105
+ internal: number;
106
+ external: number;
107
+ nofollow: number;
108
+ broken: number;
109
+ withoutText: number;
110
+ sponsoredLinks: number;
111
+ ugcLinks: number;
112
+ internalHttpLinks?: number;
113
+ internalHttpLinkUrls?: string[];
114
+ };
115
+ images: {
116
+ total: number;
117
+ withAlt: number;
118
+ withoutAlt: number;
119
+ lazy: number;
120
+ missingDimensions: number;
121
+ modernFormats: number;
122
+ altTextLengths: number[];
123
+ imageAltTexts: string[];
124
+ imageFilenames: string[];
125
+ imagesWithAsyncDecoding: number;
126
+ };
127
+ social: {
128
+ openGraph: {
129
+ present: boolean;
130
+ hasTitle: boolean;
131
+ hasDescription: boolean;
132
+ hasImage: boolean;
133
+ hasUrl: boolean;
134
+ issues: string[];
135
+ };
136
+ twitterCard: {
137
+ present: boolean;
138
+ hasCard: boolean;
139
+ hasTitle: boolean;
140
+ hasDescription: boolean;
141
+ hasImage: boolean;
142
+ issues: string[];
143
+ };
144
+ };
145
+ technical: {
146
+ hasCanonical: boolean;
147
+ canonicalUrl?: string;
148
+ hasRobotsMeta: boolean;
149
+ robotsContent?: string[];
150
+ hasViewport: boolean;
151
+ hasCharset: boolean;
152
+ hasLang: boolean;
153
+ langValue?: string;
154
+ };
155
+ };
156
+
157
+ type ReckerAnalyzeSeo = (html: string, options?: {
158
+ baseUrl?: string;
159
+ analyzeContent?: boolean;
160
+ checkBrokenLinks?: boolean;
161
+ rules?: {
162
+ categories?: string[];
163
+ excludeCategories?: string[];
164
+ rules?: string[];
165
+ excludeRules?: string[];
166
+ minSeverity?: 'error' | 'warning' | 'info';
167
+ };
168
+ }) => Promise<ReckerSeoReport>;
169
+
170
+ const ACTIVITY_TO_CATEGORY: Record<string, string> = {
171
+ 'seo_meta_tags': 'meta',
172
+ 'seo_opengraph': 'og',
173
+ 'seo_twitter_card': 'twitter',
174
+ 'seo_content_analysis': 'content',
175
+ 'seo_heading_structure': 'headings',
176
+ 'seo_links_analysis': 'links',
177
+ 'seo_accessibility': 'accessibility',
178
+ 'seo_keyword_optimization': 'content'
179
+ };
180
+
181
+ export class ReckerSEOAdapter {
182
+ private config: Required<SEOAnalyzerConfig>;
183
+ private reckerAvailable: boolean | null = null;
184
+ private analyzeSeo: ReckerAnalyzeSeo | null = null;
185
+ private fallbackAnalyzer: import('./seo-analyzer.js').SEOAnalyzer | null = null;
186
+
187
+ constructor(config: SEOAnalyzerConfig = {}) {
188
+ this.config = {
189
+ extractMetaTags: config.extractMetaTags !== false,
190
+ extractOpenGraph: config.extractOpenGraph !== false,
191
+ extractTwitterCard: config.extractTwitterCard !== false,
192
+ extractAssets: config.extractAssets !== false,
193
+ assetMetadata: config.assetMetadata !== false,
194
+ analyzeOnPageSEO: config.analyzeOnPageSEO !== false,
195
+ analyzeAccessibility: config.analyzeAccessibility !== false,
196
+ analyzeInternalLinks: config.analyzeInternalLinks !== false,
197
+ analyzeKeywordOptimization: config.analyzeKeywordOptimization !== false
198
+ };
199
+ }
200
+
201
+ private async _checkReckerAvailability(): Promise<boolean> {
202
+ if (this.reckerAvailable !== null) {
203
+ return this.reckerAvailable;
204
+ }
205
+
206
+ try {
207
+ const recker = await import('recker');
208
+ if (recker.analyzeSeo) {
209
+ this.analyzeSeo = recker.analyzeSeo as unknown as ReckerAnalyzeSeo;
210
+ this.reckerAvailable = true;
211
+ return true;
212
+ }
213
+ } catch {
214
+ // Recker not available
215
+ }
216
+ this.reckerAvailable = false;
217
+ return false;
218
+ }
219
+
220
+ private async _getFallbackAnalyzer(): Promise<import('./seo-analyzer.js').SEOAnalyzer> {
221
+ if (!this.fallbackAnalyzer) {
222
+ const { SEOAnalyzer } = await import('./seo-analyzer.js');
223
+ this.fallbackAnalyzer = new SEOAnalyzer(this.config);
224
+ }
225
+ return this.fallbackAnalyzer;
226
+ }
227
+
228
+ async analyze(html: string, baseUrl: string): Promise<AnalysisResult> {
229
+ const isReckerAvailable = await this._checkReckerAvailability();
230
+
231
+ if (isReckerAvailable && this.analyzeSeo) {
232
+ return this._analyzeWithRecker(html, baseUrl);
233
+ }
234
+
235
+ const fallback = await this._getFallbackAnalyzer();
236
+ return fallback.analyze(html, baseUrl);
237
+ }
238
+
239
+ async analyzeSelective(html: string, baseUrl: string, activities: string[] = []): Promise<AnalysisResult> {
240
+ if (!activities || activities.length === 0) {
241
+ return this.analyze(html, baseUrl);
242
+ }
243
+
244
+ const isReckerAvailable = await this._checkReckerAvailability();
245
+
246
+ if (isReckerAvailable && this.analyzeSeo) {
247
+ const categories = this._mapActivitiesToCategories(activities);
248
+ return this._analyzeWithRecker(html, baseUrl, categories);
249
+ }
250
+
251
+ const fallback = await this._getFallbackAnalyzer();
252
+ return fallback.analyzeSelective(html, baseUrl, activities);
253
+ }
254
+
255
+ private async _analyzeWithRecker(
256
+ html: string,
257
+ baseUrl: string,
258
+ categories?: string[]
259
+ ): Promise<AnalysisResult> {
260
+ if (!this.analyzeSeo) {
261
+ throw new Error('Recker analyzeSeo not available');
262
+ }
263
+
264
+ const report = await this.analyzeSeo(html, {
265
+ baseUrl,
266
+ analyzeContent: true,
267
+ rules: categories ? { categories } : undefined
268
+ });
269
+
270
+ return this._mapReckerToAnalysisResult(report, baseUrl);
271
+ }
272
+
273
+ private _mapActivitiesToCategories(activities: string[]): string[] {
274
+ const categories = new Set<string>();
275
+
276
+ for (const activity of activities) {
277
+ const category = ACTIVITY_TO_CATEGORY[activity];
278
+ if (category) {
279
+ categories.add(category);
280
+ }
281
+ }
282
+
283
+ return Array.from(categories);
284
+ }
285
+
286
+ private _mapReckerToAnalysisResult(report: ReckerSeoReport, baseUrl: string): AnalysisResult {
287
+ const metaTags: Record<string, string> = {};
288
+
289
+ if (report.title?.text) {
290
+ metaTags.title = report.title.text;
291
+ }
292
+ if (report.metaDescription?.text) {
293
+ metaTags.description = report.metaDescription.text;
294
+ }
295
+ if (report.technical.hasViewport) {
296
+ metaTags.viewport = 'present';
297
+ }
298
+ if (report.technical.hasRobotsMeta && report.technical.robotsContent) {
299
+ metaTags.robots = report.technical.robotsContent.join(', ');
300
+ }
301
+
302
+ const openGraph: Record<string, string> = {};
303
+ if (report.openGraph) {
304
+ if (report.openGraph.title) openGraph.title = report.openGraph.title;
305
+ if (report.openGraph.description) openGraph.description = report.openGraph.description;
306
+ if (report.openGraph.image) openGraph.image = report.openGraph.image;
307
+ if (report.openGraph.url) openGraph.url = report.openGraph.url;
308
+ if (report.openGraph.type) openGraph.type = report.openGraph.type;
309
+ if (report.openGraph.siteName) openGraph.site_name = report.openGraph.siteName;
310
+ }
311
+
312
+ const twitterCard: Record<string, string> = {};
313
+ if (report.twitterCard) {
314
+ if (report.twitterCard.card) twitterCard.card = report.twitterCard.card;
315
+ if (report.twitterCard.title) twitterCard.title = report.twitterCard.title;
316
+ if (report.twitterCard.description) twitterCard.description = report.twitterCard.description;
317
+ if (report.twitterCard.image) twitterCard.image = report.twitterCard.image;
318
+ if (report.twitterCard.site) twitterCard.site = report.twitterCard.site;
319
+ }
320
+
321
+ const onPageSEO = this._mapOnPageSEO(report);
322
+ const accessibility = this._mapAccessibility(report);
323
+ const internalLinks = this._mapInternalLinks(report);
324
+ const keywordOptimization = this._mapKeywordOptimization(report);
325
+ const seoScore = this._mapSEOScore(report);
326
+
327
+ return {
328
+ metaTags: Object.keys(metaTags).length > 0 ? metaTags : null,
329
+ openGraph: Object.keys(openGraph).length > 0 ? openGraph : null,
330
+ twitterCard: Object.keys(twitterCard).length > 0 ? twitterCard : null,
331
+ canonical: report.technical.canonicalUrl || null,
332
+ alternates: [],
333
+ assets: null,
334
+ onPageSEO,
335
+ accessibility,
336
+ internalLinks,
337
+ keywordOptimization,
338
+ seoScore
339
+ };
340
+ }
341
+
342
+ private _mapOnPageSEO(report: ReckerSeoReport): OnPageSEOAnalysis | null {
343
+ const recommendations: string[] = [];
344
+
345
+ for (const issue of report.summary.topIssues) {
346
+ recommendations.push(issue.message);
347
+ }
348
+
349
+ for (const win of report.summary.quickWins) {
350
+ recommendations.push(win);
351
+ }
352
+
353
+ const h1Texts = report.headings.structure
354
+ .filter(h => h.level === 1)
355
+ .map(h => ({ text: h.text, quality: 'good' as const }));
356
+
357
+ return {
358
+ title: report.title ? {
359
+ text: report.title.text,
360
+ length: report.title.length,
361
+ hasKeyword: true,
362
+ quality: report.title.length >= 30 && report.title.length <= 60 ? 'optimal' :
363
+ report.title.length < 30 ? 'short' : 'long'
364
+ } : null,
365
+ h1: {
366
+ count: report.headings.h1Count,
367
+ texts: h1Texts,
368
+ issue: report.headings.h1Count !== 1
369
+ ? `Found ${report.headings.h1Count} H1 tags (should be 1)`
370
+ : null
371
+ },
372
+ headingStructure: {
373
+ total: report.headings.structure.reduce((sum, h) => sum + h.count, 0),
374
+ byLevel: report.headings.structure.reduce((acc, h) => {
375
+ acc[`H${h.level}`] = h.count;
376
+ return acc;
377
+ }, {} as Record<string, number>),
378
+ hierarchy: report.headings.hasProperHierarchy ? 'proper' : 'improper',
379
+ issue: report.headings.issues[0] || null
380
+ },
381
+ paragraphs: {
382
+ count: report.content.paragraphCount,
383
+ avgLength: report.content.avgParagraphLength,
384
+ quality: {
385
+ total: report.content.paragraphCount,
386
+ tooShort: 0,
387
+ readability: report.content.fleschReadingEase && report.content.fleschReadingEase >= 60
388
+ ? 'good'
389
+ : 'needs-improvement'
390
+ },
391
+ issue: report.content.paragraphCount < 3 ? 'Limited content - should have more paragraphs' : null
392
+ },
393
+ lists: {
394
+ count: report.content.listCount,
395
+ unordered: report.content.listCount,
396
+ ordered: 0,
397
+ totalItems: 0
398
+ },
399
+ images: {
400
+ count: report.images.total,
401
+ withAlt: report.images.withAlt,
402
+ withoutAlt: report.images.withoutAlt,
403
+ images: report.images.imageFilenames.map((filename, i) => ({
404
+ src: filename,
405
+ alt: report.images.imageAltTexts[i] || null,
406
+ hasAlt: !!report.images.imageAltTexts[i],
407
+ width: null,
408
+ height: null
409
+ }))
410
+ },
411
+ contentMetrics: {
412
+ totalWordCount: report.content.totalWordCount || report.content.wordCount,
413
+ mainContentWordCount: report.content.wordCount,
414
+ contentRatio: report.content.totalWordCount
415
+ ? report.content.wordCount / report.content.totalWordCount
416
+ : 1,
417
+ characterCount: report.content.characterCount,
418
+ quality: report.content.wordCount < 300 ? 'short' :
419
+ report.content.wordCount < 1000 ? 'medium' : 'comprehensive',
420
+ detectedContentContainers: [],
421
+ suggestions: recommendations.slice(0, 3)
422
+ },
423
+ url: report.url,
424
+ recommendations
425
+ };
426
+ }
427
+
428
+ private _mapAccessibility(report: ReckerSeoReport): AccessibilityAnalysis | null {
429
+ const recommendations: string[] = [];
430
+
431
+ const accessibilityChecks = report.checks.filter(c => c.category === 'accessibility');
432
+ for (const check of accessibilityChecks) {
433
+ if (check.status === 'fail' || check.status === 'warn') {
434
+ recommendations.push(check.recommendation || check.message);
435
+ }
436
+ }
437
+
438
+ return {
439
+ langAttribute: {
440
+ present: report.technical.hasLang,
441
+ value: report.technical.langValue || null,
442
+ issue: !report.technical.hasLang ? 'Missing lang attribute on <html>' : null
443
+ },
444
+ headingStructure: {
445
+ count: report.headings.structure.reduce((sum, h) => sum + h.count, 0),
446
+ startsWithH1: report.headings.h1Count > 0,
447
+ properlySorted: report.headings.hasProperHierarchy,
448
+ issue: !report.headings.hasProperHierarchy
449
+ ? 'Heading hierarchy not properly ordered'
450
+ : null
451
+ },
452
+ altText: {
453
+ total: report.images.total,
454
+ withAlt: report.images.withAlt,
455
+ withoutAlt: report.images.withoutAlt,
456
+ percentage: report.images.total > 0
457
+ ? (report.images.withAlt / report.images.total) * 100
458
+ : 100,
459
+ issue: report.images.withoutAlt > 0
460
+ ? `${report.images.withoutAlt} images without alt text`
461
+ : null
462
+ },
463
+ formLabels: {
464
+ inputs: 0,
465
+ labels: 0,
466
+ inputsWithLabels: 0,
467
+ issue: null
468
+ },
469
+ semanticHTML: {
470
+ elements: {},
471
+ score: report.summary.completeness.content,
472
+ issue: null
473
+ },
474
+ contrastRatios: null,
475
+ ariaLabels: {
476
+ total: 0,
477
+ withAriaLabel: 0,
478
+ withRole: 0
479
+ },
480
+ keyboardNavigation: {
481
+ focusableElements: 0,
482
+ withTabindex: 0,
483
+ hasSkipLink: false
484
+ },
485
+ recommendations
486
+ };
487
+ }
488
+
489
+ private _mapInternalLinks(report: ReckerSeoReport): InternalLinksAnalysis | null {
490
+ const recommendations: string[] = [];
491
+
492
+ const linkChecks = report.checks.filter(c => c.category === 'links');
493
+ for (const check of linkChecks) {
494
+ if (check.status === 'fail' || check.status === 'warn') {
495
+ recommendations.push(check.recommendation || check.message);
496
+ }
497
+ }
498
+
499
+ if (report.links.internal < 5) {
500
+ recommendations.push('Add more internal links to create topical clusters and improve SEO');
501
+ }
502
+
503
+ return {
504
+ total: report.links.total,
505
+ sameDomain: {
506
+ count: report.links.internal,
507
+ links: []
508
+ },
509
+ subdomains: {
510
+ count: 0,
511
+ links: [],
512
+ list: []
513
+ },
514
+ external: {
515
+ count: report.links.external,
516
+ links: [],
517
+ domains: {}
518
+ },
519
+ orphaned: 0,
520
+ anchorTextQuality: {
521
+ total: report.links.total,
522
+ descriptive: report.links.total - report.links.withoutText,
523
+ poor: report.links.withoutText,
524
+ examples: []
525
+ },
526
+ topicalClusters: {
527
+ clusters: [],
528
+ strength: [],
529
+ recommendation: report.links.internal < 5
530
+ ? 'Create more internal links to establish topical clusters'
531
+ : null
532
+ },
533
+ recommendations,
534
+ referralAttributes: {
535
+ total: report.links.total,
536
+ nofollow: report.links.nofollow,
537
+ noopener: 0,
538
+ noreferrer: 0,
539
+ sponsored: report.links.sponsoredLinks,
540
+ ugc: report.links.ugcLinks,
541
+ externalAttr: 0,
542
+ targetBlank: 0,
543
+ hasRel: report.links.nofollow + report.links.sponsoredLinks + report.links.ugcLinks,
544
+ followable: report.links.total - report.links.nofollow
545
+ }
546
+ };
547
+ }
548
+
549
+ private _mapKeywordOptimization(report: ReckerSeoReport): KeywordOptimizationAnalysis | null {
550
+ const recommendations: string[] = [];
551
+
552
+ const contentChecks = report.checks.filter(c => c.category === 'content');
553
+ for (const check of contentChecks) {
554
+ if (check.status === 'fail' || check.status === 'warn') {
555
+ recommendations.push(check.recommendation || check.message);
556
+ }
557
+ }
558
+
559
+ const primaryKeyword = report.keywords?.primary || null;
560
+ const titleText = report.title?.text?.toLowerCase() || '';
561
+ const h1Texts = report.headings.structure
562
+ .filter(h => h.level === 1)
563
+ .map(h => h.text.toLowerCase());
564
+
565
+ return {
566
+ primaryKeyword,
567
+ secondaryKeywords: report.keywords?.secondary || [],
568
+ keywordDensity: primaryKeyword && report.keywords?.density?.[primaryKeyword]
569
+ ? (report.keywords.density[primaryKeyword] * 100).toFixed(2)
570
+ : null,
571
+ inTitle: primaryKeyword ? titleText.includes(primaryKeyword.toLowerCase()) : false,
572
+ inH1: primaryKeyword ? h1Texts.some(h1 => h1.includes(primaryKeyword.toLowerCase())) : false,
573
+ inFirstParagraph: false,
574
+ distribution: report.keywords?.density || null,
575
+ recommendations
576
+ };
577
+ }
578
+
579
+ private _mapSEOScore(report: ReckerSeoReport): SEOScore {
580
+ return {
581
+ score: report.score,
582
+ maxScore: 100,
583
+ percentage: report.score.toFixed(1)
584
+ };
585
+ }
586
+
587
+ getReckerGrade(): string | null {
588
+ return null;
589
+ }
590
+
591
+ async getDetailedReport(html: string, baseUrl: string): Promise<ReckerSeoReport | null> {
592
+ const isReckerAvailable = await this._checkReckerAvailability();
593
+
594
+ if (!isReckerAvailable || !this.analyzeSeo) {
595
+ return null;
596
+ }
597
+
598
+ return this.analyzeSeo(html, {
599
+ baseUrl,
600
+ analyzeContent: true
601
+ });
602
+ }
603
+ }
604
+
605
+ export default ReckerSEOAdapter;