@lynxflow/seo-engine 1.8.28 → 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.
@@ -70,6 +70,7 @@
70
70
  { id: 'bulk_edit', label: '✏️ Bulk Edit Titres & Metas', icon: '📝', group: 'SEO' },
71
71
  { id: 'orphan_pages', label: '🔎 Détecteur Pages Orphelines', icon: '🕵️', group: 'SEO' },
72
72
  { id: 'pillar_pages', label: '🏛️ Pillar Pages & Cornerstone', icon: '⭐', group: 'SEO' },
73
+ { id: 'workouts', label: '🏋️ SEO Workouts Guidés (3 Étapes)', icon: '🏃', group: 'SEO' },
73
74
  // ── LINKS ─────────────────────────────────
74
75
  { id: 'link_assistant', label: '🔗 Link Assistant & Liens Brisés', icon: '🕵️', group: 'LINKS' },
75
76
  { id: 'redirects', label: '🔀 Redirections (301/302/307/410/451)', icon: '🔄', group: 'LINKS' },
@@ -2532,7 +2533,68 @@
2532
2533
  el('div', { style: { display: 'flex', gap: '6px' } },
2533
2534
  el('button', { onClick: function() { alert('📊 Dashboard client ouvert...'); }, style: { flex: 1, background: '#eff6ff', color: '#1d4ed8', border: 0, padding: '6px 10px', borderRadius: '6px', fontSize: '11px', fontWeight: '700', cursor: 'pointer' } }, '📊 Dashboard'),
2534
2535
  el('button', { onClick: function() { alert('📧 Rapport envoyé au client !'); }, style: { flex: 1, background: '#f0fdf4', color: '#166534', border: 0, padding: '6px 10px', borderRadius: '6px', fontSize: '11px', fontWeight: '700', cursor: 'pointer' } }, '📧 Rapport')
2535
- )
2536
+ )
2537
+ ),
2538
+
2539
+ // ══════════════════════════════════════════════════════════
2540
+ // 🏋️ SEO WORKOUTS GUIDÉS — Yoast SEO Premium Parity
2541
+ // ══════════════════════════════════════════════════════════
2542
+ activeTab === 'workouts' && el('div', null,
2543
+ el('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px', borderBottom: '1px solid #e2e8f0', paddingBottom: '16px' } },
2544
+ el('div', null,
2545
+ el('h1', { style: { fontSize: '24px', fontWeight: '800', color: '#0f172a', margin: '0 0 4px' } }, '🏋️ SEO Workouts Guidés — Routines d\'Optimisation Étape par Étape'),
2546
+ el('p', { style: { color: '#64748b', fontSize: '13px', margin: 0 } }, 'Des routines d\'entraînement SEO pas à pas pour booster votre classement. Parité Yoast Premium Workouts.')
2547
+ ),
2548
+ el('span', { style: { background: '#dcfce7', color: '#166534', fontSize: '12px', fontWeight: '700', padding: '6px 14px', borderRadius: '999px' } }, '🎯 2/3 Complétés')
2549
+ ),
2550
+ el('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: '18px' } },
2551
+ [
2552
+ {
2553
+ title: '⭐ Workout 1 : Contenu Pilier (Cornerstone)',
2554
+ desc: 'Identifiez vos 5 articles phares et créez un maillage interne pyramidal vers eux.',
2555
+ status: 'Complété',
2556
+ progress: 100,
2557
+ color: '#16a34a',
2558
+ steps: ['Désigner 5 pages piliers', 'Créer 15 liens entrants', 'Vérifier la densité sémantique']
2559
+ },
2560
+ {
2561
+ title: '🔎 Workout 2 : Nettoyage Pages Orphelines',
2562
+ desc: 'Détectez et connectez toutes les pages sans liens internes pour récupérer du jus SEO.',
2563
+ status: 'En cours',
2564
+ progress: 65,
2565
+ color: '#2563eb',
2566
+ steps: ['Scanner les 7 pages orphelines', 'Ajouter liens depuis catégories', 'Supprimer les 404']
2567
+ },
2568
+ {
2569
+ title: '🔄 Workout 3 : Rafraîchissement Contenu Obsolète',
2570
+ desc: 'Identifiez les articles publiés il y a plus de 6 mois et enrichissez-les avec l\'IA.',
2571
+ status: 'À faire',
2572
+ progress: 0,
2573
+ color: '#d97706',
2574
+ steps: ['Analyser le trafic en baisse', 'Réécrire les intros avec IA', 'Mettre à jour l\'année 2026']
2575
+ }
2576
+ ].map(function(workout, wi) {
2577
+ return el('div', { key: wi, style: { background: '#fff', border: '1px solid #e2e8f0', borderRadius: '16px', padding: '22px', display: 'flex', flexDirection: 'column', justifyContent: 'space-between' } },
2578
+ el('div', null,
2579
+ el('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' } },
2580
+ el('span', { style: { fontSize: '11px', fontWeight: '800', background: workout.color + '15', color: workout.color, padding: '3px 10px', borderRadius: '999px' } }, workout.status),
2581
+ el('span', { style: { fontSize: '12px', fontWeight: '800', color: '#64748b' } }, workout.progress + '%')
2582
+ ),
2583
+ el('h3', { style: { fontSize: '15px', fontWeight: '800', color: '#0f172a', margin: '0 0 8px' } }, workout.title),
2584
+ el('p', { style: { fontSize: '12.5px', color: '#64748b', margin: '0 0 16px', lineHeight: 1.5 } }, workout.desc),
2585
+ el('div', { style: { background: '#f8fafc', borderRadius: '10px', padding: '12px', marginBottom: '16px' } },
2586
+ el('div', { style: { fontSize: '11px', fontWeight: '700', color: '#475569', textTransform: 'uppercase', marginBottom: '6px' } }, 'Étapes :'),
2587
+ workout.steps.map(function(step, si) {
2588
+ return el('div', { key: si, style: { fontSize: '11.5px', color: '#334155', marginBottom: '4px', display: 'flex', gap: '6px' } },
2589
+ el('span', { style: { color: workout.color } }, '✓'), step
2590
+ );
2591
+ })
2592
+ )
2593
+ ),
2594
+ el('button', {
2595
+ onClick: function() { alert('🚀 Lancement du ' + workout.title + '...'); },
2596
+ style: { background: workout.color, color: '#fff', border: 0, padding: '10px', borderRadius: '8px', fontWeight: '700', fontSize: '12px', cursor: 'pointer', width: '100%' }
2597
+ }, workout.progress === 100 ? '↺ Refaire le Workout' : '▶ Démarrer le Workout')
2536
2598
  );
2537
2599
  })
