@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.
- package/connectors/wordpress/lynxseo-admin-app.js +63 -1
- package/connectors/wordpress/lynxseo-connector.php +65 -0
- package/connectors/wordpress/lynxseo-connector.zip +0 -0
- package/dist/index.js +132 -0
- package/package.json +1 -1
- package/src/engine.test.ts +46 -0
- package/src/extended-schemas.ts +208 -0
|
@@ -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>'; }
|
|
Binary file
|
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
package/src/engine.test.ts
CHANGED
|
@@ -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)", () => {
|
package/src/extended-schemas.ts
CHANGED
|
@@ -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
|
}
|