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 +495 -72 2.6.8 → 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,40 +55,29 @@
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;
79 + $any_sitemap_exists = $sitemap_exists || $news_sm_exists || $video_sm_exists;
89 80 ?>
90 81
91 82 <div class="metasync-sitemap-status-grid">
92 83 <div class="metasync-sitemap-stat">
@@ -92,11 +83,11 @@
92 83 <div class="metasync-sitemap-stat">
93 84 <div class="stat-icon"><span class="dashicons dashicons-media-default"></span></div>
94 85 <div class="stat-content">
95 86 <div class="stat-label">Status</div>
96 - <div class="stat-value <?php echo $sitemap_exists ? 'status-active' : 'status-inactive'; ?>">
97 - <?php
98 - echo $sitemap_exists ? 'Generated' : 'Not Generated';
87 + <div class="stat-value <?php echo $any_sitemap_exists ? 'status-active' : 'status-inactive'; ?>">
88 + <?php
89 + echo $any_sitemap_exists ? 'Generated' : 'Not Generated';
99 90 if ($sitemap_exists && $sitemap_generator->is_virtual_mode()): ?>
100 91 <span style="color: #2271b1; font-size: 12px; margin-left: 5px;">(Virtual)</span>
101 92 <?php endif; ?>
102 93 </div>
@@ -183,8 +174,136 @@
183 174 </div>
184 175 </div>
185 176 <?php endif; ?>
186 177
178 + <!-- Content Settings Card -->
179 + <div class="dashboard-card metasync-sitemap-content-settings-card">
180 + <h2><?php esc_html_e('Content Settings', 'metasync'); ?></h2>
181 + <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">
182 + <?php esc_html_e('Control which content types and URLs are included in the main sitemap.', 'metasync'); ?>
183 + </p>
184 +
185 + <form method="post">
186 + <?php wp_nonce_field('metasync_sitemap_settings_action', 'metasync_sitemap_settings_nonce'); ?>
187 +
188 + <table class="form-table">
189 + <tr>
190 + <th scope="row"><?php esc_html_e('Post Types', 'metasync'); ?></th>
191 + <td>
192 + <?php
193 + $general_filtered_pts = metasync_get_sitemap_post_type_objects();
194 + $sitemap_configured = !empty($sitemap_settings['_configured']);
195 + $selected_sitemap_pts = !empty($sitemap_settings['post_types']) ? (array) $sitemap_settings['post_types'] : [];
196 + $gen_pts_scrollable = count($general_filtered_pts) > 10;
197 + if ($gen_pts_scrollable) : ?>
198 + <input type="text" class="metasync-checkbox-search" placeholder="<?php esc_attr_e('Filter post types...', 'metasync'); ?>">
199 + <?php endif; ?>
200 + <div class="metasync-checkbox-list <?php echo $gen_pts_scrollable ? 'metasync-checkbox-scroll' : ''; ?>">
201 + <?php foreach ($general_filtered_pts as $pt) : ?>
202 + <label>
203 + <input type="checkbox" name="sitemap_post_types[]" value="<?php echo esc_attr($pt->name); ?>"
204 + <?php checked(!$sitemap_configured || in_array($pt->name, $selected_sitemap_pts, true)); ?> />
205 + <?php echo esc_html($pt->label); ?>
206 + </label>
207 + <?php endforeach; ?>
208 + <p class="metasync-no-results"><?php esc_html_e('No post types match your search.', 'metasync'); ?></p>
209 + </div>
210 + <p class="description"><?php esc_html_e('Select which post types to include. All are included by default.', 'metasync'); ?></p>
211 + </td>
212 + </tr>
213 + <tr>
214 + <th scope="row">
215 + <?php esc_html_e('Categories', 'metasync'); ?>
216 + <?php $gen_categories = get_categories(['hide_empty' => false]); ?>
217 + <span class="metasync-checkbox-count">(<?php echo count($gen_categories); ?>)</span>
218 + </th>
219 + <td>
220 + <?php
221 + $selected_gen_cats = isset($sitemap_settings['categories']) ? (array) $sitemap_settings['categories'] : [];
222 + $gen_cats_scrollable = count($gen_categories) > 10;
223 + if ($gen_cats_scrollable) : ?>
224 + <input type="text" class="metasync-checkbox-search" placeholder="<?php esc_attr_e('Filter categories...', 'metasync'); ?>">
225 + <?php endif; ?>
226 + <div class="metasync-checkbox-list <?php echo $gen_cats_scrollable ? 'metasync-checkbox-scroll' : ''; ?>">
227 + <?php foreach ($gen_categories as $cat) : ?>
228 + <label>
229 + <input type="checkbox" name="sitemap_categories[]" value="<?php echo esc_attr($cat->term_id); ?>"
230 + <?php checked(in_array($cat->term_id, $selected_gen_cats)); ?> />
231 + <?php echo esc_html($cat->name); ?>
232 + </label>
233 + <?php endforeach; ?>
234 + <p class="metasync-no-results"><?php esc_html_e('No categories match your search.', 'metasync'); ?></p>
235 + </div>
236 + <p class="description"><?php esc_html_e('Leave empty to include all categories.', 'metasync'); ?></p>
237 + </td>
238 + </tr>
239 + <tr>
240 + <th scope="row">
241 + <?php esc_html_e('Tags', 'metasync'); ?>
242 + <?php $gen_tags = get_tags(['hide_empty' => false]); ?>
243 + <span class="metasync-checkbox-count">(<?php echo count($gen_tags); ?>)</span>
244 + </th>
245 + <td>
246 + <?php
247 + $selected_gen_tags = isset($sitemap_settings['tags']) ? (array) $sitemap_settings['tags'] : [];
248 + if (!empty($gen_tags)) :
249 + $gen_tags_scrollable = count($gen_tags) > 10;
250 + if ($gen_tags_scrollable) : ?>
251 + <input type="text" class="metasync-checkbox-search" placeholder="<?php esc_attr_e('Filter tags...', 'metasync'); ?>">
252 + <?php endif; ?>
253 + <div class="metasync-checkbox-list <?php echo $gen_tags_scrollable ? 'metasync-checkbox-scroll' : ''; ?>">
254 + <?php foreach ($gen_tags as $tag) : ?>
255 + <label>
256 + <input type="checkbox" name="sitemap_tags[]" value="<?php echo esc_attr($tag->term_id); ?>"
257 + <?php checked(in_array($tag->term_id, $selected_gen_tags)); ?> />
258 + <?php echo esc_html($tag->name); ?>
259 + </label>
260 + <?php endforeach; ?>
261 + <p class="metasync-no-results"><?php esc_html_e('No tags match your search.', 'metasync'); ?></p>
262 + </div>
263 + <?php else : ?>
264 + <p class="description"><?php esc_html_e('No tags found.', 'metasync'); ?></p>
265 + <?php endif; ?>
266 + <p class="description"><?php esc_html_e('Leave empty to include all tags.', 'metasync'); ?></p>
267 + </td>
268 + </tr>
269 + <tr>
270 + <th scope="row"><?php esc_html_e('Taxonomies', 'metasync'); ?></th>
271 + <td>
272 + <?php
273 + $all_taxonomies = get_taxonomies(['public' => true], 'objects');
274 + $filtered_taxonomies = metasync_get_sitemap_taxonomy_picker_taxonomies($all_taxonomies);
275 + $selected_sitemap_taxes = !empty($sitemap_settings['taxonomies']) ? (array) $sitemap_settings['taxonomies'] : [];
276 + $gen_tax_scrollable = count($filtered_taxonomies) > 10;
277 + if ($gen_tax_scrollable) : ?>
278 + <input type="text" class="metasync-checkbox-search" placeholder="<?php esc_attr_e('Filter taxonomies...', 'metasync'); ?>">
279 + <?php endif; ?>
280 + <div class="metasync-checkbox-list <?php echo $gen_tax_scrollable ? 'metasync-checkbox-scroll' : ''; ?>">
281 + <?php foreach ($filtered_taxonomies as $tax) : ?>
282 + <label>
283 + <input type="checkbox" name="sitemap_taxonomies[]" value="<?php echo esc_attr($tax->name); ?>"
284 + <?php checked(!$sitemap_configured || in_array($tax->name, $selected_sitemap_taxes, true)); ?> />
285 + <?php echo esc_html($tax->label); ?> <code><?php echo esc_html($tax->name); ?></code>
286 + </label>
287 + <?php endforeach; ?>
288 + <p class="metasync-no-results"><?php esc_html_e('No taxonomies match your search.', 'metasync'); ?></p>
289 + </div>
290 + <p class="description"><?php esc_html_e('Select which taxonomy archive pages to include. All are included by default.', 'metasync'); ?></p>
291 + </td>
292 + </tr>
293 + <tr>
294 + <th scope="row"><?php esc_html_e('Exclude URLs', 'metasync'); ?></th>
295 + <td>
296 + <textarea name="sitemap_excluded_urls" rows="5" class="large-text code" placeholder="<?php esc_attr_e("https://example.com/page-to-exclude/\nhttps://example.com/another-page/", 'metasync'); ?>"><?php echo esc_textarea($sitemap_settings['excluded_urls'] ?? ''); ?></textarea>
297 + <p class="description"><?php esc_html_e('Enter one URL per line. These URLs will be excluded from the main sitemap.', 'metasync'); ?></p>
298 + </td>
299 + </tr>
300 + </table>
301 +
302 + <?php submit_button(esc_html__('Save & Generate Sitemap', 'metasync'), 'primary', 'save_sitemap_settings'); ?>
303 + </form>
304 + </div>
305 +
187 306 <!-- Generate Sitemap Card -->
188 307 <div class="dashboard-card metasync-sitemap-actions-card">
189 308 <h2>Sitemap Actions</h2>
190 309 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">
@@ -197,9 +316,9 @@
197 316 <form method="post" action="" class="metasync-sitemap-action-form">
198 317 <?php wp_nonce_field('metasync_sitemap_action', 'metasync_sitemap_nonce'); ?>
199 318 <button type="submit" name="generate_sitemap" class="button button-primary button-hero metasync-sitemap-button-large">
200 319 <span class="dashicons dashicons-update"></span>
201 - Generate Sitemap Now
320 + Generate Sitemaps Now
202 321 </button>
203 322 <p class="metasync-button-description">
204 323 <?php
205 324 $urls_per_sitemap = $sitemap_generator->get_urls_per_sitemap();
@@ -231,9 +350,9 @@
231 350 </p>
232 351 <?php endif; ?>
233 352 </form>
234 353
235 - <?php if ($sitemap_exists): ?>
354 + <?php if ($any_sitemap_exists): ?>
236 355 <form method="post" action="" class="metasync-sitemap-action-form" onsubmit="return confirm('Are you sure you want to delete all sitemap files? This action cannot be undone.');">
237 356 <?php wp_nonce_field('metasync_sitemap_action', 'metasync_sitemap_nonce'); ?>
238 357 <button type="submit" name="delete_sitemap" class="button button-secondary button-hero metasync-sitemap-button-large" style="border-color: #dc3232; color: #dc3232;">
239 358 <span class="dashicons dashicons-trash"></span>
@@ -270,21 +389,25 @@
270 389 </div>
271 390
272 391 </div>
273 392
274 - <?php if ($sitemap_exists && !empty($sitemap_files)): ?>
393 + <?php if (($sitemap_exists && !empty($sitemap_files)) || $news_sm_exists || $video_sm_exists || $news_sm_enabled || $video_sm_enabled): ?>
275 394 <!-- Sitemap Files List Card -->
276 395 <div class="dashboard-card metasync-sitemap-files-card">
277 396 <h2>Sitemap Files</h2>
278 397 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">
398 + <?php if ($total_sitemap_count > 0): ?>
279 399 Your sitemap has <?php echo $total_sitemap_count; ?> file<?php echo $total_sitemap_count > 1 ? 's' : ''; ?> — <?php echo $sitemap_count; ?> main (up to <?php echo number_format($sitemap_generator->get_urls_per_sitemap()); ?> URLs each)<?php
280 - if ($news_sm_exists || $video_sm_exists) {
281 - $extras = [];
282 - if ($news_sm_exists) $extras[] = 'news';
283 - if ($video_sm_exists) $extras[] = 'video';
284 - echo ', plus ' . implode(' &amp; ', $extras);
285 - }
286 - ?>.
400 + if ($news_sm_exists || $video_sm_exists) {
401 + $extras = [];
402 + if ($news_sm_exists) $extras[] = 'news';
403 + if ($video_sm_exists) $extras[] = 'video';
404 + echo ', plus ' . implode(' &amp; ', $extras);
405 + }
406 + ?>.
407 + <?php else: ?>
408 + <?php esc_html_e('No sitemap files generated yet. Use "Generate Sitemaps Now" above to create them.', 'metasync'); ?>
409 + <?php endif; ?>
287 410 </p>
288 411
289 412 <table class="metasync-sitemap-table">
290 413 <thead>
@@ -295,9 +418,9 @@
295 418 <th><?php esc_html_e('Actions', 'metasync'); ?></th>
296 419 </tr>
297 420 </thead>
298 421 <tbody>
299 - <?php
422 + <?php if ($sitemap_exists && !empty($sitemap_files)):
300 423 $urls_per_file = $sitemap_generator->get_urls_per_sitemap();
301 424 $total_urls = $url_count;
302 425 foreach ($sitemap_files as $index => $sitemap_file):
303 426 // Calculate URLs in this file
@@ -315,9 +438,9 @@
315 438 <td><?php echo number_format($urls_in_file); ?></td>
316 439 <td>
317 440 <?php
318 441 if ($last_generated) {
319 - 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)));
320 443 } else {
321 444 echo '—';
322 445 }
323 446 ?>
@@ -326,42 +449,73 @@
326 449 <a href="<?php echo esc_url($sitemap_file['url']); ?>" target="_blank" class="button-view">
327 450 <span class="dashicons dashicons-external"></span>
328 451 View
329 452 </a>
453 + <?php if ($index === 0): ?>
454 + <form method="post" action="" style="display:inline;" onsubmit="return confirm('Are you sure you want to delete the General sitemap? This action cannot be undone.');">
455 + <?php wp_nonce_field('metasync_sitemap_action', 'metasync_sitemap_nonce'); ?>
456 + <button type="submit" name="delete_general_sitemap" class="button-delete">
457 + <span class="dashicons dashicons-trash"></span>
458 + <?php esc_html_e('Delete', 'metasync'); ?>
459 + </button>
460 + </form>
461 + <?php endif; ?>
330 462 </td>
331 463 </tr>
332 - <?php endforeach; ?>
464 + <?php endforeach; endif; ?>
333 465
334 - <?php if ($news_sm_exists): ?>
466 + <?php if ($news_sm_enabled): ?>
335 467 <tr>
336 468 <td>
337 469 <div class="sitemap-filename">news-sitemap.xml</div>
338 470 <div class="sitemap-url-range"><?php esc_html_e('Google News Sitemap', 'metasync'); ?></div>
339 471 </td>
340 - <td><?php echo $news_url_count; ?></td>
341 - <td><?php echo $last_generated ? esc_html(date('M j, Y g:i A', strtotime($last_generated))) : '—'; ?></td>
472 + <td><?php echo $news_sm_exists ? $news_url_count : '<em style="color:var(--dashboard-text-secondary);">Not generated</em>'; ?></td>
473 + <td><?php echo ($news_sm_exists && $last_generated) ? esc_html(gmdate('M j, Y g:i A', strtotime($last_generated))) : '—'; ?></td>
342 474 <td>
475 + <?php if ($news_sm_exists): ?>
343 476 <a href="<?php echo esc_url(home_url('/news-sitemap.xml')); ?>" target="_blank" class="button-view">
344 477 <span class="dashicons dashicons-external"></span>
345 478 View
346 479 </a>
480 + <form method="post" action="" style="display:inline;" onsubmit="return confirm('Are you sure you want to delete the News sitemap? This action cannot be undone.');">
481 + <?php wp_nonce_field('metasync_sitemap_action', 'metasync_sitemap_nonce'); ?>
482 + <button type="submit" name="delete_news_sitemap" class="button-delete">
483 + <span class="dashicons dashicons-trash"></span>
484 + <?php esc_html_e('Delete', 'metasync'); ?>
485 + </button>
486 + </form>
487 + <?php else: ?>
488 + <span style="color:var(--dashboard-text-secondary); font-style:italic;"><?php esc_html_e('Enabled — generate to create file', 'metasync'); ?></span>
489 + <?php endif; ?>
347 490 </td>
348 491 </tr>
349 492 <?php endif; ?>
350 493
351 - <?php if ($video_sm_exists): ?>
494 + <?php if ($video_sm_enabled): ?>
352 495 <tr>
353 496 <td>
354 497 <div class="sitemap-filename">video-sitemap.xml</div>
355 498 <div class="sitemap-url-range"><?php esc_html_e('Video Sitemap', 'metasync'); ?></div>
356 499 </td>
357 - <td><?php echo $video_url_count; ?></td>
358 - <td><?php echo $last_generated ? esc_html(date('M j, Y g:i A', strtotime($last_generated))) : '—'; ?></td>
500 + <td><?php echo $video_sm_exists ? $video_url_count : '<em style="color:var(--dashboard-text-secondary);">Not generated</em>'; ?></td>
501 + <td><?php echo ($video_sm_exists && $last_generated) ? esc_html(gmdate('M j, Y g:i A', strtotime($last_generated))) : '—'; ?></td>
359 502 <td>
503 + <?php if ($video_sm_exists): ?>
360 504 <a href="<?php echo esc_url(home_url('/video-sitemap.xml')); ?>" target="_blank" class="button-view">
361 505 <span class="dashicons dashicons-external"></span>
362 506 View
363 507 </a>
508 + <form method="post" action="" style="display:inline;" onsubmit="return confirm('Are you sure you want to delete the Video sitemap? This action cannot be undone.');">
509 + <?php wp_nonce_field('metasync_sitemap_action', 'metasync_sitemap_nonce'); ?>
510 + <button type="submit" name="delete_video_sitemap" class="button-delete">
511 + <span class="dashicons dashicons-trash"></span>
512 + <?php esc_html_e('Delete', 'metasync'); ?>
513 + </button>
514 + </form>
515 + <?php else: ?>
516 + <span style="color:var(--dashboard-text-secondary); font-style:italic;"><?php esc_html_e('Enabled — generate to create file', 'metasync'); ?></span>
517 + <?php endif; ?>
364 518 </td>
365 519 </tr>
366 520 <?php endif; ?>
367 521 </tbody>
@@ -414,12 +568,31 @@
414 568 <?php
415 569 require_once plugin_dir_path(dirname(__FILE__)) . 'sitemap/class-metasync-sitemap-news.php';
416 570 $news_checker = new Metasync_Sitemap_News();
417 571 $news_conflicts = $news_checker->get_conflict_notices();
418 - $news_has_content = false !== get_transient('metasync_vsm_' . md5('news-sitemap.xml'))
419 - || 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;
420 585 ?>
421 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 +
422 595 <!-- News Sitemap Status -->
423 596 <div class="dashboard-card" style="margin-bottom: 20px;">
424 597 <h2><?php esc_html_e('News Sitemap Status', 'metasync'); ?></h2>
425 598 <div style="display: flex; gap: 24px; align-items: center; margin-top: 16px; flex-wrap: wrap;">
@@ -472,8 +645,16 @@
472 645 $notice_type = (strpos($notice, 'can coexist') !== false) ? 'notice-info' : 'notice-warning';
473 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>';
474 647 } ?>
475 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 +
476 657 <div class="dashboard-card">
477 658 <h2><?php esc_html_e('News Sitemap Settings', 'metasync'); ?></h2>
478 659 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">
479 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'); ?>
@@ -483,9 +664,9 @@
483 664 <?php wp_nonce_field('metasync_news_sitemap_action', 'metasync_news_sitemap_nonce'); ?>
484 665
485 666 <table class="form-table">
486 667 <tr>
487 - <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>
488 669 <td>
489 670 <label>
490 671 <input type="checkbox" name="news_enabled" value="1" <?php checked(!empty($news_settings['enabled'])); ?> />
491 672 <?php esc_html_e('Enable Google News Sitemap at /news-sitemap.xml', 'metasync'); ?>
@@ -492,22 +673,18 @@
492 673 </label>
493 674 </td>
494 675 </tr>
495 676 <tr>
496 - <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>
497 678 <td>
498 679 <?php
499 - $public_post_types = get_post_types(['public' => true], 'objects');
500 - $filtered_pts = [];
501 - foreach ($public_post_types as $pt) {
502 - if ($pt->name !== 'attachment') $filtered_pts[] = $pt;
503 - }
680 + $filtered_pts = metasync_get_sitemap_post_type_objects();
504 681 $selected_post_types = isset($news_settings['post_types']) ? (array) $news_settings['post_types'] : ['post'];
505 682 $pts_scrollable = count($filtered_pts) > 10;
506 683 if ($pts_scrollable) : ?>
507 684 <input type="text" class="metasync-checkbox-search" placeholder="<?php esc_attr_e('Filter post types...', 'metasync'); ?>">
508 685 <?php endif; ?>
509 - <div class="<?php echo $pts_scrollable ? 'metasync-checkbox-scroll' : ''; ?>">
686 + <div class="metasync-checkbox-list <?php echo $pts_scrollable ? 'metasync-checkbox-scroll' : ''; ?>">
510 687 <?php foreach ($filtered_pts as $pt) : ?>
511 688 <label>
512 689 <input type="checkbox" name="news_post_types[]" value="<?php echo esc_attr($pt->name); ?>"
513 690 <?php checked(in_array($pt->name, $selected_post_types, true)); ?> />
@@ -530,9 +707,9 @@
530 707 $cats_scrollable = count($categories) > 10;
531 708 if ($cats_scrollable) : ?>
532 709 <input type="text" class="metasync-checkbox-search" placeholder="<?php esc_attr_e('Filter categories...', 'metasync'); ?>">
533 710 <?php endif; ?>
534 - <div class="<?php echo $cats_scrollable ? 'metasync-checkbox-scroll' : ''; ?>">
711 + <div class="metasync-checkbox-list <?php echo $cats_scrollable ? 'metasync-checkbox-scroll' : ''; ?>">
535 712 <?php foreach ($categories as $cat) : ?>
536 713 <label>
537 714 <input type="checkbox" name="news_categories[]" value="<?php echo esc_attr($cat->term_id); ?>"
538 715 <?php checked(in_array($cat->term_id, $selected_cats)); ?> />
@@ -557,9 +734,9 @@
557 734 $tags_scrollable = count($tags) > 10;
558 735 if ($tags_scrollable) : ?>
559 736 <input type="text" class="metasync-checkbox-search" placeholder="<?php esc_attr_e('Filter tags...', 'metasync'); ?>">
560 737 <?php endif; ?>
561 - <div class="<?php echo $tags_scrollable ? 'metasync-checkbox-scroll' : ''; ?>">
738 + <div class="metasync-checkbox-list <?php echo $tags_scrollable ? 'metasync-checkbox-scroll' : ''; ?>">
562 739 <?php foreach ($tags as $tag) : ?>
563 740 <label>
564 741 <input type="checkbox" name="news_tags[]" value="<?php echo esc_attr($tag->term_id); ?>"
565 742 <?php checked(in_array($tag->term_id, $selected_tags)); ?> />
@@ -573,9 +750,55 @@
573 750 <?php endif; ?>
574 751 <p class="description"><?php esc_html_e('Leave empty to include all tags.', 'metasync'); ?></p>
575 752 </td>
576 753 </tr>
754 + <?php
755 + // Generic taxonomy filters for news sitemap
756 + $news_custom_taxonomies = get_taxonomies(['public' => true, '_builtin' => false], 'objects');
757 + $news_saved_taxonomies = isset($news_settings['taxonomies']) ? (array) $news_settings['taxonomies'] : [];
758 + if (!empty($news_custom_taxonomies)) :
759 + foreach ($news_custom_taxonomies as $tax) :
760 + $tax_terms = get_terms(['taxonomy' => $tax->name, 'hide_empty' => false]);
761 + if (empty($tax_terms) || is_wp_error($tax_terms)) continue;
762 + $selected_term_ids = isset($news_saved_taxonomies[$tax->name]) ? (array) $news_saved_taxonomies[$tax->name] : [];
763 + ?>
577 764 <tr>
765 + <th scope="row">
766 + <?php echo esc_html($tax->label); ?>
767 + <span class="metasync-checkbox-count">(<?php echo count($tax_terms); ?>)</span>
768 + </th>
769 + <td>
770 + <?php
771 + $tax_scrollable = count($tax_terms) > 10;
772 + if ($tax_scrollable) : ?>
773 + <?php /* translators: %s: taxonomy name. */ ?>
774 + <input type="text" class="metasync-checkbox-search" placeholder="<?php echo esc_attr(sprintf(__('Filter %s...', 'metasync'), strtolower($tax->label))); ?>">
775 + <?php endif; ?>
776 + <div class="metasync-checkbox-list <?php echo $tax_scrollable ? 'metasync-checkbox-scroll' : ''; ?>">
777 + <?php foreach ($tax_terms as $term) : ?>
778 + <label>
779 + <input type="checkbox" name="news_taxonomies[<?php echo esc_attr($tax->name); ?>][]" value="<?php echo esc_attr($term->term_id); ?>"
780 + <?php checked(in_array($term->term_id, $selected_term_ids)); ?> />
781 + <?php echo esc_html($term->name); ?>
782 + </label>
783 + <?php endforeach; ?>
784 + <p class="metasync-no-results"><?php esc_html_e('No items match your search.', 'metasync'); ?></p>
785 + </div>
786 + <p class="description"><?php esc_html_e('Leave empty to include all.', 'metasync'); ?></p>
787 + </td>
788 + </tr>
789 + <?php
790 + endforeach;
791 + endif;
792 + ?>
793 + <tr>
794 + <th scope="row"><?php esc_html_e('Exclude URLs', 'metasync'); ?></th>
795 + <td>
796 + <textarea name="news_excluded_urls" rows="4" class="large-text code" placeholder="<?php esc_attr_e("https://example.com/post-to-exclude/\nhttps://example.com/another-post/", 'metasync'); ?>"><?php echo esc_textarea($news_settings['excluded_urls'] ?? ''); ?></textarea>
797 + <p class="description"><?php esc_html_e('Enter one URL per line. These URLs will be excluded from the news sitemap.', 'metasync'); ?></p>
798 + </td>
799 + </tr>
800 + <tr>
578 801 <th scope="row"><?php esc_html_e('Publication Name', 'metasync'); ?></th>
579 802 <td>
580 803 <input type="text" name="publication_name" class="regular-text"
581 804 value="<?php echo esc_attr(!empty($news_settings['publication_name']) ? $news_settings['publication_name'] : get_bloginfo('name')); ?>" />
@@ -591,9 +814,9 @@
591 814 </td>
592 815 </tr>
593 816 </table>
594 817
595 - <?php submit_button(esc_html__('Save News Sitemap Settings', 'metasync'), 'primary', 'save_news_sitemap'); ?>
818 + <?php submit_button(esc_html__('Save & Generate Sitemap', 'metasync'), 'primary', 'save_news_sitemap'); ?>
596 819 </form>
597 820 </div>
598 821 </div>
599 822 </div>
@@ -607,12 +830,72 @@
607 830 <?php
608 831 require_once plugin_dir_path(dirname(__FILE__)) . 'sitemap/class-metasync-sitemap-video.php';
609 832 $video_checker = new Metasync_Sitemap_Video();
610 833 $video_conflicts = $video_checker->get_conflict_notices();
611 - $video_has_content = false !== get_transient('metasync_vsm_' . md5('video-sitemap.xml'))
612 - || 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;
613 888 ?>
614 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 +
615 898 <!-- Video Sitemap Status -->
616 899 <div class="dashboard-card" style="margin-bottom: 20px;">
617 900 <h2><?php esc_html_e('Video Sitemap Status', 'metasync'); ?></h2>
618 901 <div style="display: flex; gap: 24px; align-items: center; margin-top: 16px; flex-wrap: wrap;">
@@ -665,8 +948,95 @@
665 948 $notice_type = (strpos($notice, 'can coexist') !== false) ? 'notice-info' : 'notice-warning';
666 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>';
667 950 } ?>
668 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 +
669 1039 <div class="dashboard-card">
670 1040 <h2><?php esc_html_e('Video Sitemap Settings', 'metasync'); ?></h2>
671 1041 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">
672 1042 <?php esc_html_e('Configure the Video Sitemap. Auto-detects YouTube, Vimeo, VideoPress, and self-hosted videos in your content.', 'metasync'); ?>
@@ -676,9 +1046,9 @@
676 1046 <?php wp_nonce_field('metasync_video_sitemap_action', 'metasync_video_sitemap_nonce'); ?>
677 1047
678 1048 <table class="form-table">
679 1049 <tr>
680 - <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>
681 1051 <td>
682 1052 <label>
683 1053 <input type="checkbox" name="video_enabled" value="1" <?php checked(!empty($video_settings['enabled'])); ?> />
684 1054 <?php esc_html_e('Enable Video Sitemap at /video-sitemap.xml', 'metasync'); ?>
@@ -685,22 +1055,18 @@
685 1055 </label>
686 1056 </td>
687 1057 </tr>
688 1058 <tr>
689 - <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>
690 1060 <td>
691 1061 <?php
692 - $public_post_types = get_post_types(['public' => true], 'objects');
693 - $vid_filtered_pts = [];
694 - foreach ($public_post_types as $pt) {
695 - if ($pt->name !== 'attachment') $vid_filtered_pts[] = $pt;
696 - }
1062 + $vid_filtered_pts = metasync_get_sitemap_post_type_objects();
697 1063 $selected_video_pts = isset($video_settings['post_types']) ? (array) $video_settings['post_types'] : ['post', 'page'];
698 1064 $vid_pts_scrollable = count($vid_filtered_pts) > 10;
699 1065 if ($vid_pts_scrollable) : ?>
700 1066 <input type="text" class="metasync-checkbox-search" placeholder="<?php esc_attr_e('Filter post types...', 'metasync'); ?>">
701 1067 <?php endif; ?>
702 - <div class="<?php echo $vid_pts_scrollable ? 'metasync-checkbox-scroll' : ''; ?>">
1068 + <div class="metasync-checkbox-list <?php echo $vid_pts_scrollable ? 'metasync-checkbox-scroll' : ''; ?>">
703 1069 <?php foreach ($vid_filtered_pts as $pt) : ?>
704 1070 <label>
705 1071 <input type="checkbox" name="video_post_types[]" value="<?php echo esc_attr($pt->name); ?>"
706 1072 <?php checked(in_array($pt->name, $selected_video_pts, true)); ?> />
@@ -717,14 +1083,71 @@
717 1083 <label>
718 1084 <input type="checkbox" name="auto_detect" value="1" <?php checked(!empty($video_settings['auto_detect'])); ?> />
719 1085 <?php esc_html_e('Automatically detect embedded videos from post content', 'metasync'); ?>
720 1086 </label>
721 - <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>
722 1088 </td>
723 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>
1104 + <?php
1105 + // Taxonomy filters for video sitemap
1106 + $video_all_taxonomies = get_taxonomies(['public' => true], 'objects');
1107 + $video_saved_taxonomies = isset($video_settings['taxonomies']) ? (array) $video_settings['taxonomies'] : [];
1108 + // Show categories, tags, and custom taxonomies
1109 + foreach ($video_all_taxonomies as $tax) :
1110 + $tax_terms = get_terms(['taxonomy' => $tax->name, 'hide_empty' => false]);
1111 + if (empty($tax_terms) || is_wp_error($tax_terms)) continue;
1112 + $selected_term_ids = isset($video_saved_taxonomies[$tax->name]) ? (array) $video_saved_taxonomies[$tax->name] : [];
1113 + ?>
1114 + <tr>
1115 + <th scope="row">
1116 + <?php echo esc_html($tax->label); ?>
1117 + <span class="metasync-checkbox-count">(<?php echo count($tax_terms); ?>)</span>
1118 + </th>
1119 + <td>
1120 + <?php
1121 + $vtax_scrollable = count($tax_terms) > 10;
1122 + if ($vtax_scrollable) : ?>
1123 + <?php /* translators: %s: taxonomy name. */ ?>
1124 + <input type="text" class="metasync-checkbox-search" placeholder="<?php echo esc_attr(sprintf(__('Filter %s...', 'metasync'), strtolower($tax->label))); ?>">
1125 + <?php endif; ?>
1126 + <div class="metasync-checkbox-list <?php echo $vtax_scrollable ? 'metasync-checkbox-scroll' : ''; ?>">
1127 + <?php foreach ($tax_terms as $term) : ?>
1128 + <label>
1129 + <input type="checkbox" name="video_taxonomies[<?php echo esc_attr($tax->name); ?>][]" value="<?php echo esc_attr($term->term_id); ?>"
1130 + <?php checked(in_array($term->term_id, $selected_term_ids)); ?> />
1131 + <?php echo esc_html($term->name); ?>
1132 + </label>
1133 + <?php endforeach; ?>
1134 + <p class="metasync-no-results"><?php esc_html_e('No items match your search.', 'metasync'); ?></p>
1135 + </div>
1136 + <p class="description"><?php esc_html_e('Leave empty to include all.', 'metasync'); ?></p>
1137 + </td>
1138 + </tr>
1139 + <?php endforeach; ?>
1140 + <tr>
1141 + <th scope="row"><?php esc_html_e('Exclude URLs', 'metasync'); ?></th>
1142 + <td>
1143 + <textarea name="video_excluded_urls" rows="4" class="large-text code" placeholder="<?php esc_attr_e("https://example.com/post-to-exclude/\nhttps://example.com/another-post/", 'metasync'); ?>"><?php echo esc_textarea($video_settings['excluded_urls'] ?? ''); ?></textarea>
1144 + <p class="description"><?php esc_html_e('Enter one URL per line. These URLs will be excluded from the video sitemap.', 'metasync'); ?></p>
1145 + </td>
1146 + </tr>
724 1147 </table>
725 1148
726 - <?php submit_button(esc_html__('Save Video Sitemap Settings', 'metasync'), 'primary', 'save_video_sitemap'); ?>
1149 + <?php submit_button(esc_html__('Save & Generate Sitemap', 'metasync'), 'primary', 'save_video_sitemap'); ?>
727 1150 </form>
728 1151 </div>
729 1152 </div>
730 1153 </div>