PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.1
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.1
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
← All changes | views/metasync-xml-sitemap.php +223 -57 2.6.14 → 2.7.1 View file →
@@ -11,8 +11,10 @@
11 11
12 12 if (!defined('ABSPATH')) {
13 13 exit; // Exit if accessed directly
14 14 }
15 +
16 +require_once dirname(__DIR__) . '/includes/sitemap-taxonomy-picker.php';
15 17 ?>
16 18
17 19 <?php $this->render_layout_open('XML Sitemap', 'xml_sitemap', 'Manage your XML sitemap settings and status.'); ?>
18 20
@@ -53,39 +55,27 @@
53 55 <?php
54 56 $sitemap_files = $sitemap_generator->get_sitemap_files();
55 57 $sitemap_count = count($sitemap_files);
56 58
57 - // Check news/video sitemap status
59 + // Check news/video sitemap status.
60 + //
61 + // Read through get_sitemap_document() — the same accessor the
62 + // served route uses — so these tiles always describe the document
63 + // crawlers actually receive. Reading the transient and the
64 + // physical file in a different order than the serving code is
65 + // what made the dashboard report a URL count no crawler ever saw.
58 66 $news_sm_enabled = !empty($news_settings['enabled']);
59 67 $video_sm_enabled = !empty($video_settings['enabled']);
60 - $news_sm_exists = false !== get_transient('metasync_vsm_' . md5('news-sitemap.xml'))
61 - || file_exists(ABSPATH . 'news-sitemap.xml');
62 - $video_sm_exists = false !== get_transient('metasync_vsm_' . md5('video-sitemap.xml'))
63 - || file_exists(ABSPATH . 'video-sitemap.xml');
68 + $news_xml = $sitemap_generator->get_sitemap_document('news-sitemap.xml');
69 + $video_xml = $sitemap_generator->get_sitemap_document('video-sitemap.xml');
70 + $news_sm_exists = false !== $news_xml;
71 + $video_sm_exists = false !== $video_xml;
64 72 $extra_sitemap_count = ($news_sm_exists ? 1 : 0) + ($video_sm_exists ? 1 : 0);
65 73 $total_sitemap_count = $sitemap_count + $extra_sitemap_count;
66 74
67 75 // Count URLs in news/video sitemaps for total
68 - $news_url_count = 0;
69 - $video_url_count = 0;
70 - if ($news_sm_exists) {
71 - $news_xml = get_transient('metasync_vsm_' . md5('news-sitemap.xml'));
72 - if (!$news_xml && file_exists(ABSPATH . 'news-sitemap.xml')) {
73 - $news_xml = file_get_contents(ABSPATH . 'news-sitemap.xml');
74 - }
75 - if ($news_xml) {
76 - $news_url_count = substr_count($news_xml, '<url>');
77 - }
78 - }
79 - if ($video_sm_exists) {
80 - $video_xml = get_transient('metasync_vsm_' . md5('video-sitemap.xml'));
81 - if (!$video_xml && file_exists(ABSPATH . 'video-sitemap.xml')) {
82 - $video_xml = file_get_contents(ABSPATH . 'video-sitemap.xml');
83 - }
84 - if ($video_xml) {
85 - $video_url_count = substr_count($video_xml, '<url>');
86 - }
87 - }
76 + $news_url_count = $news_sm_exists ? substr_count($news_xml, '<url>') : 0;
77 + $video_url_count = $video_sm_exists ? substr_count($video_xml, '<url>') : 0;
88 78 $total_url_count = $url_count + $news_url_count + $video_url_count;
89 79 $any_sitemap_exists = $sitemap_exists || $news_sm_exists || $video_sm_exists;
90 80 ?>
91 81
@@ -199,16 +189,9 @@
199 189 <tr>
200 190 <th scope="row"><?php esc_html_e('Post Types', 'metasync'); ?></th>
201 191 <td>
202 192 <?php
203 - $all_public_pts = get_post_types(['public' => true], 'objects');
204 - $general_filtered_pts = [];
205 - $general_excluded = ['attachment', 'revision', 'nav_menu_item', 'elementor_library', 'ct_template', 'oxy_user_library', 'brizy-template', 'fusion_template', 'fusion_tb_section', 'ae_global_templates', 'custom_css', 'customize_changeset', 'oembed_cache', 'user_request', 'wp_block', 'wp_template', 'wp_template_part', 'wp_global_styles', 'wp_navigation', 'acf-field-group', 'acf-field', 'fl-builder-template', 'fl-theme-layout'];
206 - foreach ($all_public_pts as $pt) {
207 - if (!in_array($pt->name, $general_excluded, true)) {
208 - $general_filtered_pts[] = $pt;
209 - }
210 - }
193 + $general_filtered_pts = metasync_get_sitemap_post_type_objects();
211 194 $sitemap_configured = !empty($sitemap_settings['_configured']);
212 195 $selected_sitemap_pts = !empty($sitemap_settings['post_types']) ? (array) $sitemap_settings['post_types'] : [];
213 196 $gen_pts_scrollable = count($general_filtered_pts) > 10;
214 197 if ($gen_pts_scrollable) : ?>
@@ -287,15 +270,16 @@
287 270 <th scope="row"><?php esc_html_e('Taxonomies', 'metasync'); ?></th>
288 271 <td>
289 272 <?php
290 273 $all_taxonomies = get_taxonomies(['public' => true], 'objects');
274 + $filtered_taxonomies = metasync_get_sitemap_taxonomy_picker_taxonomies($all_taxonomies);
291 275 $selected_sitemap_taxes = !empty($sitemap_settings['taxonomies']) ? (array) $sitemap_settings['taxonomies'] : [];
292 - $gen_tax_scrollable = count($all_taxonomies) > 10;
276 + $gen_tax_scrollable = count($filtered_taxonomies) > 10;
293 277 if ($gen_tax_scrollable) : ?>
294 278 <input type="text" class="metasync-checkbox-search" placeholder="<?php esc_attr_e('Filter taxonomies...', 'metasync'); ?>">
295 279 <?php endif; ?>
296 280 <div class="metasync-checkbox-list <?php echo $gen_tax_scrollable ? 'metasync-checkbox-scroll' : ''; ?>">
297 - <?php foreach ($all_taxonomies as $tax) : ?>
281 + <?php foreach ($filtered_taxonomies as $tax) : ?>
298 282 <label>
299 283 <input type="checkbox" name="sitemap_taxonomies[]" value="<?php echo esc_attr($tax->name); ?>"
300 284 <?php checked(!$sitemap_configured || in_array($tax->name, $selected_sitemap_taxes, true)); ?> />
301 285 <?php echo esc_html($tax->label); ?> <code><?php echo esc_html($tax->name); ?></code>
@@ -454,9 +438,9 @@
454 438 <td><?php echo number_format($urls_in_file); ?></td>
455 439 <td>
456 440 <?php
457 441 if ($last_generated) {
458 - echo esc_html(date('M j, Y g:i A', strtotime($last_generated)));
442 + echo esc_html(gmdate('M j, Y g:i A', strtotime($last_generated)));
459 443 } else {
460 444 echo '—';
461 445 }
462 446 ?>
@@ -485,9 +469,9 @@
485 469 <div class="sitemap-filename">news-sitemap.xml</div>
486 470 <div class="sitemap-url-range"><?php esc_html_e('Google News Sitemap', 'metasync'); ?></div>
487 471 </td>
488 472 <td><?php echo $news_sm_exists ? $news_url_count : '<em style="color:var(--dashboard-text-secondary);">Not generated</em>'; ?></td>
489 - <td><?php echo ($news_sm_exists && $last_generated) ? esc_html(date('M j, Y g:i A', strtotime($last_generated))) : '—'; ?></td>
473 + <td><?php echo ($news_sm_exists && $last_generated) ? esc_html(gmdate('M j, Y g:i A', strtotime($last_generated))) : '—'; ?></td>
490 474 <td>
491 475 <?php if ($news_sm_exists): ?>
492 476 <a href="<?php echo esc_url(home_url('/news-sitemap.xml')); ?>" target="_blank" class="button-view">
493 477 <span class="dashicons dashicons-external"></span>
@@ -513,9 +497,9 @@
513 497 <div class="sitemap-filename">video-sitemap.xml</div>
514 498 <div class="sitemap-url-range"><?php esc_html_e('Video Sitemap', 'metasync'); ?></div>
515 499 </td>
516 500 <td><?php echo $video_sm_exists ? $video_url_count : '<em style="color:var(--dashboard-text-secondary);">Not generated</em>'; ?></td>
517 - <td><?php echo ($video_sm_exists && $last_generated) ? esc_html(date('M j, Y g:i A', strtotime($last_generated))) : '—'; ?></td>
501 + <td><?php echo ($video_sm_exists && $last_generated) ? esc_html(gmdate('M j, Y g:i A', strtotime($last_generated))) : '—'; ?></td>
518 502 <td>
519 503 <?php if ($video_sm_exists): ?>
520 504 <a href="<?php echo esc_url(home_url('/video-sitemap.xml')); ?>" target="_blank" class="button-view">
521 505 <span class="dashicons dashicons-external"></span>
@@ -584,12 +568,31 @@
584 568 <?php
585 569 require_once plugin_dir_path(dirname(__FILE__)) . 'sitemap/class-metasync-sitemap-news.php';
586 570 $news_checker = new Metasync_Sitemap_News();
587 571 $news_conflicts = $news_checker->get_conflict_notices();
588 - $news_has_content = false !== get_transient('metasync_vsm_' . md5('news-sitemap.xml'))
589 - || file_exists(ABSPATH . 'news-sitemap.xml');
572 + // Reuse the accessor-derived value computed for the status tiles so
573 + // this panel cannot disagree with them, or with the served document.
574 + $news_has_content = $news_sm_exists;
575 + $news_conflict_name = $news_checker->get_conflict_plugin_name();
576 + $mss_brand = Metasync::get_effective_plugin_name();
577 +
578 + # Flag a generated-but-empty news sitemap. $news_url_count is already
579 + # computed once near the top of this file for the status card, so reuse it
580 + # rather than re-reading and re-scanning the XML on the same page load.
581 + $news_empty_warning = !empty($news_settings['enabled'])
582 + && empty($news_conflicts)
583 + && $news_has_content
584 + && $news_url_count === 0;
590 585 ?>
591 586
587 + <?php if (!empty($news_conflict_name)): ?>
588 + <div class="metasync-sitemap-conflict-note" style="display:flex; gap:7px; align-items:center; margin:0 0 16px; padding:8px 12px; border-radius:7px; background:rgba(245,158,11,0.08); border:1px solid rgba(245,158,11,0.28); font-size:12px; line-height:1.5; color:var(--dashboard-text-secondary,#6b7280);">
589 + <span style="font-size:13px; line-height:1;">&#9888;&#65039;</span>
590 + <?php /* translators: %s: plugin brand name. */ ?>
591 + <span><strong style="color:#8a5a00;"><?php echo esc_html($news_conflict_name); ?></strong> <?php printf(esc_html__('is also active — its news sitemap settings may conflict with %s\'s.', 'metasync'), esc_html($mss_brand)); ?></span>
592 + </div>
593 + <?php endif; ?>
594 +
592 595 <!-- News Sitemap Status -->
593 596 <div class="dashboard-card" style="margin-bottom: 20px;">
594 597 <h2><?php esc_html_e('News Sitemap Status', 'metasync'); ?></h2>
595 598 <div style="display: flex; gap: 24px; align-items: center; margin-top: 16px; flex-wrap: wrap;">
@@ -642,8 +645,16 @@
642 645 $notice_type = (strpos($notice, 'can coexist') !== false) ? 'notice-info' : 'notice-warning';
643 646 echo '<div class="notice ' . $notice_type . ' inline" style="margin-bottom: 12px; padding: 12px 16px; border-radius: 8px; background: rgba(255, 152, 0, 0.1); border-left: 4px solid var(--dashboard-warning, #f59e0b); color: var(--dashboard-text-primary, #fff);"><p style="margin: 0;">&#9888; ' . esc_html($notice) . '</p></div>';
644 647 } ?>
645 648
649 + <?php if ($news_empty_warning): ?>
650 + <div class="notice notice-warning inline" style="margin-bottom: 12px; padding: 12px 40px 12px 16px; border-radius: 8px; background: rgba(255, 152, 0, 0.1); border-left: 4px solid var(--dashboard-warning, #f59e0b); color: var(--dashboard-text-primary, #fff);">
651 + <p style="margin: 0;">
652 + &#9888; <?php esc_html_e('The last generated news sitemap contains 0 URLs. A news sitemap only includes posts published in the last 2 days, so this can be normal. If you expected entries here, regenerate it above and review the Post Types and filters below.', 'metasync'); ?>
653 + </p>
654 + </div>
655 + <?php endif; ?>
656 +
646 657 <div class="dashboard-card">
647 658 <h2><?php esc_html_e('News Sitemap Settings', 'metasync'); ?></h2>
648 659 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">
649 660 <?php esc_html_e('Configure the Google News Sitemap. Only articles published within the last 2 days will be included (max 1,000 URLs).', 'metasync'); ?>
@@ -653,9 +664,9 @@
653 664 <?php wp_nonce_field('metasync_news_sitemap_action', 'metasync_news_sitemap_nonce'); ?>
654 665
655 666 <table class="form-table">
656 667 <tr>
657 - <th scope="row"><?php esc_html_e('Enable News Sitemap', 'metasync'); ?></th>
668 + <th scope="row"><?php esc_html_e('Enable News Sitemap', 'metasync'); ?><?php Metasync::render_tooltip_icon('news_sitemap_enable_info', 'A News sitemap is a special list of your recent news articles (published in the last 48 hours) that helps Google News find and show them quickly. Only turn this on if your site publishes news-style content and is accepted into Google News.'); ?></th>
658 669 <td>
659 670 <label>
660 671 <input type="checkbox" name="news_enabled" value="1" <?php checked(!empty($news_settings['enabled'])); ?> />
661 672 <?php esc_html_e('Enable Google News Sitemap at /news-sitemap.xml', 'metasync'); ?>
@@ -662,16 +673,12 @@
662 673 </label>
663 674 </td>
664 675 </tr>
665 676 <tr>
666 - <th scope="row"><?php esc_html_e('Post Types', 'metasync'); ?></th>
677 + <th scope="row"><?php esc_html_e('Post Types', 'metasync'); ?><?php Metasync::render_tooltip_icon('news_sitemap_post_types', 'Choose which content types are included in your Google News sitemap. Only select types that qualify as news content — including everything can hurt News eligibility.'); ?></th>
667 678 <td>
668 679 <?php
669 - $public_post_types = get_post_types(['public' => true], 'objects');
670 - $filtered_pts = [];
671 - foreach ($public_post_types as $pt) {
672 - if ($pt->name !== 'attachment') $filtered_pts[] = $pt;
673 - }
680 + $filtered_pts = metasync_get_sitemap_post_type_objects();
674 681 $selected_post_types = isset($news_settings['post_types']) ? (array) $news_settings['post_types'] : ['post'];
675 682 $pts_scrollable = count($filtered_pts) > 10;
676 683 if ($pts_scrollable) : ?>
677 684 <input type="text" class="metasync-checkbox-search" placeholder="<?php esc_attr_e('Filter post types...', 'metasync'); ?>">
@@ -762,8 +769,9 @@
762 769 <td>
763 770 <?php
764 771 $tax_scrollable = count($tax_terms) > 10;
765 772 if ($tax_scrollable) : ?>
773 + <?php /* translators: %s: taxonomy name. */ ?>
766 774 <input type="text" class="metasync-checkbox-search" placeholder="<?php echo esc_attr(sprintf(__('Filter %s...', 'metasync'), strtolower($tax->label))); ?>">
767 775 <?php endif; ?>
768 776 <div class="metasync-checkbox-list <?php echo $tax_scrollable ? 'metasync-checkbox-scroll' : ''; ?>">
769 777 <?php foreach ($tax_terms as $term) : ?>
@@ -822,12 +830,72 @@
822 830 <?php
823 831 require_once plugin_dir_path(dirname(__FILE__)) . 'sitemap/class-metasync-sitemap-video.php';
824 832 $video_checker = new Metasync_Sitemap_Video();
825 833 $video_conflicts = $video_checker->get_conflict_notices();
826 - $video_has_content = false !== get_transient('metasync_vsm_' . md5('video-sitemap.xml'))
827 - || file_exists(ABSPATH . 'video-sitemap.xml');
834 + // Reuse the accessor-derived value computed for the status tiles so
835 + // this panel cannot disagree with them, or with the served document.
836 + $video_has_content = $video_sm_exists;
837 + $video_conflict_name = $video_checker->get_conflict_plugin_name();
838 + $mss_brand = Metasync::get_effective_plugin_name();
839 +
840 + # Why videos were left out. Recorded by generate_video_sitemap() so the
841 + # notices below can name the actual cause: a post with a video but no
842 + # resolvable thumbnail is skipped (thumbnail_loc is required by Google),
843 + # which is invisible without this.
844 + $video_diagnostics = get_option('metasync_video_sitemap_diagnostics', []);
845 + $video_skipped_no_thumb = isset($video_diagnostics['skipped_no_thumbnail'])
846 + ? (int) $video_diagnostics['skipped_no_thumbnail']
847 + : 0;
848 + $video_skipped_ids = isset($video_diagnostics['skipped_no_thumbnail_ids'])
849 + ? (array) $video_diagnostics['skipped_no_thumbnail_ids']
850 + : [];
851 + # Videos, not posts: a post that emits one working video and drops
852 + # another is invisible to the per-post counter, and that partial case
853 + # is the more common shape on a real catalog.
854 + $video_skipped_videos = isset($video_diagnostics['skipped_no_thumbnail_videos'])
855 + ? (int) $video_diagnostics['skipped_no_thumbnail_videos']
856 + : $video_skipped_no_thumb;
857 + $video_skipped_dupe = isset($video_diagnostics['skipped_duplicate_thumbnail'])
858 + ? (int) $video_diagnostics['skipped_duplicate_thumbnail']
859 + : 0;
860 +
861 + # Common gate: the feature is on, nothing else owns the video sitemap,
862 + # and something has actually been generated to report on.
863 + $video_notices_active = !empty($video_settings['enabled'])
864 + && empty($video_conflicts)
865 + && $video_has_content;
866 +
867 + # Posts whose video was dropped for want of a thumbnail. Deliberately
868 + # NOT gated on an empty sitemap: the common case is a mixed site where
869 + # most videos resolve and a few do not, and gating on $video_url_count
870 + # === 0 would keep exactly that case silent — the silence this notice
871 + # exists to end.
872 + $video_nothumb_notice = $video_notices_active && $video_skipped_videos > 0;
873 +
874 + # A generated-but-empty sitemap with nothing skipped means detection
875 + # found no videos at all, which is a different problem with a different
876 + # fix (post types, or a theme-injected player). $video_url_count is
877 + # already computed near the top of this file for the status card.
878 + $video_no_videos_notice = $video_notices_active
879 + && $video_url_count === 0
880 + && $video_skipped_videos === 0;
881 +
882 + # A post-level image (featured image, OG image, first content image)
883 + # may only be the thumbnail for ONE video on that post: Google treats a
884 + # reused thumbnail as a sign that two videos are the same. Extra videos
885 + # on such a post are left out, which needs saying even when the sitemap
886 + # is otherwise healthy.
887 + $video_dupe_notice = $video_notices_active && $video_skipped_dupe > 0;
828 888 ?>
829 889
890 + <?php if (!empty($video_conflict_name)): ?>
891 + <div class="metasync-sitemap-conflict-note" style="display:flex; gap:7px; align-items:center; margin:0 0 16px; padding:8px 12px; border-radius:7px; background:rgba(245,158,11,0.08); border:1px solid rgba(245,158,11,0.28); font-size:12px; line-height:1.5; color:var(--dashboard-text-secondary,#6b7280);">
892 + <span style="font-size:13px; line-height:1;">&#9888;&#65039;</span>
893 + <?php /* translators: %s: plugin brand name. */ ?>
894 + <span><strong style="color:#8a5a00;"><?php echo esc_html($video_conflict_name); ?></strong> <?php printf(esc_html__('is also active — its video sitemap settings may conflict with %s\'s.', 'metasync'), esc_html($mss_brand)); ?></span>
895 + </div>
896 + <?php endif; ?>
897 +
830 898 <!-- Video Sitemap Status -->
831 899 <div class="dashboard-card" style="margin-bottom: 20px;">
832 900 <h2><?php esc_html_e('Video Sitemap Status', 'metasync'); ?></h2>
833 901 <div style="display: flex; gap: 24px; align-items: center; margin-top: 16px; flex-wrap: wrap;">
@@ -880,8 +948,95 @@
880 948 $notice_type = (strpos($notice, 'can coexist') !== false) ? 'notice-info' : 'notice-warning';
881 949 echo '<div class="notice ' . $notice_type . ' inline" style="margin-bottom: 12px; padding: 12px 16px; border-radius: 8px; background: rgba(255, 152, 0, 0.1); border-left: 4px solid var(--dashboard-warning, #f59e0b); color: var(--dashboard-text-primary, #fff);"><p style="margin: 0;">&#9888; ' . esc_html($notice) . '</p></div>';
882 950 } ?>
883 951
952 + <?php if ($video_nothumb_notice): ?>
953 + <div class="notice notice-warning inline" style="margin-bottom: 12px; padding: 12px 40px 12px 16px; border-radius: 8px; background: rgba(255, 152, 0, 0.1); border-left: 4px solid var(--dashboard-warning, #f59e0b); color: var(--dashboard-text-primary, #fff);">
954 + <p style="margin: 0;">
955 + &#9888;
956 + <?php
957 + printf(
958 + esc_html(
959 + /* translators: %s: number of posts. */
960 + _n(
961 + '%s video was left out because no thumbnail could be resolved for it. Google requires a thumbnail on every video sitemap entry — set a featured image on the post, or fill Thumbnail URL in the Video Sitemap box.',
962 + '%s videos were left out because no thumbnail could be resolved for them. Google requires a thumbnail on every video sitemap entry — set a featured image on those posts, or fill Thumbnail URL in the Video Sitemap box.',
963 + $video_skipped_videos,
964 + 'metasync'
965 + )
966 + ),
967 + esc_html(number_format_i18n($video_skipped_videos))
968 + );
969 + ?>
970 + </p>
971 + <?php if (!empty($video_skipped_ids)): ?>
972 + <p style="margin: 8px 0 0;">
973 + <?php esc_html_e('Affected posts:', 'metasync'); ?>
974 + <?php
975 + $video_skipped_links = [];
976 + foreach (array_slice($video_skipped_ids, 0, 20) as $skipped_id) {
977 + $skipped_id = (int) $skipped_id;
978 + $skipped_title = get_the_title($skipped_id);
979 + if ($skipped_title === '') {
980 + /* translators: %d: post ID. */
981 + $skipped_title = sprintf(esc_html__('Post %d', 'metasync'), $skipped_id);
982 + }
983 + // Null for a post deleted since the last generation, an
984 + // unregistered post type, or a user without edit_post.
985 + // esc_url(null) both emits a PHP 8.1 deprecation and
986 + // renders an anchor pointing at the current page.
987 + $skipped_link = get_edit_post_link($skipped_id);
988 + if (empty($skipped_link)) {
989 + $video_skipped_links[] = esc_html($skipped_title);
990 + continue;
991 + }
992 + $video_skipped_links[] = '<a href="' . esc_url($skipped_link) . '" target="_blank" style="color: var(--dashboard-accent, #3b82f6);">' . esc_html($skipped_title) . '</a>';
993 + }
994 + echo wp_kses_post(implode(', ', $video_skipped_links));
995 + if ($video_skipped_videos > count($video_skipped_links)) {
996 + echo ' ' . esc_html(
997 + sprintf(
998 + /* translators: %s: number of additional posts. */
999 + __('and %s more.', 'metasync'),
1000 + number_format_i18n($video_skipped_videos - count($video_skipped_links))
1001 + )
1002 + );
1003 + }
1004 + ?>
1005 + </p>
1006 + <?php endif; ?>
1007 + </div>
1008 + <?php endif; ?>
1009 +
1010 + <?php if ($video_no_videos_notice): ?>
1011 + <div class="notice notice-warning inline" style="margin-bottom: 12px; padding: 12px 40px 12px 16px; border-radius: 8px; background: rgba(255, 152, 0, 0.1); border-left: 4px solid var(--dashboard-warning, #f59e0b); color: var(--dashboard-text-primary, #fff);">
1012 + <p style="margin: 0;">
1013 + &#9888; <?php esc_html_e('The last generated video sitemap contains 0 URLs, because no videos were found in the selected post types. Check that the right content types are ticked under Post Types below. Note that players injected by a theme or page builder are not visible in post content — if that is your setup, point the plugin at the custom field holding the video with the Video URL Meta Keys setting below.', 'metasync'); ?>
1014 + </p>
1015 + </div>
1016 + <?php endif; ?>
1017 +
1018 + <?php if ($video_dupe_notice): ?>
1019 + <div class="notice notice-info inline" style="margin-bottom: 12px; padding: 12px 40px 12px 16px; border-radius: 8px; background: rgba(59, 130, 246, 0.1); border-left: 4px solid var(--dashboard-accent, #3b82f6); color: var(--dashboard-text-primary, #fff);">
1020 + <p style="margin: 0;">
1021 + <?php
1022 + printf(
1023 + esc_html(
1024 + /* translators: %s: number of videos. */
1025 + _n(
1026 + '%s video was left out because the only thumbnail available was already used by another video on the same post. Google treats a reused thumbnail as a sign that two videos are the same, so every video needs its own image. The Thumbnail URL field and thumbnail custom fields apply to the whole post, so giving each video on one post a distinct thumbnail needs the metasync_video_sitemap_thumbnail filter — or split the videos across separate posts.',
1027 + '%s videos were left out because the only thumbnail available was already used by another video on the same post. Google treats a reused thumbnail as a sign that two videos are the same, so every video needs its own image. The Thumbnail URL field and thumbnail custom fields apply to the whole post, so giving each video on one post a distinct thumbnail needs the metasync_video_sitemap_thumbnail filter — or split the videos across separate posts.',
1028 + $video_skipped_dupe,
1029 + 'metasync'
1030 + )
1031 + ),
1032 + esc_html(number_format_i18n($video_skipped_dupe))
1033 + );
1034 + ?>
1035 + </p>
1036 + </div>
1037 + <?php endif; ?>
1038 +
884 1039 <div class="dashboard-card">
885 1040 <h2><?php esc_html_e('Video Sitemap Settings', 'metasync'); ?></h2>
886 1041 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">
887 1042 <?php esc_html_e('Configure the Video Sitemap. Auto-detects YouTube, Vimeo, VideoPress, and self-hosted videos in your content.', 'metasync'); ?>
@@ -891,9 +1046,9 @@
891 1046 <?php wp_nonce_field('metasync_video_sitemap_action', 'metasync_video_sitemap_nonce'); ?>
892 1047
893 1048 <table class="form-table">
894 1049 <tr>
895 - <th scope="row"><?php esc_html_e('Enable Video Sitemap', 'metasync'); ?></th>
1050 + <th scope="row"><?php esc_html_e('Enable Video Sitemap', 'metasync'); ?><?php Metasync::render_tooltip_icon('video_sitemap_enable_info', 'A Video sitemap lists the videos embedded on your pages (with their title, description, thumbnail, and duration) so search engines can understand them and show them in video search results. Turn this on if your pages contain videos you want discovered.'); ?></th>
896 1051 <td>
897 1052 <label>
898 1053 <input type="checkbox" name="video_enabled" value="1" <?php checked(!empty($video_settings['enabled'])); ?> />
899 1054 <?php esc_html_e('Enable Video Sitemap at /video-sitemap.xml', 'metasync'); ?>
@@ -900,16 +1055,12 @@
900 1055 </label>
901 1056 </td>
902 1057 </tr>
903 1058 <tr>
904 - <th scope="row"><?php esc_html_e('Post Types', 'metasync'); ?></th>
1059 + <th scope="row"><?php esc_html_e('Post Types', 'metasync'); ?><?php Metasync::render_tooltip_icon('video_sitemap_post_types', 'Choose which content types are scanned for embedded videos to include in your video sitemap.'); ?></th>
905 1060 <td>
906 1061 <?php
907 - $public_post_types = get_post_types(['public' => true], 'objects');
908 - $vid_filtered_pts = [];
909 - foreach ($public_post_types as $pt) {
910 - if ($pt->name !== 'attachment') $vid_filtered_pts[] = $pt;
911 - }
1062 + $vid_filtered_pts = metasync_get_sitemap_post_type_objects();
912 1063 $selected_video_pts = isset($video_settings['post_types']) ? (array) $video_settings['post_types'] : ['post', 'page'];
913 1064 $vid_pts_scrollable = count($vid_filtered_pts) > 10;
914 1065 if ($vid_pts_scrollable) : ?>
915 1066 <input type="text" class="metasync-checkbox-search" placeholder="<?php esc_attr_e('Filter post types...', 'metasync'); ?>">
@@ -932,11 +1083,25 @@
932 1083 <label>
933 1084 <input type="checkbox" name="auto_detect" value="1" <?php checked(!empty($video_settings['auto_detect'])); ?> />
934 1085 <?php esc_html_e('Automatically detect embedded videos from post content', 'metasync'); ?>
935 1086 </label>
936 - <p class="description"><?php esc_html_e('Detects YouTube, Vimeo, VideoPress, and self-hosted &lt;video&gt; tags.', 'metasync'); ?></p>
1087 + <p class="description"><?php esc_html_e('Detects YouTube, Vimeo, Vimeo OTT (VHX), VideoPress, and self-hosted &lt;video&gt; tags in post content.', 'metasync'); ?></p>
937 1088 </td>
938 1089 </tr>
1090 + <tr>
1091 + <th scope="row"><?php esc_html_e('Video URL Meta Keys', 'metasync'); ?><?php Metasync::render_tooltip_icon('video_sitemap_url_meta_keys', 'For sites whose theme or page builder stores the video in a custom field (ACF, for example) instead of the post content. Enter the custom-field name(s) holding the video URL, one per line. Leave empty if your videos are embedded directly in the content.'); ?></th>
1092 + <td>
1093 + <textarea name="video_url_meta_keys" rows="3" class="large-text code" placeholder="<?php esc_attr_e("my_video_url\nepisode_embed_url", 'metasync'); ?>"><?php echo esc_textarea($video_settings['video_url_meta_keys'] ?? ''); ?></textarea>
1094 + <p class="description"><?php esc_html_e('One custom-field name per line. Use this when the video player is added by your theme or page builder, so the video URL never appears in the post content. Checked in the order listed; the first field with a valid URL wins.', 'metasync'); ?></p>
1095 + </td>
1096 + </tr>
1097 + <tr>
1098 + <th scope="row"><?php esc_html_e('Thumbnail Meta Keys', 'metasync'); ?><?php Metasync::render_tooltip_icon('video_sitemap_thumbnail_meta_keys', 'Custom-field name(s) holding the video thumbnail image URL. Google requires a thumbnail for every video, and entries without one are left out. If empty, the plugin falls back to the post\'s Open Graph image, then its featured image, then the first image in the content.'); ?></th>
1099 + <td>
1100 + <textarea name="video_thumbnail_meta_keys" rows="3" class="large-text code" placeholder="<?php esc_attr_e("my_video_thumbnail\nepisode_poster", 'metasync'); ?>"><?php echo esc_textarea($video_settings['video_thumbnail_meta_keys'] ?? ''); ?></textarea>
1101 + <p class="description"><?php esc_html_e('One custom-field name per line. Falls back to the post\'s Open Graph image, then the featured image, then the first image in the content. Each video needs its own thumbnail — Google treats a reused thumbnail as a sign that two videos are the same.', 'metasync'); ?></p>
1102 + </td>
1103 + </tr>
939 1104 <?php
940 1105 // Taxonomy filters for video sitemap
941 1106 $video_all_taxonomies = get_taxonomies(['public' => true], 'objects');
942 1107 $video_saved_taxonomies = isset($video_settings['taxonomies']) ? (array) $video_settings['taxonomies'] : [];
@@ -954,8 +1119,9 @@
954 1119 <td>
955 1120 <?php
956 1121 $vtax_scrollable = count($tax_terms) > 10;
957 1122 if ($vtax_scrollable) : ?>
1123 + <?php /* translators: %s: taxonomy name. */ ?>
958 1124 <input type="text" class="metasync-checkbox-search" placeholder="<?php echo esc_attr(sprintf(__('Filter %s...', 'metasync'), strtolower($tax->label))); ?>">
959 1125 <?php endif; ?>
960 1126 <div class="metasync-checkbox-list <?php echo $vtax_scrollable ? 'metasync-checkbox-scroll' : ''; ?>">
961 1127 <?php foreach ($tax_terms as $term) : ?>