2538
2600
  )
@@ -53,6 +53,9 @@ class LynxSeoUltimateEnterprisePlugin {
53
53
  add_action('admin_init', array($this, 'register_settings'));
54
54
  add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));
55
55
 
56
+ // Freeze Modified Date Hook (SEOPress PRO Parity)
57
+ add_filter('wp_insert_post_data', array($this, 'handle_freeze_modified_date'), 10, 2);
58
+
56
59
  // Instant IndexNow Pinging
57
60
  add_action('wp_after_insert_post', array($this, 'trigger_instant_indexing'), 10, 2);
58
61
 
@@ -1193,6 +1196,18 @@ class LynxSeoUltimateEnterprisePlugin {
1193
1196
  get_footer();
1194
1197
  }
1195
1198
 
1199
+ public function handle_freeze_modified_date($data, $postarr) {
1200
+ $post_id = $postarr['ID'] ?? 0;
1201
+ if ($post_id && !empty($_POST['_lynxseo_freeze_modified'])) {
1202
+ $prev_post = get_post($post_id);
1203
+ if ($prev_post) {
1204
+ $data['post_modified'] = $prev_post->post_modified;
1205
+ $data['post_modified_gmt'] = $prev_post->post_modified_gmt;
1206
+ }
1207
+ }
1208
+ return $data;
1209
+ }
1210
+
1196
1211
  /* ────────────────────────────────────────────────────────────────────────
1197
1212
  * 8. SUITE DES SHORTCODES
1198
1213
  * ──────────────────────────────────────────────────────────────────────── */
