@lynxflow/seo-engine 1.8.27 → 1.8.29

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.
@@ -91,6 +91,52 @@ describe("Extended Schema.org Builder", () => {
91
91
  expect(schema["@type"]).toBe("Organization");
92
92
  expect(schema.sameAs).toContain("https://wikidata.org/wiki/Q123");
93
93
  });
94
+
95
+ it("Builds complete Speakable schema for voice search & assistant", () => {
96
+ const speakable = ExtendedSchemaGraphBuilder.buildSpeakable({
97
+ cssSelectors: [".headline", ".direct-answer"],
98
+ });
99
+ expect(speakable["@type"]).toBe("SpeakableSpecification");
100
+ expect(speakable.cssSelector).toEqual([".headline", ".direct-answer"]);
101
+ });
102
+
103
+ it("Builds complete PodcastEpisode and Restaurant schemas", () => {
104
+ const podcast = ExtendedSchemaGraphBuilder.buildPodcastEpisode({
105
+ title: "SEO Trends 2026",
106
+ description: "Deep dive into GEO",
107
+ audioUrl: "https://example.com/ep1.mp3",
108
+ publishedDate: "2026-08-28",
109
+ seriesName: "LynxCast",
110
+ });
111
+ expect(podcast["@type"]).toBe("PodcastEpisode");
112
+ expect(podcast.name).toBe("SEO Trends 2026");
113
+
114
+ const restaurant = ExtendedSchemaGraphBuilder.buildRestaurant({
115
+ name: "Le Gourmet",
116
+ url: "https://legourmet.fr",
117
+ servesCuisine: ["French", "Mediterranean"],
118
+ address: { street: "12 Rue de Paris", city: "Paris", postalCode: "75001", country: "FR" },
119
+ });
120
+ expect(restaurant["@type"]).toBe("Restaurant");
121
+ expect(restaurant.servesCuisine).toContain("French");
122
+ });
123
+
124
+ it("Builds Book, Dataset, FactCheck, Carousel, and QAPage schemas", () => {
125
+ const book = ExtendedSchemaGraphBuilder.buildBook({ name: "SEO in 2026", author: "LynxFlow" });
126
+ expect(book["@type"]).toBe("Book");
127
+
128
+ const dataset = ExtendedSchemaGraphBuilder.buildDataset({ name: "Search Data", description: "Keywords dataset", url: "https://data.com", keywords: ["seo", "ai"] });
129
+ expect(dataset["@type"]).toBe("Dataset");
130
+
131
+ const factCheck = ExtendedSchemaGraphBuilder.buildFactCheck({ claimReviewed: "AI replaces search", ratingValue: "4", authorName: "LynxFact", url: "https://fact.com" });
132
+ expect(factCheck["@type"]).toBe("ClaimReview");
133
+
134
+ const carousel = ExtendedSchemaGraphBuilder.buildCarousel([{ name: "Item 1", url: "https://item1.com", position: 1 }]);
135
+ expect(carousel["@type"]).toBe("ItemList");
136
+
137
+ const qa = ExtendedSchemaGraphBuilder.buildQAPage({ questionText: "How to rank #1?", authorName: "User1", answerText: "Use LynxSEO.", answerAuthor: "LynxBot" });
138
+ expect(qa["@type"]).toBe("QAPage");
139
+ });
94
140
  });
95
141
 
