@rankcli/agent-runtime 0.0.8 → 0.0.11

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 (49) hide show
  1. package/README.md +90 -196
  2. package/dist/analyzer-GMURJADU.mjs +7 -0
  3. package/dist/chunk-2JADKV3Z.mjs +244 -0
  4. package/dist/chunk-3ZSCLNTW.mjs +557 -0
  5. package/dist/chunk-4E4MQOSP.mjs +374 -0
  6. package/dist/chunk-6BWS3CLP.mjs +16 -0
  7. package/dist/chunk-AK2IC22C.mjs +206 -0
  8. package/dist/chunk-K6VSXDD6.mjs +293 -0
  9. package/dist/chunk-M27NQCWW.mjs +303 -0
  10. package/dist/{chunk-YNZYHEYM.mjs → chunk-PJLNXOLN.mjs} +0 -14
  11. package/dist/chunk-VSQD74I7.mjs +474 -0
  12. package/dist/core-web-vitals-analyzer-TE6LQJMS.mjs +7 -0
  13. package/dist/geo-analyzer-D47LTMMA.mjs +25 -0
  14. package/dist/image-optimization-analyzer-XP4OQGRP.mjs +9 -0
  15. package/dist/index.d.mts +1523 -17
  16. package/dist/index.d.ts +1523 -17
  17. package/dist/index.js +9582 -2664
  18. package/dist/index.mjs +4812 -380
  19. package/dist/internal-linking-analyzer-MRMBV7NM.mjs +9 -0
  20. package/dist/mobile-seo-analyzer-67HNQ7IO.mjs +7 -0
  21. package/dist/security-headers-analyzer-3ZUQARS5.mjs +9 -0
  22. package/dist/structured-data-analyzer-2I4NQAUP.mjs +9 -0
  23. package/package.json +2 -2
  24. package/src/analyzers/core-web-vitals-analyzer.test.ts +236 -0
  25. package/src/analyzers/core-web-vitals-analyzer.ts +557 -0
  26. package/src/analyzers/geo-analyzer.test.ts +310 -0
  27. package/src/analyzers/geo-analyzer.ts +814 -0
  28. package/src/analyzers/image-optimization-analyzer.test.ts +145 -0
  29. package/src/analyzers/image-optimization-analyzer.ts +348 -0
  30. package/src/analyzers/index.ts +233 -0
  31. package/src/analyzers/internal-linking-analyzer.test.ts +141 -0
  32. package/src/analyzers/internal-linking-analyzer.ts +419 -0
  33. package/src/analyzers/mobile-seo-analyzer.test.ts +140 -0
  34. package/src/analyzers/mobile-seo-analyzer.ts +455 -0
  35. package/src/analyzers/security-headers-analyzer.test.ts +115 -0
  36. package/src/analyzers/security-headers-analyzer.ts +318 -0
  37. package/src/analyzers/structured-data-analyzer.test.ts +210 -0
  38. package/src/analyzers/structured-data-analyzer.ts +590 -0
  39. package/src/audit/engine.ts +3 -3
  40. package/src/audit/types.ts +3 -2
  41. package/src/fixer/framework-fixes.test.ts +489 -0
  42. package/src/fixer/framework-fixes.ts +3418 -0
  43. package/src/fixer/index.ts +1 -0
  44. package/src/fixer/schemas.ts +971 -0
  45. package/src/frameworks/detector.ts +642 -114
  46. package/src/frameworks/suggestion-engine.ts +38 -1
  47. package/src/index.ts +6 -0
  48. package/src/types.ts +15 -1
  49. package/dist/analyzer-2CSWIQGD.mjs +0 -6