@@ -1205,6 +1220,56 @@ class LynxSeoUltimateEnterprisePlugin {
1205
1220
  add_shortcode('lynxseo_geolinks', array($this, 'sc_geolinks'));
1206
1221
  add_shortcode('lynxseo_faq_accordion', array($this, 'sc_faq_accordion'));
1207
1222
  add_shortcode('lynxseo_vs_table', array($this, 'sc_vs_table'));
1223
+ add_shortcode('lynxseo_html_sitemap', array($this, 'sc_html_sitemap'));
1224
+ add_shortcode('lynxseo_reading_time', array($this, 'sc_reading_time'));
1225
+ add_shortcode('lynxseo_local_map', array($this, 'sc_local_map'));
1226
+ add_shortcode('lynxseo_contact_card', array($this, 'sc_contact_card'));
1227
+ add_shortcode('lynxseo_contact_info', array($this, 'sc_contact_info'));
1228
+ }
1229
+
1230
+ public function sc_html_sitemap() {
1231
+ $out = '<div class="lynxseo-html-sitemap" style="background:#fff;border:1px solid #e2e8f0;padding:24px;border-radius:16px;margin:20px 0;font-family:-apple-system,sans-serif;">';
1232
+ $out .= '<h3 style="font-size:18px;margin-top:0;color:#0f172a;">🗺️ Plan du Site (HTML Sitemap)</h3>';
1233
+ $pages = get_posts(array('numberposts' => 50, 'post_type' => 'page', 'post_status' => 'publish'));
1234
+ $out .= '<h4 style="font-size:14px;color:#2563eb;margin-bottom:8px;">📄 Pages Principales</h4><ul style="margin:0 0 16px 20px;line-height:1.8;">';
1235
+ foreach ($pages as $p) {
1236
+ $out .= '<li><a href="' . esc_url(get_permalink($p->ID)) . '" style="color:#2563eb;text-decoration:none;font-weight:600;">' . esc_html($p->post_title) . '</a></li>';
1237
+ }
1238
+ $out .= '</ul>';
1239
+ $posts = get_posts(array('numberposts' => 50, 'post_type' => 'post', 'post_status' => 'publish'));
1240
+ if (!empty($posts)) {
1241
+ $out .= '<h4 style="font-size:14px;color:#16a34a;margin-bottom:8px;">✍️ Articles & Guides</h4><ul style="margin:0 0 0 20px;line-height:1.8;">';
1242
+ foreach ($posts as $p) {
1243
+ $out .= '<li><a href="' . esc_url(get_permalink($p->ID)) . '" style="color:#16a34a;text-decoration:none;">' . esc_html($p->post_title) . '</a></li>';
1244
+ }
1245
+ $out .= '</ul>';
1246
+ }
1247
+ $out .= '</div>';
1248
+ return $out;
1249
+ }
1250
+
1251
+ public function sc_reading_time($atts) {
1252
+ global $post;
1253
+ if (!$post) return '<span class="lynxseo-reading-time" style="font-size:12px;color:#64748b;font-weight:600;">⏱️ 3 min de lecture</span>';
1254
+ $word_count = str_word_count(strip_tags($post->post_content));
1255
+ $minutes = max(1, ceil($word_count / 200));
1256
+ return '<span class="lynxseo-reading-time" style="font-size:12px;color:#64748b;font-weight:600;">⏱️ ' . $minutes . ' min de lecture</span>';
1257
+ }
1258
+
1259
+ public function sc_local_map() {
1260
+ return '<div style="background:#f1f5f9;border:1px solid #cbd5e1;padding:20px;border-radius:12px;text-align:center;">📍 <strong>Carte Google Maps Active</strong> (Intégration LynxSEO Local Pack)</div>';
1261
+ }
1262
+
1263
+ public function sc_contact_card() {
1264
+ $gbp = get_option($this->gbp_option, array());
1265
+ $name = $gbp['business_name'] ?? get_bloginfo('name');
1266
+ $phone = $gbp['telephone'] ?? '+33 1 23 45 67 89';
1267
+ $city = $gbp['city'] ?? 'Paris';
1268
+ return '<div style="background:#f8fafc;border:1px solid #e2e8f0;padding:18px;border-radius:14px;display:flex;justify-content:space-between;align-items:center;"><div><strong>' . esc_html($name) . '</strong><div style="font-size:12px;color:#64748b;">📍 ' . esc_html($city) . '</div></div><a href="tel:' . esc_attr($phone) . '" style="background:#2563eb;color:#fff;padding:8px 16px;border-radius:8px;text-decoration:none;font-weight:700;font-size:12px;">📞 ' . esc_html($phone) . '</a></div>';
1269
+ }
1270
+
1271
+ public function sc_contact_info() {
1272
+ return $this->sc_contact_card();
1208
1273
  }
1209
1274
 
1210
1275
  public function sc_google_business_card() { return '<div style="background:#fff;border:1px solid #e2e8f0;padding:16px;border-radius:12px;"><strong>Google Business Profile Vérifié</strong></div>'; }
package/dist/index.js CHANGED
@@ -3244,6 +3244,138 @@ class ExtendedSchemaGraphBuilder {
3244
3244
  } : undefined
3245
3245
  };
3246
3246
  }