96
142
  describe("Master Programmatic Matrix Engine (English Default)", () => {
@@ -378,4 +378,212 @@ export class ExtendedSchemaGraphBuilder {
378
378
  : undefined,
379
379
  };
380
380
  }
381
+
382
+ /**
383
+ * 11. Speakable Schema (Google Assistant & Voice Search) - Rank Math Pro Parity
384
+ */
385
+ static buildSpeakable(opts: {
386
+ cssSelectors?: string[];
387
+ xpaths?: string[];
388
+ }): Record<string, unknown> {
389
+ return {
390
+ "@type": "SpeakableSpecification",
391
+ cssSelector: opts.cssSelectors || [".headline", ".summary", ".direct-answer", "article h1", "article p:first-of-type"],
392
+ xpath: opts.xpaths,
393
+ };
394
+ }
395
+
396
+ /**
397
+ * 12. Podcast & PodcastEpisode Schema
398
+ */
399
+ static buildPodcastEpisode(opts: {
400
+ title: string;
401
+ description: string;
402
+ audioUrl: string;
403
+ duration?: string;
404
+ publishedDate: string;
405
+ seriesName: string;
406
+ }): Record<string, unknown> {
407
+ return {
408
+ "@context": "https://schema.org",
409
+ "@type": "PodcastEpisode",
410
+ name: opts.title,
411
+ description: opts.description,
412
+ datePublished: opts.publishedDate,
413
+ timeRequired: opts.duration || "PT15M",
414
+ associatedMedia: {
415
+ "@type": "AudioObject",
416
+ contentUrl: opts.audioUrl,
417
+ },
418
+ partOfSeries: {
419
+ "@type": "PodcastSeries",
420
+ name: opts.seriesName,
421
+ },
422
+ };
423
+ }
424
+
425
+ /**
426
+ * 13. Restaurant Schema (with Menu & Cuisine)
427
+ */
428
+ static buildRestaurant(opts: {
429
+ name: string;
430
+ url: string;
431
+ servesCuisine: string[];
432
+ menuUrl?: string;
433
+ priceRange?: string;
434
+ telephone?: string;
435
+ address: { street: string; city: string; postalCode: string; country: string };
436
+ }): Record<string, unknown> {
437
+ return {
438
+ "@context": "https://schema.org",
439
+ "@type": "Restaurant",
440
+ name: opts.name,
441
+ url: opts.url,
442
+ servesCuisine: opts.servesCuisine,
443
+ hasMenu: opts.menuUrl,
444
+ priceRange: opts.priceRange || "€€",
445
+ telephone: opts.telephone,
446
+ address: {
447
+ "@type": "PostalAddress",
448
+ streetAddress: opts.address.street,
449
+ addressLocality: opts.address.city,
450
+ postalCode: opts.address.postalCode,
451
+ addressCountry: opts.address.country,
452
+ },
453
+ };
454
+ }
455
+
456
+ /**
457
+ * 14. Book Schema
458
+ */
459
+ static buildBook(opts: {
460
+ name: string;
461
+ author: string;
462
+ isbn?: string;
463
+ bookFormat?: string;
464
+ url?: string;
465
+ }): Record<string, unknown> {
466
+ return {
467
+ "@context": "https://schema.org",
468
+ "@type": "Book",
469
+ name: opts.name,
470
+ author: {
471
+ "@type": "Person",
472
+ name: opts.author,
473
+ },
474
+ isbn: opts.isbn,
475
+ bookFormat: opts.bookFormat || "https://schema.org/EBook",
476
+ url: opts.url,
477
+ };
478
+ }
479
+
480
+ /**
481
+ * 15. Dataset Schema (Data Search & Research)
482
+ */
483
+ static buildDataset(opts: {
484
+ name: string;
485
+ description: string;
486
+ url: string;
487
+ keywords: string[];
488
+ temporalCoverage?: string;
489
+ distributionUrl?: string;
490
+ }): Record<string, unknown> {
491
+ return {
492
+ "@context": "https://schema.org",
493
+ "@type": "Dataset",
494
+ name: opts.name,
495
+ description: opts.description,
496
+ url: opts.url,
497
+ keywords: opts.keywords,
498
+ temporalCoverage: opts.temporalCoverage || "2024/2026",
499
+ distribution: opts.distributionUrl
500
+ ? [
501
+ {
502
+ "@type": "DataDownload",
503
+ encodingFormat: "application/json",
504
+ contentUrl: opts.distributionUrl,
505
+ },
506
+ ]
507
+ : undefined,
508
+ };
509
+ }
510
+
511
+ /**
512
+ * 16. FactCheck Schema (Google ClaimReview)
513
+ */
514
+ static buildFactCheck(opts: {
515
+ claimReviewed: string;
516
+ ratingValue: string;
517
+ authorName: string;
518
+ url: string;
519
+ }): Record<string, unknown> {
520
+ return {
521
+ "@context": "https://schema.org",
522
+ "@type": "ClaimReview",
523
+ claimReviewed: opts.claimReviewed,
524
+ url: opts.url,
525
+ reviewRating: {
526
+ "@type": "Rating",
527
+ ratingValue: opts.ratingValue,
528
+ bestRating: "5",
529
+ worstRating: "1",
530
+ },
531
+ author: {
532
+ "@type": "Organization",
533
+ name: opts.authorName,
534
+ },
535
+ };
536
+ }
537
+
538
+ /**
539
+ * 17. Carousel / ItemList Schema
540
+ */
541
+ static buildCarousel(items: Array<{ name: string; url: string; position: number; image?: string }>): Record<string, unknown> {
542
+ return {
543
+ "@context": "https://schema.org",
544
+ "@type": "ItemList",
545
+ itemListElement: items.map((item) => ({
546
+ "@type": "ListItem",
547
+ position: item.position,
548
+ url: item.url,
549
+ name: item.name,
550
+ image: item.image,
551
+ })),
552
+ };
553
+ }
554
+
555
+ /**
556
+ * 18. QAPage Schema (bbPress / Forums / Community QA)
557
+ */
558
+ static buildQAPage(opts: {
559
+ questionText: string;
560
+ authorName: string;
561
+ answerText: string;
562
+ answerAuthor: string;
563
+ upvoteCount?: number;
564
+ }): Record<string, unknown> {
565
+ return {
566
+ "@context": "https://schema.org",
567
+ "@type": "QAPage",
568
+ mainEntity: {
569
+ "@type": "Question",
570
+ name: opts.questionText,
571
+ text: opts.questionText,
572
+ author: {
573
+ "@type": "Person",
574
+ name: opts.authorName,
575
+ },
576
+ answerCount: 1,
577
+ acceptedAnswer: {
578
+ "@type": "Answer",
579
+ text: opts.answerText,
580
+ upvoteCount: opts.upvoteCount ?? 12,
581
+ author: {
582
+ "@type": "Person",
583
+ name: opts.answerAuthor,
584
+ },
585
+ },
586
+ },
587
+ };
588
+ }
381
589
  }