@@ -0,0 +1,971 @@
1
+ /**
2
+ * World-Class JSON-LD Schema Generators
3
+ *
4
+ * Comprehensive schema.org structured data for all major content types.
5
+ * Follows Google's structured data guidelines for rich results.
6
+ */
7
+
8
+ // ============================================================================
9
+ // ORGANIZATION & BRAND
10
+ // ============================================================================
11
+
12
+ export interface OrganizationSchema {
13
+ name: string;
14
+ url: string;
15
+ logo?: string;
16
+ description?: string;
17
+ foundingDate?: string;
18
+ founders?: { name: string; url?: string }[];
19
+ address?: AddressSchema;
20
+ contactPoint?: ContactPointSchema[];
21
+ sameAs?: string[]; // Social profiles
22
+ }
23
+
24
+ export interface AddressSchema {
25
+ streetAddress: string;
26
+ addressLocality: string; // City
27
+ addressRegion: string; // State
28
+ postalCode: string;
29
+ addressCountry: string;
30
+ }
31
+
32
+ export interface ContactPointSchema {
33
+ telephone: string;
34
+ contactType: 'customer service' | 'technical support' | 'sales' | 'billing';
35
+ availableLanguage?: string[];
36
+ areaServed?: string[];
37
+ }
38
+
39
+ export function generateOrganizationSchema(data: OrganizationSchema) {
40
+ return {
41
+ '@context': 'https://schema.org',
42
+ '@type': 'Organization',
43
+ name: data.name,
44
+ url: data.url,
45
+ logo: data.logo,
46
+ description: data.description,
47
+ foundingDate: data.foundingDate,
48
+ founder: data.founders?.map(f => ({
49
+ '@type': 'Person',
50
+ name: f.name,
51
+ url: f.url,
52
+ })),
53
+ address: data.address ? {
54
+ '@type': 'PostalAddress',
55
+ streetAddress: data.address.streetAddress,
56
+ addressLocality: data.address.addressLocality,
57
+ addressRegion: data.address.addressRegion,
58
+ postalCode: data.address.postalCode,
59
+ addressCountry: data.address.addressCountry,
60
+ } : undefined,
61
+ contactPoint: data.contactPoint?.map(cp => ({
62
+ '@type': 'ContactPoint',
63
+ telephone: cp.telephone,
64
+ contactType: cp.contactType,
65
+ availableLanguage: cp.availableLanguage,
66
+ areaServed: cp.areaServed,
67
+ })),
68
+ sameAs: data.sameAs,
69
+ };
70
+ }
71
+
72
+ // ============================================================================
73
+ // WEBSITE & WEBPAGE
74
+ // ============================================================================
75
+
76
+ export interface WebSiteSchema {
77
+ name: string;
78
+ url: string;
79
+ description?: string;
80
+ publisher?: string;
81
+ potentialAction?: {
82
+ target: string;
83
+ queryInput: string;
84
+ };
85
+ }
86
+
87
+ export function generateWebSiteSchema(data: WebSiteSchema) {
88
+ return {
89
+ '@context': 'https://schema.org',
90
+ '@type': 'WebSite',
91
+ name: data.name,
92
+ url: data.url,
93
+ description: data.description,
94
+ publisher: data.publisher ? {
95
+ '@type': 'Organization',
96
+ name: data.publisher,
97
+ } : undefined,
98
+ potentialAction: data.potentialAction ? {
99
+ '@type': 'SearchAction',
100
+ target: {
101
+ '@type': 'EntryPoint',
102
+ urlTemplate: data.potentialAction.target,
103
+ },
104
+ 'query-input': data.potentialAction.queryInput,
105
+ } : undefined,
106
+ };
107
+ }
108
+
109
+ // ============================================================================
110
+ // ARTICLE & BLOG POST
111
+ // ============================================================================
112
+
113
+ export interface ArticleSchema {
114
+ headline: string;
115
+ description: string;
116
+ image: string | string[];
117
+ datePublished: string;
118
+ dateModified?: string;
119
+ author: PersonSchema | PersonSchema[];
120
+ publisher: {
121
+ name: string;
122
+ logo: string;
123
+ };
124
+ mainEntityOfPage?: string;
125
+ wordCount?: number;
126
+ articleSection?: string;
127
+ keywords?: string[];
128
+ }
129
+
130
+ export interface PersonSchema {
131
+ name: string;
132
+ url?: string;
133
+ image?: string;
134
+ jobTitle?: string;
135
+ sameAs?: string[];
136
+ }
137
+
138
+ export function generateArticleSchema(data: ArticleSchema) {
139
+ const authors = Array.isArray(data.author) ? data.author : [data.author];
140
+
141
+ return {
142
+ '@context': 'https://schema.org',
143
+ '@type': 'Article',
144
+ headline: data.headline,
145
+ description: data.description,
146
+ image: data.image,
147
+ datePublished: data.datePublished,
148
+ dateModified: data.dateModified || data.datePublished,
149
+ author: authors.map(a => ({
150
+ '@type': 'Person',
151
+ name: a.name,
152
+ url: a.url,
153
+ image: a.image,
154
+ jobTitle: a.jobTitle,
155
+ sameAs: a.sameAs,
156
+ })),
157
+ publisher: {
158
+ '@type': 'Organization',
159
+ name: data.publisher.name,
160
+ logo: {
161
+ '@type': 'ImageObject',
162
+ url: data.publisher.logo,
163
+ },
164
+ },
165
+ mainEntityOfPage: data.mainEntityOfPage ? {
166
+ '@type': 'WebPage',
167
+ '@id': data.mainEntityOfPage,
168
+ } : undefined,
169
+ wordCount: data.wordCount,
170
+ articleSection: data.articleSection,
171
+ keywords: data.keywords?.join(', '),
172
+ };
173
+ }
174
+
175
+ export function generateBlogPostingSchema(data: ArticleSchema) {
176
+ const schema = generateArticleSchema(data);
177
+ return { ...schema, '@type': 'BlogPosting' };
178
+ }
179
+
180
+ export function generateNewsArticleSchema(data: ArticleSchema & { dateline?: string }) {
181
+ const schema = generateArticleSchema(data);
182
+ return { ...schema, '@type': 'NewsArticle', dateline: data.dateline };
183
+ }
184
+
185
+ // ============================================================================
186
+ // PRODUCT & E-COMMERCE
187
+ // ============================================================================
188
+
189
+ export interface ProductSchema {
190
+ name: string;
191
+ description: string;
192
+ image: string | string[];
193
+ sku?: string;
194
+ mpn?: string;
195
+ gtin?: string;
196
+ brand?: string;
197
+ offers: OfferSchema | OfferSchema[];
198
+ aggregateRating?: AggregateRatingSchema;
199
+ review?: ReviewSchema[];
200
+ category?: string;
201
+ }
202
+
203
+ export interface OfferSchema {
204
+ price: number | string;
205
+ priceCurrency?: string;
206
+ availability?: 'InStock' | 'OutOfStock' | 'PreOrder' | 'SoldOut' | 'BackOrder';
207
+ priceValidUntil?: string;
208
+ url?: string;
209
+ seller?: string;
210
+ itemCondition?: 'NewCondition' | 'UsedCondition' | 'RefurbishedCondition';
211
+ }
212
+
213
+ export interface AggregateRatingSchema {
214
+ ratingValue: number;
215
+ reviewCount?: number;
216
+ ratingCount?: number;
217
+ bestRating?: number;
218
+ worstRating?: number;
219
+ }
220
+
221
+ export interface ReviewSchema {
222
+ author: string;
223
+ datePublished: string;
224
+ reviewBody: string;
225
+ reviewRating: {
226
+ ratingValue: number;
227
+ bestRating?: number;
228
+ };
229
+ }
230
+
231
+ export function generateProductSchema(data: ProductSchema) {
232
+ const offers = Array.isArray(data.offers) ? data.offers : [data.offers];
233
+
234
+ return {
235
+ '@context': 'https://schema.org',
236
+ '@type': 'Product',
237
+ name: data.name,
238
+ description: data.description,
239
+ image: data.image,
240
+ sku: data.sku,
241
+ mpn: data.mpn,
242
+ gtin: data.gtin,
243
+ brand: data.brand ? {
244
+ '@type': 'Brand',
245
+ name: data.brand,
246
+ } : undefined,
247
+ category: data.category,
248
+ offers: offers.map(o => ({
249
+ '@type': 'Offer',
250
+ price: o.price,
251
+ priceCurrency: o.priceCurrency || 'USD',
252
+ availability: `https://schema.org/${o.availability || 'InStock'}`,
253
+ priceValidUntil: o.priceValidUntil,
254
+ url: o.url,
255
+ seller: o.seller ? {
256
+ '@type': 'Organization',
257
+ name: o.seller,
258
+ } : undefined,
259
+ itemCondition: o.itemCondition ? `https://schema.org/${o.itemCondition}` : undefined,
260
+ })),
261
+ aggregateRating: data.aggregateRating ? {
262
+ '@type': 'AggregateRating',
263
+ ratingValue: data.aggregateRating.ratingValue,
264
+ reviewCount: data.aggregateRating.reviewCount,
265
+ ratingCount: data.aggregateRating.ratingCount,
266
+ bestRating: data.aggregateRating.bestRating || 5,
267
+ worstRating: data.aggregateRating.worstRating || 1,
268
+ } : undefined,
269
+ review: data.review?.map(r => ({
270
+ '@type': 'Review',
271
+ author: {
272
+ '@type': 'Person',
273
+ name: r.author,
274
+ },
275
+ datePublished: r.datePublished,
276
+ reviewBody: r.reviewBody,
277
+ reviewRating: {
278
+ '@type': 'Rating',
279
+ ratingValue: r.reviewRating.ratingValue,
280
+ bestRating: r.reviewRating.bestRating || 5,
281
+ },
282
+ })),
283
+ };
284
+ }
285
+
286
+ // ============================================================================
287
+ // SOFTWARE APPLICATION (SaaS)
288
+ // ============================================================================
289
+
290
+ export interface SoftwareApplicationSchema {
291
+ name: string;
292
+ description: string;
293
+ applicationCategory: 'BusinessApplication' | 'DeveloperApplication' | 'EducationalApplication' | 'GameApplication' | 'LifestyleApplication' | 'MultimediaApplication' | 'SocialNetworkingApplication' | 'UtilitiesApplication';
294
+ operatingSystem?: string;
295
+ offers: OfferSchema | OfferSchema[];
296
+ aggregateRating?: AggregateRatingSchema;
297
+ screenshot?: string | string[];
298
+ featureList?: string[];
299
+ }
300
+
301
+ export function generateSoftwareApplicationSchema(data: SoftwareApplicationSchema) {
302
+ const offers = Array.isArray(data.offers) ? data.offers : [data.offers];
303
+
304
+ return {
305
+ '@context': 'https://schema.org',
306
+ '@type': 'SoftwareApplication',
307
+ name: data.name,
308
+ description: data.description,
309
+ applicationCategory: data.applicationCategory,
310
+ operatingSystem: data.operatingSystem || 'Web',
311
+ offers: offers.map(o => ({
312
+ '@type': 'Offer',
313
+ price: o.price,
314
+ priceCurrency: o.priceCurrency || 'USD',
315
+ })),
316
+ aggregateRating: data.aggregateRating ? {
317
+ '@type': 'AggregateRating',
318
+ ratingValue: data.aggregateRating.ratingValue,
319
+ reviewCount: data.aggregateRating.reviewCount,
320
+ bestRating: data.aggregateRating.bestRating || 5,
321
+ } : undefined,
322
+ screenshot: data.screenshot,
323
+ featureList: data.featureList,
324
+ };
325
+ }
326
+
327
+ // ============================================================================
328
+ // FAQ PAGE
329
+ // ============================================================================
330
+
331
+ export interface FAQSchema {
332
+ questions: { question: string; answer: string }[];
333
+ }
334
+
335
+ export function generateFAQSchema(data: FAQSchema) {
336
+ return {
337
+ '@context': 'https://schema.org',
338
+ '@type': 'FAQPage',
339
+ mainEntity: data.questions.map(q => ({
340
+ '@type': 'Question',
341
+ name: q.question,
342
+ acceptedAnswer: {
343
+ '@type': 'Answer',
344
+ text: q.answer,
345
+ },
346
+ })),
347
+ };
348
+ }
349
+
350
+ // ============================================================================
351
+ // HOW-TO
352
+ // ============================================================================
353
+
354
+ export interface HowToSchema {
355
+ name: string;
356
+ description: string;
357
+ image?: string;
358
+ totalTime?: string; // ISO 8601 duration, e.g., "PT30M"
359
+ estimatedCost?: { currency: string; value: number };
360
+ supply?: string[];
361
+ tool?: string[];
362
+ steps: {
363
+ name: string;
364
+ text: string;
365
+ image?: string;
366
+ url?: string;
367
+ }[];
368
+ }
369
+
370
+ export function generateHowToSchema(data: HowToSchema) {
371
+ return {
372
+ '@context': 'https://schema.org',
373
+ '@type': 'HowTo',
374
+ name: data.name,
375
+ description: data.description,
376
+ image: data.image,
377
+ totalTime: data.totalTime,
378
+ estimatedCost: data.estimatedCost ? {
379
+ '@type': 'MonetaryAmount',
380
+ currency: data.estimatedCost.currency,
381
+ value: data.estimatedCost.value,
382
+ } : undefined,
383
+ supply: data.supply?.map(s => ({
384
+ '@type': 'HowToSupply',
385
+ name: s,
386
+ })),
387
+ tool: data.tool?.map(t => ({
388
+ '@type': 'HowToTool',
389
+ name: t,
390
+ })),
391
+ step: data.steps.map((step, i) => ({
392
+ '@type': 'HowToStep',
393
+ position: i + 1,
394
+ name: step.name,
395
+ text: step.text,
396
+ image: step.image,
397
+ url: step.url,
398
+ })),
399
+ };
400
+ }
401
+
402
+ // ============================================================================
403
+ // VIDEO
404
+ // ============================================================================
405
+
406
+ export interface VideoSchema {
407
+ name: string;
408
+ description: string;
409
+ thumbnailUrl: string | string[];
410
+ uploadDate: string;
411
+ duration?: string; // ISO 8601, e.g., "PT1H30M"
412
+ contentUrl?: string;
413
+ embedUrl?: string;
414
+ interactionStatistic?: {
415
+ watchCount?: number;
416
+ likeCount?: number;
417
+ commentCount?: number;
418
+ };
419
+ publication?: {
420
+ isLiveBroadcast: boolean;
421
+ startDate?: string;
422
+ endDate?: string;
423
+ };
424
+ }
425
+
426
+ export function generateVideoSchema(data: VideoSchema) {
427
+ const stats = data.interactionStatistic;
428
+
429
+ return {
430
+ '@context': 'https://schema.org',
431
+ '@type': 'VideoObject',
432
+ name: data.name,
433
+ description: data.description,
434
+ thumbnailUrl: data.thumbnailUrl,
435
+ uploadDate: data.uploadDate,
436
+ duration: data.duration,
437
+ contentUrl: data.contentUrl,
438
+ embedUrl: data.embedUrl,
439
+ interactionStatistic: stats ? [
440
+ stats.watchCount ? {
441
+ '@type': 'InteractionCounter',
442
+ interactionType: { '@type': 'WatchAction' },
443
+ userInteractionCount: stats.watchCount,
444
+ } : null,
445
+ stats.likeCount ? {
446
+ '@type': 'InteractionCounter',
447
+ interactionType: { '@type': 'LikeAction' },
448
+ userInteractionCount: stats.likeCount,
449
+ } : null,
450
+ stats.commentCount ? {
451
+ '@type': 'InteractionCounter',
452
+ interactionType: { '@type': 'CommentAction' },
453
+ userInteractionCount: stats.commentCount,
454
+ } : null,
455
+ ].filter(Boolean) : undefined,
456
+ publication: data.publication ? {
457
+ '@type': 'BroadcastEvent',
458
+ isLiveBroadcast: data.publication.isLiveBroadcast,
459
+ startDate: data.publication.startDate,
460
+ endDate: data.publication.endDate,
461
+ } : undefined,
462
+ };
463
+ }
464
+
465
+ // ============================================================================
466
+ // EVENT
467
+ // ============================================================================
468
+
469
+ export interface EventSchema {
470
+ name: string;
471
+ description: string;
472
+ startDate: string;
473
+ endDate?: string;
474
+ location: {
475
+ type: 'Place' | 'VirtualLocation';
476
+ name?: string;
477
+ address?: AddressSchema;
478
+ url?: string; // For virtual events
479
+ };
480
+ image?: string | string[];
481
+ performer?: { name: string; url?: string }[];
482
+ organizer?: { name: string; url?: string };
483
+ offers?: OfferSchema[];
484
+ eventStatus?: 'EventScheduled' | 'EventCancelled' | 'EventPostponed' | 'EventRescheduled';
485
+ eventAttendanceMode?: 'OfflineEventAttendanceMode' | 'OnlineEventAttendanceMode' | 'MixedEventAttendanceMode';
486
+ }
487
+
488
+ export function generateEventSchema(data: EventSchema) {
489
+ return {
490
+ '@context': 'https://schema.org',
491
+ '@type': 'Event',
492
+ name: data.name,
493
+ description: data.description,
494
+ startDate: data.startDate,
495
+ endDate: data.endDate,
496
+ image: data.image,
497
+ eventStatus: data.eventStatus ? `https://schema.org/${data.eventStatus}` : undefined,
498
+ eventAttendanceMode: data.eventAttendanceMode ? `https://schema.org/${data.eventAttendanceMode}` : undefined,
499
+ location: data.location.type === 'VirtualLocation' ? {
500
+ '@type': 'VirtualLocation',
501
+ url: data.location.url,
502
+ } : {
503
+ '@type': 'Place',
504
+ name: data.location.name,
505
+ address: data.location.address ? {
506
+ '@type': 'PostalAddress',
507
+ ...data.location.address,
508
+ } : undefined,
509
+ },
510
+ performer: data.performer?.map(p => ({
511
+ '@type': 'Person',
512
+ name: p.name,
513
+ url: p.url,
514
+ })),
515
+ organizer: data.organizer ? {
516
+ '@type': 'Organization',
517
+ name: data.organizer.name,
518
+ url: data.organizer.url,
519
+ } : undefined,
520
+ offers: data.offers?.map(o => ({
521
+ '@type': 'Offer',
522
+ price: o.price,
523
+ priceCurrency: o.priceCurrency || 'USD',
524
+ availability: `https://schema.org/${o.availability || 'InStock'}`,
525
+ url: o.url,
526
+ validFrom: o.priceValidUntil,
527
+ })),
528
+ };
529
+ }
530
+
531
+ // ============================================================================
532
+ // LOCAL BUSINESS
533
+ // ============================================================================
534
+
535
+ export interface LocalBusinessSchema {
536
+ name: string;
537
+ description: string;
538
+ url: string;
539
+ telephone: string;
540
+ address: AddressSchema;
541
+ geo?: { latitude: number; longitude: number };
542
+ openingHours?: string[]; // e.g., ["Mo-Fr 09:00-17:00", "Sa 10:00-14:00"]
543
+ priceRange?: string; // e.g., "$$"
544
+ image?: string | string[];
545
+ servesCuisine?: string[]; // For restaurants
546
+ menu?: string; // URL to menu
547
+ aggregateRating?: AggregateRatingSchema;
548
+ review?: ReviewSchema[];
549
+ }
550
+
551
+ export function generateLocalBusinessSchema(data: LocalBusinessSchema) {
552
+ return {
553
+ '@context': 'https://schema.org',
554
+ '@type': 'LocalBusiness',
555
+ name: data.name,
556
+ description: data.description,
557
+ url: data.url,
558
+ telephone: data.telephone,
559
+ image: data.image,
560
+ priceRange: data.priceRange,
561
+ servesCuisine: data.servesCuisine,
562
+ menu: data.menu,
563
+ address: {
564
+ '@type': 'PostalAddress',
565
+ streetAddress: data.address.streetAddress,
566
+ addressLocality: data.address.addressLocality,
567
+ addressRegion: data.address.addressRegion,
568
+ postalCode: data.address.postalCode,
569
+ addressCountry: data.address.addressCountry,
570
+ },
571
+ geo: data.geo ? {
572
+ '@type': 'GeoCoordinates',
573
+ latitude: data.geo.latitude,
574
+ longitude: data.geo.longitude,
575
+ } : undefined,
576
+ openingHoursSpecification: data.openingHours?.map(parseOpeningHours).filter(Boolean),
577
+ aggregateRating: data.aggregateRating ? {
578
+ '@type': 'AggregateRating',
579
+ ratingValue: data.aggregateRating.ratingValue,
580
+ reviewCount: data.aggregateRating.reviewCount,
581
+ bestRating: data.aggregateRating.bestRating || 5,
582
+ } : undefined,
583
+ review: data.review?.map(r => ({
584
+ '@type': 'Review',
585
+ author: { '@type': 'Person', name: r.author },
586
+ datePublished: r.datePublished,
587
+ reviewBody: r.reviewBody,
588
+ reviewRating: {
589
+ '@type': 'Rating',
590
+ ratingValue: r.reviewRating.ratingValue,
591
+ },
592
+ })),
593
+ };
594
+ }
595
+
596
+ function parseOpeningHours(hours: string) {
597
+ // Parse "Mo-Fr 09:00-17:00" format
598
+ const match = hours.match(/^([A-Za-z,-]+)\s+(\d{2}:\d{2})-(\d{2}:\d{2})$/);
599
+ if (!match) return null;
600
+
601
+ const dayMap: Record<string, string> = {
602
+ 'Mo': 'Monday', 'Tu': 'Tuesday', 'We': 'Wednesday',
603
+ 'Th': 'Thursday', 'Fr': 'Friday', 'Sa': 'Saturday', 'Su': 'Sunday',
604
+ };
605
+
606
+ const dayPart = match[1];
607
+ const opens = match[2];
608
+ const closes = match[3];
609
+
610
+ // Handle ranges like "Mo-Fr"
611
+ if (dayPart.includes('-')) {
612
+ const [start, end] = dayPart.split('-');
613
+ const days = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
614
+ const startIdx = days.indexOf(start);
615
+ const endIdx = days.indexOf(end);
616
+ const dayOfWeek = days.slice(startIdx, endIdx + 1).map(d => dayMap[d]);
617
+ return { '@type': 'OpeningHoursSpecification', dayOfWeek, opens, closes };
618
+ }
619
+
620
+ return {
621
+ '@type': 'OpeningHoursSpecification',
622
+ dayOfWeek: dayPart.split(',').map(d => dayMap[d.trim()]),
623
+ opens,
624
+ closes,
625
+ };
626
+ }
627
+
628
+ // ============================================================================
629
+ // BREADCRUMB
630
+ // ============================================================================
631
+
632
+ export interface BreadcrumbSchema {
633
+ items: { name: string; url: string }[];
634
+ }
635
+
636
+ export function generateBreadcrumbSchema(data: BreadcrumbSchema) {
637
+ return {
638
+ '@context': 'https://schema.org',
639
+ '@type': 'BreadcrumbList',
640
+ itemListElement: data.items.map((item, index) => ({
641
+ '@type': 'ListItem',
642
+ position: index + 1,
643
+ name: item.name,
644
+ item: item.url,
645
+ })),
646
+ };
647
+ }
648
+
649
+ // ============================================================================
650
+ // JOB POSTING
651
+ // ============================================================================
652
+
653
+ export interface JobPostingSchema {
654
+ title: string;
655
+ description: string;
656
+ datePosted: string;
657
+ validThrough?: string;
658
+ employmentType?: ('FULL_TIME' | 'PART_TIME' | 'CONTRACTOR' | 'TEMPORARY' | 'INTERN')[];
659
+ hiringOrganization: {
660
+ name: string;
661
+ url?: string;
662
+ logo?: string;
663
+ };
664
+ jobLocation?: {
665
+ address: AddressSchema;
666
+ };
667
+ jobLocationType?: 'TELECOMMUTE';
668
+ baseSalary?: {
669
+ value: { minValue: number; maxValue: number } | number;
670
+ currency: string;
671
+ unitText: 'HOUR' | 'DAY' | 'WEEK' | 'MONTH' | 'YEAR';
672
+ };
673
+ experienceRequirements?: string;
674
+ educationRequirements?: string;
675
+ skills?: string[];
676
+ }
677
+
678
+ export function generateJobPostingSchema(data: JobPostingSchema) {
679
+ return {
680
+ '@context': 'https://schema.org',
681
+ '@type': 'JobPosting',
682
+ title: data.title,
683
+ description: data.description,
684
+ datePosted: data.datePosted,
685
+ validThrough: data.validThrough,
686
+ employmentType: data.employmentType,
687
+ hiringOrganization: {
688
+ '@type': 'Organization',
689
+ name: data.hiringOrganization.name,
690
+ sameAs: data.hiringOrganization.url,
691
+ logo: data.hiringOrganization.logo,
692
+ },
693
+ jobLocation: data.jobLocation ? {
694
+ '@type': 'Place',
695
+ address: {
696
+ '@type': 'PostalAddress',
697
+ ...data.jobLocation.address,
698
+ },
699
+ } : undefined,
700
+ jobLocationType: data.jobLocationType,
701
+ baseSalary: data.baseSalary ? {
702
+ '@type': 'MonetaryAmount',
703
+ currency: data.baseSalary.currency,
704
+ value: typeof data.baseSalary.value === 'number' ? {
705
+ '@type': 'QuantitativeValue',
706
+ value: data.baseSalary.value,
707
+ unitText: data.baseSalary.unitText,
708
+ } : {
709
+ '@type': 'QuantitativeValue',
710
+ minValue: data.baseSalary.value.minValue,
711
+ maxValue: data.baseSalary.value.maxValue,
712
+ unitText: data.baseSalary.unitText,
713
+ },
714
+ } : undefined,
715
+ experienceRequirements: data.experienceRequirements,
716
+ educationRequirements: data.educationRequirements ? {
717
+ '@type': 'EducationalOccupationalCredential',
718
+ credentialCategory: data.educationRequirements,
719
+ } : undefined,
720
+ skills: data.skills,
721
+ };
722
+ }
723
+
724
+ // ============================================================================
725
+ // COURSE
726
+ // ============================================================================
727
+
728
+ export interface CourseSchema {
729
+ name: string;
730
+ description: string;
731
+ provider: {
732
+ name: string;
733
+ url?: string;
734
+ };
735
+ offers?: OfferSchema;
736
+ coursePrerequisites?: string[];
737
+ educationalLevel?: string;
738
+ timeRequired?: string; // ISO 8601 duration
739
+ image?: string;
740
+ aggregateRating?: AggregateRatingSchema;
741
+ }
742
+
743
+ export function generateCourseSchema(data: CourseSchema) {
744
+ return {
745
+ '@context': 'https://schema.org',
746
+ '@type': 'Course',
747
+ name: data.name,
748
+ description: data.description,
749
+ provider: {
750
+ '@type': 'Organization',
751
+ name: data.provider.name,
752
+ sameAs: data.provider.url,
753
+ },
754
+ offers: data.offers ? {
755
+ '@type': 'Offer',
756
+ price: data.offers.price,
757
+ priceCurrency: data.offers.priceCurrency || 'USD',
758
+ availability: `https://schema.org/${data.offers.availability || 'InStock'}`,
759
+ } : undefined,
760
+ coursePrerequisites: data.coursePrerequisites,
761
+ educationalLevel: data.educationalLevel,
762
+ timeRequired: data.timeRequired,
763
+ image: data.image,
764
+ aggregateRating: data.aggregateRating ? {
765
+ '@type': 'AggregateRating',
766
+ ratingValue: data.aggregateRating.ratingValue,
767
+ reviewCount: data.aggregateRating.reviewCount,
768
+ } : undefined,
769
+ };
770
+ }
771
+
772
+ // ============================================================================
773
+ // RECIPE
774
+ // ============================================================================
775
+
776
+ export interface RecipeSchema {
777
+ name: string;
778
+ description: string;
779
+ image: string | string[];
780
+ author: { name: string; url?: string };
781
+ datePublished: string;
782
+ prepTime?: string; // ISO 8601 duration
783
+ cookTime?: string;
784
+ totalTime?: string;
785
+ recipeYield?: string; // e.g., "4 servings"
786
+ recipeCategory?: string; // e.g., "Dessert"
787
+ recipeCuisine?: string; // e.g., "Italian"
788
+ recipeIngredient: string[];
789
+ recipeInstructions: { name?: string; text: string }[];
790
+ nutrition?: {
791
+ calories?: string;
792
+ fatContent?: string;
793
+ proteinContent?: string;
794
+ carbohydrateContent?: string;
795
+ };
796
+ aggregateRating?: AggregateRatingSchema;
797
+ video?: VideoSchema;
798
+ }
799
+
800
+ export function generateRecipeSchema(data: RecipeSchema) {
801
+ return {
802
+ '@context': 'https://schema.org',
803
+ '@type': 'Recipe',
804
+ name: data.name,
805
+ description: data.description,
806
+ image: data.image,
807
+ author: {
808
+ '@type': 'Person',
809
+ name: data.author.name,
810
+ url: data.author.url,
811
+ },
812
+ datePublished: data.datePublished,
813
+ prepTime: data.prepTime,
814
+ cookTime: data.cookTime,
815
+ totalTime: data.totalTime,
816
+ recipeYield: data.recipeYield,
817
+ recipeCategory: data.recipeCategory,
818
+ recipeCuisine: data.recipeCuisine,
819
+ recipeIngredient: data.recipeIngredient,
820
+ recipeInstructions: data.recipeInstructions.map((step, i) => ({
821
+ '@type': 'HowToStep',
822
+ position: i + 1,
823
+ name: step.name,
824
+ text: step.text,
825
+ })),
826
+ nutrition: data.nutrition ? {
827
+ '@type': 'NutritionInformation',
828
+ calories: data.nutrition.calories,
829
+ fatContent: data.nutrition.fatContent,
830
+ proteinContent: data.nutrition.proteinContent,
831
+ carbohydrateContent: data.nutrition.carbohydrateContent,
832
+ } : undefined,
833
+ aggregateRating: data.aggregateRating ? {
834
+ '@type': 'AggregateRating',
835
+ ratingValue: data.aggregateRating.ratingValue,
836
+ reviewCount: data.aggregateRating.reviewCount,
837
+ } : undefined,
838
+ video: data.video ? generateVideoSchema(data.video) : undefined,
839
+ };
840
+ }
841
+
842
+ // ============================================================================
843
+ // BOOK
844
+ // ============================================================================
845
+
846
+ export interface BookSchema {
847
+ name: string;
848
+ description: string;
849
+ author: { name: string; url?: string } | { name: string; url?: string }[];
850
+ isbn?: string;
851
+ numberOfPages?: number;
852
+ bookFormat?: 'Hardcover' | 'Paperback' | 'EBook' | 'AudioBook';
853
+ publisher?: string;
854
+ datePublished?: string;
855
+ image?: string;
856
+ inLanguage?: string;
857
+ aggregateRating?: AggregateRatingSchema;
858
+ offers?: OfferSchema;
859
+ }
860
+
861
+ export function generateBookSchema(data: BookSchema) {
862
+ const authors = Array.isArray(data.author) ? data.author : [data.author];
863
+
864
+ return {
865
+ '@context': 'https://schema.org',
866
+ '@type': 'Book',
867
+ name: data.name,
868
+ description: data.description,
869
+ author: authors.map(a => ({
870
+ '@type': 'Person',
871
+ name: a.name,
872
+ url: a.url,
873
+ })),
874
+ isbn: data.isbn,
875
+ numberOfPages: data.numberOfPages,
876
+ bookFormat: data.bookFormat ? `https://schema.org/${data.bookFormat}` : undefined,
877
+ publisher: data.publisher ? {
878
+ '@type': 'Organization',
879
+ name: data.publisher,
880
+ } : undefined,
881
+ datePublished: data.datePublished,
882
+ image: data.image,
883
+ inLanguage: data.inLanguage,
884
+ aggregateRating: data.aggregateRating ? {
885
+ '@type': 'AggregateRating',
886
+ ratingValue: data.aggregateRating.ratingValue,
887
+ reviewCount: data.aggregateRating.reviewCount,
888
+ } : undefined,
889
+ offers: data.offers ? {
890
+ '@type': 'Offer',
891
+ price: data.offers.price,
892
+ priceCurrency: data.offers.priceCurrency || 'USD',
893
+ availability: `https://schema.org/${data.offers.availability || 'InStock'}`,
894
+ } : undefined,
895
+ };
896
+ }
897
+
898
+ // ============================================================================
899
+ // SPEAKABLE (for Google Assistant)
900
+ // ============================================================================
901
+
902
+ export interface SpeakableSchema {
903
+ url: string;
904
+ cssSelector?: string[];
905
+ xpath?: string[];
906
+ }
907
+
908
+ export function generateSpeakableSchema(data: SpeakableSchema) {
909
+ return {
910
+ '@context': 'https://schema.org',
911
+ '@type': 'WebPage',
912
+ speakable: {
913
+ '@type': 'SpeakableSpecification',
914
+ cssSelector: data.cssSelector,
915
+ xpath: data.xpath,
916
+ },
917
+ url: data.url,
918
+ };
919
+ }
920
+
921
+ // ============================================================================
922
+ // SITELINKS SEARCHBOX
923
+ // ============================================================================
924
+
925
+ export interface SitelinksSearchboxSchema {
926
+ url: string;
927
+ searchUrl: string;
928
+ queryInput?: string;
929
+ }
930
+
931
+ export function generateSitelinksSearchboxSchema(data: SitelinksSearchboxSchema) {
932
+ return {
933
+ '@context': 'https://schema.org',
934
+ '@type': 'WebSite',
935
+ url: data.url,
936
+ potentialAction: {
937
+ '@type': 'SearchAction',
938
+ target: {
939
+ '@type': 'EntryPoint',
940
+ urlTemplate: data.searchUrl,
941
+ },
942
+ 'query-input': data.queryInput || 'required name=search_term_string',
943
+ },
944
+ };
945
+ }
946
+
947
+ // ============================================================================
948
+ // ALL SCHEMAS EXPORT
949
+ // ============================================================================
950
+
951
+ export const Schemas = {
952
+ organization: generateOrganizationSchema,
953
+ webSite: generateWebSiteSchema,
954
+ article: generateArticleSchema,
955
+ blogPosting: generateBlogPostingSchema,
956
+ newsArticle: generateNewsArticleSchema,
957
+ product: generateProductSchema,
958
+ softwareApplication: generateSoftwareApplicationSchema,
959
+ faq: generateFAQSchema,
960
+ howTo: generateHowToSchema,
961
+ video: generateVideoSchema,
962
+ event: generateEventSchema,
963
+ localBusiness: generateLocalBusinessSchema,
964
+ breadcrumb: generateBreadcrumbSchema,
965
+ jobPosting: generateJobPostingSchema,
966
+ course: generateCourseSchema,
967
+ recipe: generateRecipeSchema,
968
+ book: generateBookSchema,
969
+ speakable: generateSpeakableSchema,
970
+ sitelinksSearchbox: generateSitelinksSearchboxSchema,
971
+ };