3247
+ static buildSpeakable(opts) {
3248
+ return {
3249
+ "@type": "SpeakableSpecification",
3250
+ cssSelector: opts.cssSelectors || [".headline", ".summary", ".direct-answer", "article h1", "article p:first-of-type"],
3251
+ xpath: opts.xpaths
3252
+ };
3253
+ }
3254
+ static buildPodcastEpisode(opts) {
3255
+ return {
3256
+ "@context": "https://schema.org",
3257
+ "@type": "PodcastEpisode",
3258
+ name: opts.title,
3259
+ description: opts.description,
3260
+ datePublished: opts.publishedDate,
3261
+ timeRequired: opts.duration || "PT15M",
3262
+ associatedMedia: {
3263
+ "@type": "AudioObject",
3264
+ contentUrl: opts.audioUrl
3265
+ },
3266
+ partOfSeries: {
3267
+ "@type": "PodcastSeries",
3268
+ name: opts.seriesName
3269
+ }
3270
+ };
3271
+ }
3272
+ static buildRestaurant(opts) {
3273
+ return {
3274
+ "@context": "https://schema.org",
3275
+ "@type": "Restaurant",
3276
+ name: opts.name,
3277
+ url: opts.url,
3278
+ servesCuisine: opts.servesCuisine,
3279
+ hasMenu: opts.menuUrl,
3280
+ priceRange: opts.priceRange || "€€",
3281
+ telephone: opts.telephone,
3282
+ address: {
3283
+ "@type": "PostalAddress",
3284
+ streetAddress: opts.address.street,
3285
+ addressLocality: opts.address.city,
3286
+ postalCode: opts.address.postalCode,
3287
+ addressCountry: opts.address.country
3288
+ }
3289
+ };
3290
+ }
3291
+ static buildBook(opts) {
3292
+ return {
3293
+ "@context": "https://schema.org",
3294
+ "@type": "Book",
3295
+ name: opts.name,
3296
+ author: {
3297
+ "@type": "Person",
3298
+ name: opts.author
3299
+ },
3300
+ isbn: opts.isbn,
3301
+ bookFormat: opts.bookFormat || "https://schema.org/EBook",
3302
+ url: opts.url
3303
+ };
3304
+ }
3305
+ static buildDataset(opts) {
3306
+ return {
3307
+ "@context": "https://schema.org",
3308
+ "@type": "Dataset",
3309
+ name: opts.name,
3310
+ description: opts.description,
3311
+ url: opts.url,
3312
+ keywords: opts.keywords,
3313
+ temporalCoverage: opts.temporalCoverage || "2024/2026",
3314
+ distribution: opts.distributionUrl ? [
3315
+ {
3316
+ "@type": "DataDownload",
3317
+ encodingFormat: "application/json",
3318
+ contentUrl: opts.distributionUrl
3319
+ }
3320
+ ] : undefined
3321
+ };
3322
+ }
3323
+ static buildFactCheck(opts) {
3324
+ return {
3325
+ "@context": "https://schema.org",
3326
+ "@type": "ClaimReview",
3327
+ claimReviewed: opts.claimReviewed,
3328
+ url: opts.url,
3329
+ reviewRating: {
3330
+ "@type": "Rating",
3331
+ ratingValue: opts.ratingValue,
3332
+ bestRating: "5",
3333
+ worstRating: "1"
3334
+ },
3335
+ author: {
3336
+ "@type": "Organization",
3337
+ name: opts.authorName
3338
+ }
3339
+ };
3340
+ }
3341
+ static buildCarousel(items) {
3342
+ return {
3343
+ "@context": "https://schema.org",
3344
+ "@type": "ItemList",
3345
+ itemListElement: items.map((item) => ({
3346
+ "@type": "ListItem",
3347
+ position: item.position,
3348
+ url: item.url,
3349
+ name: item.name,
3350
+ image: item.image
3351
+ }))
3352
+ };
3353
+ }
3354
+ static buildQAPage(opts) {
3355
+ return {
3356
+ "@context": "https://schema.org",
3357
+ "@type": "QAPage",
3358
+ mainEntity: {
3359
+ "@type": "Question",
3360
+ name: opts.questionText,
3361
+ text: opts.questionText,
3362
+ author: {
3363
+ "@type": "Person",
3364
+ name: opts.authorName
3365
+ },
3366
+ answerCount: 1,
3367
+ acceptedAnswer: {
3368
+ "@type": "Answer",
3369
+ text: opts.answerText,
3370
+ upvoteCount: opts.upvoteCount ?? 12,
3371
+ author: {
3372
+ "@type": "Person",
3373
+ name: opts.answerAuthor
3374
+ }
3375
+ }
3376
+ }
3377
+ };
3378
+ }
3247
3379
  }
3248
3380
 
3249
3381
  // packages/lynx-seo-engine/src/built-in-locations.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lynxflow/seo-engine",
3
- "version": "1.8.28",
3
+ "version": "1.8.29",
4
4
  "description": "High-Performance Multilingual Programmatic SEO & AI Search Engine SDK",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -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
  }