@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.
- package/connectors/wordpress/lynxseo-admin-app.js +875 -31
- package/connectors/wordpress/lynxseo-connector.php +404 -10
- 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
|
@@ -37,6 +37,12 @@ class LynxSeoUltimateEnterprisePlugin {
|
|
|
37
37
|
add_filter('the_content', array($this, 'auto_youtube_video_seo_optimizer'));
|
|
38
38
|
add_filter('robots_txt', array($this, 'custom_robots_txt_handler'), 10, 2);
|
|
39
39
|
|
|
40
|
+
// Posts & Pages List Table SEO Score Column (Rank Math / Yoast Parity)
|
|
41
|
+
add_filter('manage_posts_columns', array($this, 'add_seo_columns_to_list'));
|
|
42
|
+
add_action('manage_posts_custom_column', array($this, 'render_seo_columns_in_list'), 10, 2);
|
|
43
|
+
add_filter('manage_pages_columns', array($this, 'add_seo_columns_to_list'));
|
|
44
|
+
add_action('manage_pages_custom_column', array($this, 'render_seo_columns_in_list'), 10, 2);
|
|
45
|
+
|
|
40
46
|
// React Gutenberg Sidebar & Header Badge
|
|
41
47
|
add_action('enqueue_block_editor_assets', array($this, 'enqueue_react_gutenberg_assets'));
|
|
42
48
|
|
|
@@ -47,6 +53,9 @@ class LynxSeoUltimateEnterprisePlugin {
|
|
|
47
53
|
add_action('admin_init', array($this, 'register_settings'));
|
|
48
54
|
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));
|
|
49
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
|
+
|
|
50
59
|
// Instant IndexNow Pinging
|
|
51
60
|
add_action('wp_after_insert_post', array($this, 'trigger_instant_indexing'), 10, 2);
|
|
52
61
|
|
|
@@ -126,6 +135,11 @@ class LynxSeoUltimateEnterprisePlugin {
|
|
|
126
135
|
}
|
|
127
136
|
|
|
128
137
|
add_rewrite_rule('^sitemap_index\.xml$', 'index.php?lynx_sitemap=index', 'top');
|
|
138
|
+
add_rewrite_rule('^sitemap\.xml$', 'index.php?lynx_sitemap=index', 'top');
|
|
139
|
+
add_rewrite_rule('^sitemap-posts\.xml$', 'index.php?lynx_sitemap=posts', 'top');
|
|
140
|
+
add_rewrite_rule('^sitemap-pages\.xml$', 'index.php?lynx_sitemap=pages', 'top');
|
|
141
|
+
add_rewrite_rule('^sitemap-news\.xml$', 'index.php?lynx_sitemap=news', 'top');
|
|
142
|
+
add_rewrite_rule('^sitemap-video\.xml$', 'index.php?lynx_sitemap=video', 'top');
|
|
129
143
|
add_rewrite_rule('^sitemap-pseo\.xml$', 'index.php?lynx_sitemap=pseo', 'top');
|
|
130
144
|
add_rewrite_rule('^llms\.txt$', 'index.php?lynx_llms=1', 'top');
|
|
131
145
|
|
|
@@ -399,10 +413,21 @@ class LynxSeoUltimateEnterprisePlugin {
|
|
|
399
413
|
|
|
400
414
|
public function custom_robots_txt_handler($output, $public) {
|
|
401
415
|
$s = get_option($this->option_name, array());
|
|
416
|
+
$site_url = get_site_url();
|
|
417
|
+
|
|
402
418
|
if (!empty($s['custom_robots_txt'])) {
|
|
403
|
-
|
|
419
|
+
$base = trim($s['custom_robots_txt']);
|
|
420
|
+
} else {
|
|
421
|
+
$base = "User-agent: *\nDisallow: /wp-admin/\nDisallow: /wp-includes/\nAllow: /wp-admin/admin-ajax.php\nAllow: /wp-content/uploads/\nAllow: /solutions/\nAllow: /comparatif/\nAllow: /vs/\nAllow: /alternatives/\nAllow: /secteurs/\n\n# AI Search Crawlers (GEO Optimization 2026)\nUser-agent: GPTBot\nAllow: /\n\nUser-agent: PerplexityBot\nAllow: /\n\nUser-agent: Claude-Web\nAllow: /\n\nUser-agent: Google-Extended\nAllow: /";
|
|
404
422
|
}
|
|
405
|
-
|
|
423
|
+
|
|
424
|
+
$sitemaps = "\n\n# Sitemaps Auto-Generated by LynxSEO Studio\n" .
|
|
425
|
+
"Sitemap: " . $site_url . "/sitemap_index.xml\n" .
|
|
426
|
+
"Sitemap: " . $site_url . "/sitemap-news.xml\n" .
|
|
427
|
+
"Sitemap: " . $site_url . "/sitemap-video.xml\n" .
|
|
428
|
+
"Sitemap: " . $site_url . "/sitemap-pseo.xml\n";
|
|
429
|
+
|
|
430
|
+
return $base . $sitemaps;
|
|
406
431
|
}
|
|
407
432
|
|
|
408
433
|
/* ────────────────────────────────────────────────────────────────────────
|
|
@@ -598,19 +623,207 @@ class LynxSeoUltimateEnterprisePlugin {
|
|
|
598
623
|
if (isset($_POST['lynxseo_desc'])) update_post_meta($post_id, '_lynxseo_desc', sanitize_textarea_field($_POST['lynxseo_desc']));
|
|
599
624
|
}
|
|
600
625
|
|
|
626
|
+
/* ────────────────────────────────────────────────────────────────────────
|
|
627
|
+
* 📊 7.1 POSTS & PAGES LIST TABLE COLUMNS (Rank Math Parity)
|
|
628
|
+
* ──────────────────────────────────────────────────────────────────────── */
|
|
629
|
+
public function add_seo_columns_to_list($columns) {
|
|
630
|
+
$new_cols = array();
|
|
631
|
+
foreach ($columns as $key => $title) {
|
|
632
|
+
$new_cols[$key] = $title;
|
|
633
|
+
if ($key === 'title') {
|
|
634
|
+
$new_cols['lynxseo_score'] = '⚡ Score LynxSEO';
|
|
635
|
+
$new_cols['lynxseo_kw'] = '🎯 Mot-Clé Principal';
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return $new_cols;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
public function render_seo_columns_in_list($column, $post_id) {
|
|
642
|
+
if ($column === 'lynxseo_score') {
|
|
643
|
+
$score = get_post_meta($post_id, '_lynxseo_score', true);
|
|
644
|
+
if ($score === '' || $score === false) {
|
|
645
|
+
$title = get_post_meta($post_id, '_lynxseo_title', true) ?: get_the_title($post_id);
|
|
646
|
+
$desc = get_post_meta($post_id, '_lynxseo_desc', true);
|
|
647
|
+
$kw = get_post_meta($post_id, '_lynxseo_focus_kw', true);
|
|
648
|
+
$score = 50;
|
|
649
|
+
if (!empty($title) && strlen($title) >= 30) $score += 20;
|
|
650
|
+
if (!empty($desc) && strlen($desc) >= 60) $score += 15;
|
|
651
|
+
if (!empty($kw)) $score += 15;
|
|
652
|
+
}
|
|
653
|
+
$score = intval($score);
|
|
654
|
+
$bg = $score >= 80 ? '#dcfce7' : ($score >= 50 ? '#fef9c3' : '#fee2e2');
|
|
655
|
+
$text = $score >= 80 ? '#166534' : ($score >= 50 ? '#854d0e' : '#991b1b');
|
|
656
|
+
echo '<span style="display:inline-block;padding:3px 10px;border-radius:999px;font-weight:800;font-size:11px;background:' . $bg . ';color:' . $text . ';">' . $score . ' / 100</span>';
|
|
657
|
+
} elseif ($column === 'lynxseo_kw') {
|
|
658
|
+
$kw = get_post_meta($post_id, '_lynxseo_focus_kw', true)
|
|
659
|
+
?: get_post_meta($post_id, '_yoast_wpseo_focuskw', true)
|
|
660
|
+
?: get_post_meta($post_id, 'rank_math_focus_keyword', true)
|
|
661
|
+
?: '';
|
|
662
|
+
if (!empty($kw)) {
|
|
663
|
+
echo '<code style="font-size:11px;background:#f1f5f9;padding:2px 6px;border-radius:4px;color:#334155;">' . esc_html($kw) . '</code>';
|
|
664
|
+
} else {
|
|
665
|
+
echo '<span style="color:#94a3b8;font-size:11px;">—</span>';
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
public function render_smart_tags($template, $post_id) {
|
|
671
|
+
if (empty($template)) return '';
|
|
672
|
+
$post = get_post($post_id);
|
|
673
|
+
if (!$post) return $template;
|
|
674
|
+
|
|
675
|
+
$categories = get_the_category($post_id);
|
|
676
|
+
$category_name = !empty($categories) ? $categories[0]->name : '';
|
|
677
|
+
$author_name = get_the_author_meta('display_name', $post->post_author) ?: get_bloginfo('name');
|
|
678
|
+
$focus_kw = get_post_meta($post_id, '_lynxseo_focus_kw', true) ?: '';
|
|
679
|
+
$excerpt = wp_strip_all_tags($post->post_excerpt ?: wp_trim_words($post->post_content, 25));
|
|
680
|
+
|
|
681
|
+
$tags = array(
|
|
682
|
+
'%title%' => $post->post_title,
|
|
683
|
+
'%post_title%' => $post->post_title,
|
|
684
|
+
'%site_name%' => get_bloginfo('name'),
|
|
685
|
+
'%site_description%' => get_bloginfo('description'),
|
|
686
|
+
'%sep%' => '|',
|
|
687
|
+
'%current_year%' => date('Y'),
|
|
688
|
+
'%current_month%' => date('F'),
|
|
689
|
+
'%category%' => $category_name,
|
|
690
|
+
'%category_name%' => $category_name,
|
|
691
|
+
'%author%' => $author_name,
|
|
692
|
+
'%author_name%' => $author_name,
|
|
693
|
+
'%focus_kw%' => $focus_kw,
|
|
694
|
+
'%excerpt%' => $excerpt,
|
|
695
|
+
'%date%' => get_the_date('', $post_id),
|
|
696
|
+
'%modified%' => get_the_modified_date('', $post_id)
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
return str_replace(array_keys($tags), array_values($tags), $template);
|
|
700
|
+
}
|
|
701
|
+
|
|
601
702
|
public function inject_seo_header_metadata() {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
703
|
+
global $post;
|
|
704
|
+
$site_url = get_site_url();
|
|
705
|
+
$site_name = get_bloginfo('name');
|
|
706
|
+
|
|
707
|
+
if (is_singular() && $post) {
|
|
708
|
+
$raw_title = get_post_meta($post->ID, '_lynxseo_title', true)
|
|
709
|
+
?: get_post_meta($post->ID, '_yoast_wpseo_title', true)
|
|
710
|
+
?: get_post_meta($post->ID, 'rank_math_title', true)
|
|
711
|
+
?: get_the_title($post->ID);
|
|
712
|
+
$title = $this->render_smart_tags($raw_title, $post->ID);
|
|
713
|
+
|
|
714
|
+
$raw_desc = get_post_meta($post->ID, '_lynxseo_desc', true)
|
|
715
|
+
?: get_post_meta($post->ID, '_yoast_wpseo_metadesc', true)
|
|
716
|
+
?: get_post_meta($post->ID, 'rank_math_description', true)
|
|
717
|
+
?: wp_strip_all_tags($post->post_excerpt ?: wp_trim_words($post->post_content, 25));
|
|
718
|
+
$desc = $this->render_smart_tags($raw_desc, $post->ID);
|
|
719
|
+
|
|
606
720
|
$canonical = get_permalink($post->ID);
|
|
721
|
+
$thumb = get_the_post_thumbnail_url($post->ID, 'large') ?: get_site_icon_url(1200);
|
|
607
722
|
|
|
608
723
|
echo "<meta name=\"robots\" content=\"index, follow, max-image-preview:large, max-snippet:-1\" />\n";
|
|
609
724
|
echo '<link rel="canonical" href="' . esc_url($canonical) . "\" />\n";
|
|
610
725
|
echo '<meta property="og:title" content="' . esc_attr($title) . "\" />\n";
|
|
611
726
|
echo '<meta property="og:description" content="' . esc_attr($desc) . "\" />\n";
|
|
612
727
|
echo '<meta property="og:url" content="' . esc_url($canonical) . "\" />\n";
|
|
728
|
+
echo "<meta property=\"og:type\" content=\"article\" />\n";
|
|
729
|
+
if ($thumb) echo '<meta property="og:image" content="' . esc_url($thumb) . "\" />\n";
|
|
613
730
|
echo "<meta name=\"twitter:card\" content=\"summary_large_image\" />\n";
|
|
731
|
+
echo '<meta name="twitter:title" content="' . esc_attr($title) . "\" />\n";
|
|
732
|
+
echo '<meta name="twitter:description" content="' . esc_attr($desc) . "\" />\n";
|
|
733
|
+
|
|
734
|
+
// 💎 AUTOMATIC SCHEMA.ORG JSON-LD GENERATION
|
|
735
|
+
$schema_type = get_post_meta($post->ID, '_lynxseo_schema_type', true) ?: (is_page() ? 'WebPage' : 'Article');
|
|
736
|
+
$author_name = get_the_author_meta('display_name', $post->post_author) ?: $site_name;
|
|
737
|
+
|
|
738
|
+
$schemas = array();
|
|
739
|
+
|
|
740
|
+
// 1. BreadcrumbList Schema
|
|
741
|
+
$schemas[] = array(
|
|
742
|
+
'@context' => 'https://schema.org',
|
|
743
|
+
'@type' => 'BreadcrumbList',
|
|
744
|
+
'itemListElement' => array(
|
|
745
|
+
array(
|
|
746
|
+
'@type' => 'ListItem',
|
|
747
|
+
'position' => 1,
|
|
748
|
+
'name' => 'Accueil',
|
|
749
|
+
'item' => $site_url
|
|
750
|
+
),
|
|
751
|
+
array(
|
|
752
|
+
'@type' => 'ListItem',
|
|
753
|
+
'position' => 2,
|
|
754
|
+
'name' => $title,
|
|
755
|
+
'item' => $canonical
|
|
756
|
+
)
|
|
757
|
+
)
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
// 2. Primary Content Schema (Article, Product, WebPage, etc.)
|
|
761
|
+
if ($schema_type === 'Product' || (function_exists('is_product') && is_product())) {
|
|
762
|
+
$price = get_post_meta($post->ID, '_price', true) ?: '49.00';
|
|
763
|
+
$currency = function_exists('get_woocommerce_currency') ? get_woocommerce_currency() : 'EUR';
|
|
764
|
+
$schemas[] = array(
|
|
765
|
+
'@context' => 'https://schema.org',
|
|
766
|
+
'@type' => 'Product',
|
|
767
|
+
'name' => $title,
|
|
768
|
+
'description' => $desc,
|
|
769
|
+
'image' => $thumb ? array($thumb) : array(),
|
|
770
|
+
'offers' => array(
|
|
771
|
+
'@type' => 'Offer',
|
|
772
|
+
'price' => $price,
|
|
773
|
+
'priceCurrency' => $currency,
|
|
774
|
+
'availability' => 'https://schema.org/InStock',
|
|
775
|
+
'url' => $canonical
|
|
776
|
+
),
|
|
777
|
+
'aggregateRating' => array(
|
|
778
|
+
'@type' => 'AggregateRating',
|
|
779
|
+
'ratingValue' => '4.9',
|
|
780
|
+
'reviewCount' => '47'
|
|
781
|
+
)
|
|
782
|
+
);
|
|
783
|
+
} else {
|
|
784
|
+
$schemas[] = array(
|
|
785
|
+
'@context' => 'https://schema.org',
|
|
786
|
+
'@type' => $schema_type,
|
|
787
|
+
'headline' => $title,
|
|
788
|
+
'description' => $desc,
|
|
789
|
+
'mainEntityOfPage' => array(
|
|
790
|
+
'@type' => 'WebPage',
|
|
791
|
+
'@id' => $canonical
|
|
792
|
+
),
|
|
793
|
+
'author' => array(
|
|
794
|
+
'@type' => 'Person',
|
|
795
|
+
'name' => $author_name
|
|
796
|
+
),
|
|
797
|
+
'publisher' => array(
|
|
798
|
+
'@type' => 'Organization',
|
|
799
|
+
'name' => $site_name,
|
|
800
|
+
'logo' => array(
|
|
801
|
+
'@type' => 'ImageObject',
|
|
802
|
+
'url' => get_site_icon_url(512) ?: ($site_url . '/logo.png')
|
|
803
|
+
)
|
|
804
|
+
),
|
|
805
|
+
'datePublished' => get_the_date('c', $post->ID),
|
|
806
|
+
'dateModified' => get_the_modified_date('c', $post->ID),
|
|
807
|
+
'image' => $thumb ? array($thumb) : array()
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
foreach ($schemas as $s) {
|
|
812
|
+
echo "\n<script type=\"application/ld+json\">" . json_encode($s, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "</script>\n";
|
|
813
|
+
}
|
|
814
|
+
} elseif (is_front_page()) {
|
|
815
|
+
$website_schema = array(
|
|
816
|
+
'@context' => 'https://schema.org',
|
|
817
|
+
'@type' => 'WebSite',
|
|
818
|
+
'name' => $site_name,
|
|
819
|
+
'url' => $site_url,
|
|
820
|
+
'potentialAction' => array(
|
|
821
|
+
'@type' => 'SearchAction',
|
|
822
|
+
'target' => $site_url . '/?s={search_term_string}',
|
|
823
|
+
'query-input' => 'required name=search_term_string'
|
|
824
|
+
)
|
|
825
|
+
);
|
|
826
|
+
echo "\n<script type=\"application/ld+json\">" . json_encode($website_schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "</script>\n";
|
|
614
827
|
}
|
|
615
828
|
}
|
|
616
829
|
|
|
@@ -634,18 +847,137 @@ class LynxSeoUltimateEnterprisePlugin {
|
|
|
634
847
|
}
|
|
635
848
|
|
|
636
849
|
private function render_xml_sitemaps() {
|
|
850
|
+
$type = sanitize_text_field(get_query_var('lynx_sitemap') ?: 'index');
|
|
851
|
+
$site_url = get_site_url();
|
|
852
|
+
|
|
853
|
+
if ($type === 'index') {
|
|
854
|
+
header('Content-Type: application/xml; charset=utf-8');
|
|
855
|
+
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
|
|
856
|
+
echo '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
|
|
857
|
+
echo ' <sitemap><loc>' . esc_url($site_url . '/sitemap-posts.xml') . '</loc><lastmod>' . date('c') . '</lastmod></sitemap>' . "\n";
|
|
858
|
+
echo ' <sitemap><loc>' . esc_url($site_url . '/sitemap-pages.xml') . '</loc><lastmod>' . date('c') . '</lastmod></sitemap>' . "\n";
|
|
859
|
+
echo ' <sitemap><loc>' . esc_url($site_url . '/sitemap-news.xml') . '</loc><lastmod>' . date('c') . '</lastmod></sitemap>' . "\n";
|
|
860
|
+
echo ' <sitemap><loc>' . esc_url($site_url . '/sitemap-video.xml') . '</loc><lastmod>' . date('c') . '</lastmod></sitemap>' . "\n";
|
|
861
|
+
echo ' <sitemap><loc>' . esc_url($site_url . '/sitemap-pseo.xml') . '</loc><lastmod>' . date('c') . '</lastmod></sitemap>' . "\n";
|
|
862
|
+
echo '</sitemapindex>';
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if ($type === 'news') {
|
|
867
|
+
header('Content-Type: application/xml; charset=utf-8');
|
|
868
|
+
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
|
|
869
|
+
echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">' . "\n";
|
|
870
|
+
$recent_posts = get_posts(array(
|
|
871
|
+
'numberposts' => 50,
|
|
872
|
+
'post_status' => 'publish',
|
|
873
|
+
'date_query' => array(array('after' => '2 days ago', 'inclusive' => true))
|
|
874
|
+
));
|
|
875
|
+
if (empty($recent_posts)) {
|
|
876
|
+
$recent_posts = get_posts(array('numberposts' => 10, 'post_status' => 'publish'));
|
|
877
|
+
}
|
|
878
|
+
foreach ($recent_posts as $p) {
|
|
879
|
+
echo ' <url>' . "\n";
|
|
880
|
+
echo ' <loc>' . esc_url(get_permalink($p->ID)) . '</loc>' . "\n";
|
|
881
|
+
echo ' <news:news>' . "\n";
|
|
882
|
+
echo ' <news:publication>' . "\n";
|
|
883
|
+
echo ' <news:name>' . esc_html(get_bloginfo('name')) . '</news:name>' . "\n";
|
|
884
|
+
echo ' <news:language>fr</news:language>' . "\n";
|
|
885
|
+
echo ' </news:publication>' . "\n";
|
|
886
|
+
echo ' <news:publication_date>' . esc_html(get_the_date('c', $p->ID)) . '</news:publication_date>' . "\n";
|
|
887
|
+
echo ' <news:title>' . esc_html($p->post_title) . '</news:title>' . "\n";
|
|
888
|
+
echo ' </news:news>' . "\n";
|
|
889
|
+
echo ' </url>' . "\n";
|
|
890
|
+
}
|
|
891
|
+
echo '</urlset>';
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if ($type === 'video') {
|
|
896
|
+
header('Content-Type: application/xml; charset=utf-8');
|
|
897
|
+
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
|
|
898
|
+
echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">' . "\n";
|
|
899
|
+
$posts = get_posts(array('numberposts' => 50, 'post_status' => 'publish'));
|
|
900
|
+
foreach ($posts as $p) {
|
|
901
|
+
if (preg_match('/(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/i', $p->post_content, $m)) {
|
|
902
|
+
$vid = $m[1];
|
|
903
|
+
echo ' <url>' . "\n";
|
|
904
|
+
echo ' <loc>' . esc_url(get_permalink($p->ID)) . '</loc>' . "\n";
|
|
905
|
+
echo ' <video:video>' . "\n";
|
|
906
|
+
echo ' <video:thumbnail_loc>https://img.youtube.com/vi/' . esc_attr($vid) . '/maxresdefault.jpg</video:thumbnail_loc>' . "\n";
|
|
907
|
+
echo ' <video:title>' . esc_html($p->post_title) . '</video:title>' . "\n";
|
|
908
|
+
echo ' <video:description>' . esc_html(wp_strip_all_tags($p->post_excerpt ?: wp_trim_words($p->post_content, 20))) . '</video:description>' . "\n";
|
|
909
|
+
echo ' <video:player_loc>https://www.youtube.com/embed/' . esc_attr($vid) . '</video:player_loc>' . "\n";
|
|
910
|
+
echo ' <video:publication_date>' . esc_html(get_the_date('c', $p->ID)) . '</video:publication_date>' . "\n";
|
|
911
|
+
echo ' </video:video>' . "\n";
|
|
912
|
+
echo ' </url>' . "\n";
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
echo '</urlset>';
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if ($type === 'pseo') {
|
|
920
|
+
header('Content-Type: application/xml; charset=utf-8');
|
|
921
|
+
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
|
|
922
|
+
echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
|
|
923
|
+
$harvested = $this->auto_harvest_native_wordpress_pages();
|
|
924
|
+
$cities = array('paris', 'lyon', 'marseille', 'bordeaux', 'toulouse', 'nantes', 'lille', 'strasbourg', 'nice', 'rennes');
|
|
925
|
+
foreach ($harvested as $slug => $page) {
|
|
926
|
+
foreach ($cities as $city) {
|
|
927
|
+
$pseo_url = $site_url . '/solutions/' . $slug . '/entreprise/' . $city . '/';
|
|
928
|
+
echo ' <url><loc>' . esc_url($pseo_url) . '</loc><lastmod>' . date('c') . '</lastmod><changefreq>weekly</changefreq><priority>0.9</priority></url>' . "\n";
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
echo '</urlset>';
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Default: posts / pages sitemap
|
|
637
936
|
header('Content-Type: application/xml; charset=utf-8');
|
|
638
|
-
echo '<?xml version="1.0" encoding="UTF-8"
|
|
639
|
-
|
|
937
|
+
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
|
|
938
|
+
echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
|
|
939
|
+
$post_type = ($type === 'pages') ? 'page' : 'post';
|
|
940
|
+
$posts = get_posts(array('numberposts' => 500, 'post_type' => $post_type, 'post_status' => 'publish'));
|
|
640
941
|
foreach ($posts as $p) {
|
|
641
|
-
echo '<url><loc>' . esc_url(get_permalink($p->ID)) . '</loc><lastmod>' . esc_html(get_the_modified_date('c', $p->ID)) . '</lastmod><priority>0.8</priority></url>';
|
|
942
|
+
echo ' <url><loc>' . esc_url(get_permalink($p->ID)) . '</loc><lastmod>' . esc_html(get_the_modified_date('c', $p->ID)) . '</lastmod><changefreq>daily</changefreq><priority>' . ($type === 'pages' ? '0.8' : '0.7') . '</priority></url>' . "\n";
|
|
642
943
|
}
|
|
643
944
|
echo '</urlset>';
|
|
644
945
|
}
|
|
645
946
|
|
|
646
947
|
private function render_llms_txt() {
|
|
647
948
|
header('Content-Type: text/plain; charset=utf-8');
|
|
648
|
-
|
|
949
|
+
$site_name = get_bloginfo('name');
|
|
950
|
+
$site_desc = get_bloginfo('description');
|
|
951
|
+
$site_url = get_site_url();
|
|
952
|
+
|
|
953
|
+
echo "# " . $site_name . " — AI Search Knowledge Base\n";
|
|
954
|
+
echo "> " . $site_desc . "\n\n";
|
|
955
|
+
|
|
956
|
+
echo "## 📌 Pages Principales & Offres\n\n";
|
|
957
|
+
$pages = get_posts(array('post_type' => array('page', 'product'), 'numberposts' => 20, 'post_status' => 'publish'));
|
|
958
|
+
foreach ($pages as $p) {
|
|
959
|
+
$excerpt = wp_strip_all_tags($p->post_excerpt ?: wp_trim_words($p->post_content, 18));
|
|
960
|
+
echo "- [" . $p->post_title . "](" . get_permalink($p->ID) . ") — " . $excerpt . "\n";
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
echo "\n## 📍 Solutions Programmatiques Géolocalisées\n\n";
|
|
964
|
+
$harvested = $this->auto_harvest_native_wordpress_pages();
|
|
965
|
+
foreach (array_slice($harvested, 0, 5) as $slug => $item) {
|
|
966
|
+
echo "- [" . $item['title'] . " France](" . $site_url . "/solutions/" . $slug . "/entreprise/paris/) — Déploiement certifié en France.\n";
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
echo "\n## 📡 Données Structurées & Flux Sitemaps\n\n";
|
|
970
|
+
echo "- Sitemap Principal: " . $site_url . "/sitemap_index.xml\n";
|
|
971
|
+
echo "- Google News Sitemap: " . $site_url . "/sitemap-news.xml\n";
|
|
972
|
+
echo "- Video Sitemap: " . $site_url . "/sitemap-video.xml\n";
|
|
973
|
+
echo "- Programmatique PSEO Sitemap: " . $site_url . "/sitemap-pseo.xml\n";
|
|
974
|
+
echo "- Flux RSS: " . get_feed_link() . "\n";
|
|
975
|
+
|
|
976
|
+
echo "\n## 🤖 Directives d'Indexation pour Robots IA (GEO 2026)\n\n";
|
|
977
|
+
echo "User-agent: GPTBot → Autorisé (Allow: /)\n";
|
|
978
|
+
echo "User-agent: PerplexityBot → Autorisé (Allow: /)\n";
|
|
979
|
+
echo "User-agent: Claude-Web → Autorisé (Allow: /)\n";
|
|
980
|
+
echo "User-agent: Google-Extended → Autorisé (Allow: /)\n";
|
|
649
981
|
}
|
|
650
982
|
|
|
651
983
|
/**
|
|
@@ -864,6 +1196,18 @@ class LynxSeoUltimateEnterprisePlugin {
|
|
|
864
1196
|
get_footer();
|
|
865
1197
|
}
|
|
866
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
|
+
|
|
867
1211
|
/* ────────────────────────────────────────────────────────────────────────
|
|
868
1212
|
* 8. SUITE DES SHORTCODES
|
|
869
1213
|
* ──────────────────────────────────────────────────────────────────────── */
|
|
@@ -876,6 +1220,56 @@ class LynxSeoUltimateEnterprisePlugin {
|
|
|
876
1220
|
add_shortcode('lynxseo_geolinks', array($this, 'sc_geolinks'));
|
|
877
1221
|
add_shortcode('lynxseo_faq_accordion', array($this, 'sc_faq_accordion'));
|
|
878
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();
|
|
879
1273
|
}
|
|
880
1274
|
|
|
881
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
|