@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.
@@ -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
- return $s['custom_robots_txt'] . "\n\nSitemap: " . get_site_url() . "/sitemap_index.xml\n";
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
- return $output . "\nSitemap: " . get_site_url() . "/sitemap_index.xml\n";
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
- if (is_singular()) {
603
- global $post;
604
- $title = get_post_meta($post->ID, '_lynxseo_title', true) ?: get_the_title($post->ID);
605
- $desc = get_post_meta($post->ID, '_lynxseo_desc', true) ?: wp_strip_all_tags($post->post_excerpt ?: wp_trim_words($post->post_content, 25));
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"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
639
- $posts = get_posts(array('numberposts' => 100, 'post_status' => 'publish'));
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
- echo "# " . get_bloginfo('name') . " — AI Search Knowledge Base\n\n> " . get_bloginfo('description') . "\n\n- Sitemaps: " . get_site_url() . "/sitemap_index.xml\n";
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>'; }
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.27",
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",