PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
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 | includes/class-metasync-settings-fields.php +261 -103 2.6.23 → 2.7.2 View file →
@@ -46,9 +46,23 @@
46 46 // Accordion / Section helpers
47 47 // ────────────────────────────────────────────────────────────────
48 48
49 49 public function get_accordion_sections_config() {
50 - return array(
50 + // Consent switch for writing into other SEO plugins. Loaded here rather
51 + // than through the committed classmap so a stale autoload map can never
52 + // make the switch — and with it the user's only way to stop those
53 + // writes — silently disappear from the page.
54 + require_once plugin_dir_path(dirname(__FILE__)) . 'admin/class-metasync-seo-sync-settings.php';
55 +
56 + $sections = array(
57 + 'analytics' => array(
58 + 'title' => 'Usage Data & Product Improvement',
59 + 'description' => 'Choose whether to share limited usage data to help improve MetaSync',
60 + 'icon' => 'chart-bar',
61 + 'priority' => 70,
62 + 'default_open' => false,
63 + 'render_callback' => array($this, 'render_analytics_section'),
64 + ),
51 65 'connection' => array(
52 66 'title' => 'Connection & Authentication',
53 67 'description' => 'Manage your API connection and authentication settings',
54 68 'icon' => 'admin-network',
@@ -68,11 +82,9 @@
68 82 'fields' => array(
69 83 'otto_pixel_uuid',
70 84 'otto_disable_on_loggedin',
71 85 'otto_disable_preview_button',
72 - 'otto_wp_rocket_compat',
73 - 'otto_sg_optimizer_compat',
74 - 'bookingpress_session_compat',
86 + 'seo_priority',
75 87 )
76 88 ),
77 89 'edge_cache' => array(
78 90 'title' => 'Edge Cache / CDN',
@@ -81,8 +93,16 @@
81 93 'priority' => 22,
82 94 'default_open' => false,
83 95 'render_callback' => array('Metasync_Edge_Cache_Settings', 'render'),
84 96 ),
97 + 'seo_plugin_sync' => array(
98 + 'title' => 'Sync to Other SEO Plugins',
99 + 'description' => 'Control whether ' . Metasync::get_whitelabel_otto_name() . '\'s SEO values are written into Yoast, Rank Math or All in One SEO',
100 + 'icon' => 'update',
101 + 'priority' => 23,
102 + 'default_open' => false,
103 + 'render_callback' => array('Metasync_Seo_Sync_Settings', 'render'),
104 + ),
85 105 'bot_detection' => array(
86 106 'title' => 'Bot Detection & Filtering',
87 107 'description' => 'Manage bot traffic and reduce unnecessary API calls',
88 108 'icon' => 'filter',
@@ -94,16 +114,8 @@
94 114 'otto_bot_blacklist',
95 115 'otto_bot_statistics_link'
96 116 )
97 117 ),
98 - 'breadcrumbs' => array(
99 - 'title' => 'Breadcrumbs',
100 - 'description' => 'Configure breadcrumb navigation display',
101 - 'icon' => 'format-links',
102 - 'priority' => 35,
103 - 'default_open' => false,
104 - 'render_callback' => array($this, 'render_breadcrumbs_section')
105 - ),
106 118 'open_graph' => array(
107 119 'title' => 'Open Graph',
108 120 'description' => 'Control which Open Graph and article meta tags are output',
109 121 'icon' => 'share',
@@ -130,8 +142,9 @@
130 142 'disable_redirection_metabox',
131 143 'disable_canonical_metabox',
132 144 'disable_social_opengraph_metabox',
133 145 'disable_schema_markup_metabox',
146 + 'disable_language_alternates_metabox',
134 147 'disable_seo_metabox',
135 148 'open_external_links'
136 149 )
137 150 ),
@@ -172,13 +185,22 @@
172 185 'fields' => array(
173 186 'permalink_structure',
174 187 'hide_dashboard_framework',
175 188 'show_admin_bar_status',
176 - 'enable_auto_updates',
177 189 'import_external_data'
178 190 )
179 191 )
180 192 );
193 +
194 + // Hidden only when there is nothing to say: no SEO plugin installed, the
195 + // switch off, and no saved originals. It is never hidden by the
196 + // access-control or whitelabel flags that can conceal Advanced Settings
197 + // — a switch that rewrites another plugin's data must stay reachable.
198 + if (!Metasync_Seo_Sync_Settings::should_display()) {
199 + unset($sections['seo_plugin_sync']);
200 + }
201 +
202 + return $sections;
181 203 }
182 204
183 205 public function get_advanced_accordion_config() {
184 206 $config = array();
@@ -203,8 +225,17 @@
203 225 'default_open' => false,
204 226 'render_callback' => array($this->admin_instance, 'render_debug_mode_section')
205 227 );
206 228
229 + $config['headless_mode'] = array(
230 + 'title' => 'Headless Mode',
231 + 'description' => 'Connect WordPress content to a separate public frontend',
232 + 'icon' => 'admin-site-alt3',
233 + 'priority' => 29,
234 + 'default_open' => false,
235 + 'render_callback' => array(Metasync_Settings_Registration::instance(), 'render_headless_mode_section')
236 + );
237 +
207 238 $config['error_logs'] = array(
208 239 'title' => 'Error Logs',
209 240 'description' => 'View and manage error logs to troubleshoot issues',
210 241 'icon' => 'warning',
@@ -277,9 +308,9 @@
277 308 $content_state = $is_open ? 'open' : 'closed';
278 309
279 310 echo '<div class="metasync-accordion-section" data-section="' . esc_attr($section_key) . '">';
280 311
281 - echo '<div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="' . $aria_expanded . '" aria-controls="' . $section_id . '">';
312 + echo '<div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="' . esc_attr($aria_expanded) . '" aria-controls="' . esc_attr($section_id) . '">';
282 313 echo '<div class="metasync-accordion-title">';
283 314 echo '<span class="metasync-accordion-icon"><span class="dashicons dashicons-' . esc_attr($section_data['icon']) . '"></span></span>';
284 315 echo '<div class="metasync-accordion-text">';
285 316 echo '<h3>' . esc_html($section_data['title']) . '</h3>';
@@ -290,9 +321,9 @@
290 321 echo '<span class="toggle-icon">▼</span>';
291 322 echo '</button>';
292 323 echo '</div>';
293 324
294 - echo '<div class="metasync-accordion-content" id="' . $section_id . '" data-state="' . $content_state . '">';
325 + echo '<div class="metasync-accordion-content" id="' . esc_attr($section_id) . '" data-state="' . esc_attr($content_state) . '">';
295 326
296 327 if (isset($section_data['render_callback']) && is_callable($section_data['render_callback'])) {
297 328 call_user_func($section_data['render_callback']);
298 329 }
@@ -314,9 +345,8 @@
314 345 </h4>
315 346 <p style="color: var(--dashboard-text-secondary); margin: 0 0 12px 0;">This action will permanently delete:</p>
316 347 <ul style="color: var(--dashboard-text-secondary); margin: 0 0 12px 20px; line-height: 1.8;">
317 348 <li>All API keys and authentication tokens</li>
318 - <li>White label branding settings</li>
319 349 <li>Plugin configuration and preferences</li>
320 350 <li>Instant indexing settings</li>
321 351 <li>Media optimization settings and cache</li>
322 352 <li>Code minification settings</li>
@@ -321,9 +351,9 @@
321 351 <li>Media optimization settings and cache</li>
322 352 <li>Code minification settings</li>
323 353 <li>All cached data and crawl information</li>
324 354 </ul>
325 - <p style="color: var(--dashboard-warning, #f59e0b); margin: 0; font-weight: 600;">You will need to reconfigure the plugin completely after this reset.</p>
355 + <p style="color: var(--dashboard-warning, #f59e0b); margin: 0; font-weight: 600;">You will need to reconfigure the plugin's operational settings after this reset.</p>
326 356 </div>
327 357 <form method="post" action="" onsubmit="return confirmClearSettings(event)">
328 358 <?php wp_nonce_field('metasync_clear_all_settings_nonce', 'clear_all_settings_nonce'); ?>
329 359 <input type="hidden" name="clear_all_settings" value="yes" />
@@ -331,8 +361,31 @@
331 361 <span class="dashicons dashicons-trash" style="margin-top:3px;font-size:15px;width:15px;height:15px;"></span> Clear All Settings
332 362 </button>
333 363 </form>
334 364 </div>
365 + <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px; margin-top: 20px;">
366 + <h4 style="color: var(--dashboard-text-primary, #1e1e1e); margin: 0 0 12px 0; display: flex; align-items: center; gap: 8px;">
367 + <span class="dashicons dashicons-universal-access-alt" style="font-size:18px;width:18px;height:18px;"></span>
368 + <span>Delete data on uninstall</span>
369 + </h4>
370 + <p style="color: var(--dashboard-text-secondary); margin: 0 0 12px 0;">
371 + By default, deleting the plugin keeps your data: sitemap and redirection settings, per-post SEO values, and the plugin's database tables stay in place in case you reinstall. Enable this to also remove all of that when the plugin itself is deleted from the Plugins screen.
372 + </p>
373 + <p style="color: var(--dashboard-text-secondary); margin: 0 0 12px 0;">
374 + The Google service account and the IndexNow API key are always deleted on uninstall, regardless of this setting.
375 + </p>
376 + <form method="post" action="">
377 + <?php wp_nonce_field('metasync_uninstall_data_toggle_nonce', 'uninstall_data_toggle_nonce'); ?>
378 + <input type="hidden" name="metasync_uninstall_data_toggle_form" value="1" />
379 + <label style="color: var(--dashboard-text-primary, #1e1e1e); display: flex; align-items: center; gap: 8px; margin: 0 0 12px 0; cursor: pointer;">
380 + <input type="checkbox" name="metasync_delete_data_on_uninstall" value="yes" <?php checked(get_option('metasync_delete_data_on_uninstall', 'no'), 'yes'); ?> />
381 + Delete all plugin data (tables, settings, per-post SEO values, logs) when the plugin is uninstalled
382 + </label>
383 + <button type="submit" class="button button-secondary">
384 + Save Uninstall Setting
385 + </button>
386 + </form>
387 + </div>
335 388 <script>
336 389 function confirmClearSettings(event) {
337 390 event.preventDefault();
338 391
@@ -340,9 +393,9 @@
340 393 if (!firstConfirm) {
341 394 return false;
342 395 }
343 396
344 - var secondConfirm = confirm("🚨 FINAL WARNING 🚨\n\nThis will delete:\n• All API keys and authentication tokens\n• White label branding settings\n• Plugin configuration and preferences\n• Instant indexing settings\n• Media optimization settings and cache\n• Code minification settings\n• All cached data\n\nYou will need to reconfigure the entire plugin from scratch.\n\nType 'DELETE' in the next prompt to confirm.");
397 + var secondConfirm = confirm("🚨 FINAL WARNING 🚨\n\nThis will delete:\n• All API keys and authentication tokens\n• Plugin configuration and preferences\n• Instant indexing settings\n• Media optimization settings and cache\n• Code minification settings\n• All cached data\n\nYou will need to reconfigure the plugin's operational settings from scratch.\n\nType 'DELETE' in the next prompt to confirm.");
345 398 if (!secondConfirm) {
346 399 return false;
347 400 }
348 401
@@ -363,14 +416,18 @@
363 416 if (!function_exists('google_index_direct')) {
364 417 if (file_exists(plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php')) {
365 418 require_once plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php';
366 419 } else {
367 - error_log('MetaSync Google Index: google-index-init.php not found at ' . plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php');
420 + error_log('MetaSync Google Index: google-index-init.php not found at ' . plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
368 421 return;
369 422 }
370 423 }
371 424
372 425 $google_index = google_index_direct();
426 + if (!$google_index) {
427 + // Nothing to render against, and calling through would be fatal.
428 + return;
429 + }
373 430 $service_info = $google_index->get_service_account_info();
374 431 $is_configured = !isset($service_info['error']);
375 432
376 433 $saved_json_display = $is_configured ? $google_index->get_redacted_config_json() : '';
@@ -649,9 +706,9 @@
649 706 <p style="color: var(--dashboard-text-secondary); margin: 0 0 20px 0;">
650 707 Select which user roles can see and access this plugin's menu, settings, and options in the WordPress admin area.
651 708 </p>
652 709
653 - <form method="post" action="<?php echo admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '&tab=advanced'); ?>" id="plugin-access-roles-form">
710 + <form method="post" action="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '&tab=advanced')); ?>" id="plugin-access-roles-form">
654 711 <?php wp_nonce_field('metasync_plugin_access_roles_nonce', 'plugin_access_roles_nonce'); ?>
655 712 <input type="hidden" name="save_plugin_access_roles" value="yes" />
656 713
657 714 <div class="metasync-role-selector-container" style="background: var(--dashboard-card-bg-alt, rgba(255,255,255,0.05)); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 16px; max-height: 300px; overflow-y: auto;">
@@ -769,9 +826,9 @@
769 826
770 827 public function can_change_memory_limit() {
771 828 $current_limit = ini_get('memory_limit');
772 829
773 - $test_result = @ini_set('memory_limit', $current_limit);
830 + $test_result = @ini_set('memory_limit', $current_limit); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- runtime PHP setting with no WordPress API
774 831
775 832 return $test_result !== false;
776 833 }
777 834
@@ -823,9 +880,9 @@
823 880 }
824 881
825 882 $memory_limit_mb = $this->get_execution_setting('max_memory_limit');
826 883
827 - $result = @ini_set('memory_limit', $memory_limit_mb . 'M');
884 + $result = @ini_set('memory_limit', $memory_limit_mb . 'M'); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- runtime PHP setting with no WordPress API
828 885
829 886 return $result !== false;
830 887 }
831 888
@@ -1077,8 +1134,9 @@
1077 1134 'apikey' => sprintf('Auto-generated authentication token used for secure API communication between your WordPress site and %s services. You can refresh this token if needed for security purposes.', $plugin_name),
1078 1135 'otto_pixel_uuid' => sprintf('Your unique %s tracking pixel identifier. This UUID is used to track %s modifications and analytics on your website pages.', $otto_name, $otto_name),
1079 1136 'otto_disable_on_loggedin' => sprintf('Disable %s modifications when you are logged in to WordPress. This allows you to see and edit the original content without %s\'s enhancements during editing sessions.', $otto_name, $otto_name),
1080 1137 'otto_disable_preview_button' => sprintf('Hide the %s frontend toolbar that displays the status indicator, preview button, and debug button. Enable this for a cleaner frontend experience.', $otto_name),
1138 + 'seo_priority' => sprintf('Choose which value appears first on a page when both your custom SEO value (title, description or focus keyword, edited on the post/page screen) and an approved %s suggestion are available. This changes what visitors see; it does not delete or overwrite saved values.', $otto_name),
1081 1139 'otto_wp_rocket_compat' => sprintf('WP Rocket Compatibility Mode: Controls how %s interacts with WP Rocket. "Auto" (recommended) allows both to work together by avoiding DONOTCACHEPAGE constant unless necessary for Brizy pages or SG Optimizer conflicts. This ensures WP Rocket\'s JavaScript delay and optimization features continue working.', $otto_name),
1082 1140 'otto_sg_optimizer_compat' => sprintf('SiteGround Optimizer Compatibility Mode: Controls how %s renders on SiteGround hosting. "Auto" (recommended) uses the internal HTTP fetch, which protects themes that defer inline CSS to the footer (e.g. Divi). "Buffer Mode" renders in-process with no internal fetch — faster on sites whose pages can never be host-cached, for example when a plugin starts a PHP session on every request (BookingPress sessions are removed automatically, so this does not apply to them).', $otto_name),
1083 1141 'bookingpress_session_compat' => 'BookingPress Session Compatibility: Controls whether MetaSync removes the PHP session BookingPress starts on every public page, so hosts such as SiteGround can page-cache the site again.',
1084 1142 'otto_disable_for_bots' => sprintf('Enable bot detection to automatically skip %s processing for search engine crawlers, SEO tools, and other bots. This reduces unnecessary API calls and improves performance.', $otto_name),
@@ -1084,14 +1142,15 @@
1084 1142 'otto_disable_for_bots' => sprintf('Enable bot detection to automatically skip %s processing for search engine crawlers, SEO tools, and other bots. This reduces unnecessary API calls and improves performance.', $otto_name),
1085 1143 'otto_bot_whitelist' => sprintf('Enter bot names or user-agent patterns (one per line) that should always be processed by %s, even when "Disable for Bots" is enabled. For example: Googlebot, Bingbot. This allows you to ensure specific search engines always see your optimized content.', $otto_name),
1086 1144 'otto_bot_blacklist' => sprintf('Enter bot names or user-agent patterns (one per line) that should always be blocked from %s processing, regardless of other settings. For example: BadBot, MaliciousCrawler. Use this to exclude problematic crawlers or unwanted traffic sources.', $otto_name),
1087 1145 'otto_bot_statistics_link' => 'View detailed bot detection statistics including total hits, API calls saved, breakdown by bot type, and unique bot entries with hit counts.',
1088 - 'disable_common_robots_metabox' => 'Hide the Common Robots meta box from post and page edit screens. This removes the robots meta tag controls (index/noindex, follow/nofollow) from the editor interface.',
1089 - 'disable_advance_robots_metabox' => 'Hide the Advanced Robots meta box from post and page edit screens. This removes advanced robots directives like max-snippet, max-image-preview, and max-video-preview settings.',
1090 - 'disable_redirection_metabox' => 'Hide the Redirection meta box from post and page edit screens. This removes the URL redirect configuration options from the editor interface.',
1091 - 'disable_canonical_metabox' => 'Hide the Canonical URL meta box from post and page edit screens. This removes the canonical URL override field from the editor interface.',
1092 - 'disable_social_opengraph_metabox' => 'Hide the Social Media & Open Graph meta box from post and page edit screens. This removes Facebook, Twitter, and other social media meta tag controls from the editor.',
1093 - 'disable_schema_markup_metabox' => 'Hide the Schema Markup meta box from post and page edit screens. This removes the structured data (Article, FAQ, Product, Recipe, etc.) configuration from the editor interface.',
1146 + 'disable_common_robots_metabox' => sprintf('Hide the Common Robots meta box and stop %1$s emitting index/noindex and follow/nofollow directives on the front end. %1$s also stops overriding WordPress core or another SEO plugin\'s robots tag. Saved values are kept and restored if you re-enable this.', Metasync::get_effective_plugin_name('MetaSync')),
1147 + 'disable_advance_robots_metabox' => sprintf('Hide the Advanced Robots meta box and stop %1$s emitting max-snippet, max-image-preview and max-video-preview directives, including the site-wide advanced defaults. Saved values are kept and restored if you re-enable this.', Metasync::get_effective_plugin_name('MetaSync')),
1148 + 'disable_redirection_metabox' => 'Hide the Redirection meta box and stop redirects configured through it from running. Rules added by hand on the Redirections screen keep working. Saved redirects are kept and resume if you re-enable this.',
1149 + 'disable_canonical_metabox' => sprintf('Hide the Canonical meta box and stop %1$s emitting or overriding the canonical URL on every render path. WordPress core and third-party canonicals are left in place. Saved values are kept and restored if you re-enable this.', Metasync::get_effective_plugin_name('MetaSync')),
1150 + 'disable_social_opengraph_metabox' => sprintf('Hide the Social Media & Open Graph meta box and stop %1$s emitting any Open Graph, Twitter or article tags. Tags from another SEO plugin are left in place. Saved values are kept and restored if you re-enable this.', Metasync::get_effective_plugin_name('MetaSync')),
1151 + 'disable_schema_markup_metabox' => sprintf('Hide the Schema Markup meta box and stop %1$s emitting any JSON-LD, including the site-wide Local SEO markup and OTTO structured data. Breadcrumb markup has its own setting. Saved values are kept and restored if you re-enable this.', Metasync::get_effective_plugin_name('MetaSync')),
1152 + 'disable_language_alternates_metabox' => sprintf('Hide the Language Alternates (hreflang) panel in the block editor and stop %1$s emitting any hreflang tags on the front end, including the auto-detected WPML entries. While disabled, %1$s does not manage or override this data — another SEO plugin, or WPML itself, is free to handle it. Saved values are kept and restored if you re-enable this.', Metasync::get_effective_plugin_name('MetaSync')),
1094 1153 'disable_seo_metabox' => 'Hide the SEO Title & Meta Description meta box from post and page edit screens. This removes the Classic editor SEO fields (the Gutenberg sidebar is unaffected).',
1095 1154 'open_external_links' => sprintf('Automatically add target="_blank" attribute to external links appearing in your posts, pages, and other post types when rendered by %s.', $otto_name),
1096 1155 'content_genius_sync_roles' => 'Select which WordPress user roles should be synchronized with Content Genius. This determines which users will have their profiles and permissions synced for content collaboration.',
1097 1156 'permalink_structure' => sprintf('Displays your current WordPress permalink structure. %s works best with pretty permalinks (not "Plain"). If you see a warning, visit Settings > Permalinks to change your structure.', $plugin_name),
@@ -1096,9 +1155,8 @@
1096 1155 'content_genius_sync_roles' => 'Select which WordPress user roles should be synchronized with Content Genius. This determines which users will have their profiles and permissions synced for content collaboration.',
1097 1156 'permalink_structure' => sprintf('Displays your current WordPress permalink structure. %s works best with pretty permalinks (not "Plain"). If you see a warning, visit Settings > Permalinks to change your structure.', $plugin_name),
1098 1157 'hide_dashboard_framework' => sprintf('Hide the main %s dashboard from the WordPress admin menu. This is useful if you want to reduce menu clutter but still keep the plugin active.', $plugin_name),
1099 1158 'show_admin_bar_status' => sprintf('Display the %s status indicator in the WordPress admin bar at the top of your screen. This provides quick visibility of plugin status and key metrics.', $plugin_name),
1100 - 'enable_auto_updates' => sprintf('Allow WordPress to automatically update the %s plugin when new versions are released. Recommended for security patches, but you may prefer manual updates for major versions.', $plugin_name),
1101 1159 'import_external_data' => sprintf('Import your existing SEO settings and metadata from other popular SEO plugins like Yoast, Rank Math, or All in One SEO. This makes migration to %s seamless without losing your SEO data.', $plugin_name),
1102 1160 'import_seo_metadata' => sprintf('Migrate your existing SEO titles and meta descriptions from Yoast, Rank Math, or All in One SEO. This one-click import preserves your search rankings by copying your optimized meta data to %s, even if the source plugin is deactivated.', $plugin_name),
1103 1161 'default_page_builder' => 'Choose how Content Genius stores synced articles. "Gutenberg" works with all themes and page builders. Selecting a specific builder converts content to that builder\'s widget format, which inherits the builder\'s global typography and layout styles.',
1104 1162 'og_image_dimensions' => 'Adds your share image\'s width, height, and file type to the Open Graph tags so Facebook, LinkedIn, and others render your preview instantly at the right size instead of guessing. Leave on unless another plugin already outputs these.',
@@ -1109,8 +1167,35 @@
1109 1167 'twitter_image_alt' => 'Adds descriptive alt text to the image shown when your page is shared on X/Twitter, improving accessibility for screen-reader users. Uses your saved alt text or the image\'s own alt attribute.'
1110 1168 );
1111 1169 }
1112 1170
1171 + public function render_analytics_section() {
1172 + $enabled = get_option('metasync_analytics_opt_in', 'no') === 'yes';
1173 + $plugin_name = Metasync::get_effective_plugin_name();
1174 + /* translators: %s: Plugin name. */
1175 + $heading = sprintf(__('Help improve %s', 'metasync'), $plugin_name);
1176 + /* translators: %s: Plugin name. */
1177 + $intro = sprintf(__('Allow %s to collect limited usage analytics. This helps us understand which features are used, improve the product, and identify problems.', 'metasync'), $plugin_name);
1178 + /* translators: %s: Plugin name. */
1179 + $toggle_label = sprintf(__('Share usage data with %s', 'metasync'), $plugin_name);
1180 + ?>
1181 + <div class="metasync-analytics-consent-card">
1182 + <div class="metasync-analytics-consent-copy">
1183 + <h3><?php echo esc_html($heading); ?></h3>
1184 + <p><?php echo esc_html($intro); ?></p>
1185 + <p class="description"><?php esc_html_e('We collect limited technical information such as your site URL, plugin version, WordPress and PHP versions, user role, and feature usage. We do not collect passwords, API keys, customer content, names, or email addresses.', 'metasync'); ?></p>
1186 + </div>
1187 + <label class="metasync-consent-switch" for="metasync_analytics_opt_in">
1188 + <input type="hidden" name="metasync_analytics_opt_in" value="no" />
1189 + <input type="checkbox" id="metasync_analytics_opt_in" name="metasync_analytics_opt_in" value="yes" <?php checked($enabled); ?> />
1190 + <span class="metasync-consent-slider" aria-hidden="true"></span>
1191 + <span class="screen-reader-text"><?php echo esc_html($toggle_label); ?></span>
1192 + </label>
1193 + <span class="metasync-consent-status" aria-live="polite" data-enabled-label="<?php esc_attr_e('Enabled', 'metasync'); ?>" data-disabled-label="<?php esc_attr_e('Off', 'metasync'); ?>"><?php echo $enabled ? esc_html__('Enabled', 'metasync') : esc_html__('Off', 'metasync'); ?></span>
1194 + </div>
1195 + <?php
1196 + }
1197 +
1113 1198 public function get_field_section($field_id) {
1114 1199 $sections = $this->get_accordion_sections_config();
1115 1200
1116 1201 foreach ($sections as $section_key => $section_data) {
@@ -1144,9 +1229,9 @@
1144 1229 $content_state = $is_open ? 'open' : 'closed';
1145 1230
1146 1231 echo '<div class="metasync-accordion-section" data-section="' . esc_attr($section_key) . '">';
1147 1232
1148 - echo '<div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="' . $aria_expanded . '" aria-controls="' . $section_id . '">';
1233 + echo '<div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="' . esc_attr($aria_expanded) . '" aria-controls="' . esc_attr($section_id) . '">';
1149 1234 echo '<div class="metasync-accordion-title">';
1150 1235 echo '<span class="metasync-accordion-icon"><span class="dashicons dashicons-' . esc_attr($section_data['icon']) . '"></span></span>';
1151 1236 echo '<div class="metasync-accordion-text">';
1152 1237 echo '<h3>' . esc_html($section_data['title']) . '</h3>';
@@ -1157,9 +1242,9 @@
1157 1242 echo '<span class="toggle-icon">▼</span>';
1158 1243 echo '</button>';
1159 1244 echo '</div>';
1160 1245
1161 - echo '<div class="metasync-accordion-content" id="' . $section_id . '" data-state="' . $content_state . '">';
1246 + echo '<div class="metasync-accordion-content" id="' . esc_attr($section_id) . '" data-state="' . esc_attr($content_state) . '">';
1162 1247
1163 1248 if (isset($section_data['render_callback']) && is_callable($section_data['render_callback'])) {
1164 1249 call_user_func($section_data['render_callback']);
1165 1250 } elseif (isset($section_data['fields']) && is_array($section_data['fields'])) {
@@ -1174,9 +1259,9 @@
1174 1259 echo '<tr>';
1175 1260 echo '<th scope="row">';
1176 1261 if (!empty($field['title'])) {
1177 1262 echo '<div class="metasync-field-label-wrapper">';
1178 - echo '<label for="' . esc_attr($field['id']) . '">' . $field['title'] . '</label>';
1263 + echo '<label for="' . esc_attr($field['id']) . '">' . esc_html($field['title']) . '</label>';
1179 1264
1180 1265 if (isset($tooltips[$field_id])) {
1181 1266 echo '<button type="button" class="metasync-tooltip-trigger" data-tooltip-id="' . esc_attr($field_id) . '" aria-label="More information">';
1182 1267 echo '<svg class="metasync-info-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">';
@@ -1231,9 +1316,9 @@
1231 1316 echo '<tr>';
1232 1317 echo '<th scope="row">';
1233 1318 if (!empty($field['title'])) {
1234 1319 echo '<div class="metasync-field-label-wrapper">';
1235 - echo '<label for="' . esc_attr($field['id']) . '">' . $field['title'] . '</label>';
1320 + echo '<label for="' . esc_attr($field['id']) . '">' . esc_html($field['title']) . '</label>';
1236 1321 if (isset($llms_tooltips[$field['id']])) {
1237 1322 Metasync::render_tooltip_icon($field['id'], $llms_tooltips[$field['id']]);
1238 1323 }
1239 1324 echo '</div>';
@@ -1287,32 +1372,32 @@
1287 1372 $display_value = $current_token;
1288 1373 $status_message = 'Plugin Auth Token is active and ready for authentication.';
1289 1374 $refresh_help = 'Click refresh to generate a new token and update the heartbeat API.';
1290 1375 } else {
1291 - $display_value = 'Auto-generated when connecting to ' . esc_html(Metasync::get_effective_plugin_name());
1292 - $status_message = 'Plugin Auth Token will be automatically generated when you click "Connect to ' . esc_html(Metasync::get_effective_plugin_name()) . '".';
1376 + $display_value = 'Auto-generated when connecting to ' . Metasync::get_effective_plugin_name();
1377 + $status_message = 'Plugin Auth Token will be automatically generated when you click "Connect to ' . Metasync::get_effective_plugin_name() . '".';
1293 1378 $refresh_help = 'You can also manually generate a token by clicking refresh.';
1294 1379 }
1295 1380
1296 1381 printf('<div class="metasync-auth-token-row">');
1297 1382 printf(
1298 - '<input type="text" id="apikey" name="' . Metasync_Admin::option_key . '[general][apikey]" value="%s" size="40" class="regular-text" readonly="readonly" />',
1383 + '<input type="text" id="apikey" name="' . esc_attr(Metasync_Admin::option_key) . '[general][apikey]" value="%s" size="40" class="regular-text" readonly="readonly" />',
1299 1384 esc_attr($display_value)
1300 1385 );
1301 1386 printf('<button type="button" id="refresh-plugin-auth-token" class="button button-secondary metasync-refresh-token-btn"><span class="dashicons dashicons-controls-repeat"></span> Refresh Token</button>');
1302 1387 printf('</div>');
1303 - printf('<p class="description">%s %s</p>', $status_message, $refresh_help);
1388 + printf('<p class="description">%s %s</p>', esc_html($status_message), esc_html($refresh_help));
1304 1389 }
1305 1390
1306 1391 public function linkgraph_token_callback()
1307 1392 {
1308 1393 printf(
1309 - '<input type="text" id="linkgraph_token" name="' . Metasync_Admin::option_key . '[general][linkgraph_token]" value="%s" size="25" readonly="readonly" />',
1394 + '<input type="text" id="linkgraph_token" name="' . esc_attr(Metasync_Admin::option_key) . '[general][linkgraph_token]" value="%s" size="25" readonly="readonly" />',
1310 1395 isset(Metasync::get_option('general')['linkgraph_token']) ? esc_attr(Metasync::get_option('general')['linkgraph_token']) : ''
1311 1396 );
1312 1397
1313 1398 printf(
1314 - '<input type="text" id="linkgraph_customer_id" name="' . Metasync_Admin::option_key . '[general][linkgraph_customer_id]" value="%s" size="25" readonly="readonly" />',
1399 + '<input type="text" id="linkgraph_customer_id" name="' . esc_attr(Metasync_Admin::option_key) . '[general][linkgraph_customer_id]" value="%s" size="25" readonly="readonly" />',
1315 1400 isset(Metasync::get_option('general')['linkgraph_customer_id']) ? esc_attr(Metasync::get_option('general')['linkgraph_customer_id']) : ''
1316 1401 );
1317 1402
1318 1403 ?>
@@ -1437,9 +1522,9 @@
1437 1522 printf('• The authentication window will open in a popup - please allow popups<br/>');
1438 1523 printf('• The process typically takes 15-30 seconds to complete<br/>');
1439 1524 printf('• Your API key will be stored securely and shown masked below<br/>');
1440 1525 printf('• If you encounter issues, try disabling ad blockers temporarily<br/>');
1441 - printf('• Contact <a href="mailto:%s">%s</a> if you need assistance', Metasync::SUPPORT_EMAIL, Metasync::SUPPORT_EMAIL);
1526 + printf('• Contact <a href="mailto:%s">%s</a> if you need assistance', esc_attr(Metasync::SUPPORT_EMAIL), esc_html(Metasync::SUPPORT_EMAIL));
1442 1527 printf('</div>');
1443 1528 printf('</details>');
1444 1529 printf('</div>');
1445 1530
@@ -1486,14 +1571,17 @@
1486 1571 printf('</p>');
1487 1572
1488 1573 printf('</div>');
1489 1574
1575 + // The OTTO Pixel UUID is shown once, by the read-only Settings-API
1576 + // field in the OTTO Server-Side Rendering section; do not repeat it here.
1577 +
1490 1578 if( isset(Metasync::get_option('general')['searchatlas_api_key'])&&Metasync::get_option('general')['searchatlas_api_key']!=''){
1491 1579 $timestamp = @Metasync::get_option('general')['send_auth_token_timestamp'];
1492 1580 printf(
1493 1581 '<p id="sendAuthTokenTimestamp" class="descriptionValue">%s (%s)</p>',
1494 1582 esc_attr($timestamp),
1495 - $this->time_elapsed_string($timestamp)
1583 + esc_html($this->time_elapsed_string($timestamp))
1496 1584 );
1497 1585
1498 1586
1499 1587 }
@@ -1501,9 +1589,9 @@
1501 1589
1502 1590 public function bing_site_verification_callback()
1503 1591 {
1504 1592 printf(
1505 - '<input type="text" id="bing_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][bing_site_verification]" value="%s" size="50" />',
1593 + '<input type="text" id="bing_site_verification" name="' . esc_attr(Metasync_Admin::option_key) . '[searchengines][bing_site_verification]" value="%s" size="50" />',
1506 1594 isset(Metasync::get_option('searchengines')['bing_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['bing_site_verification']) : ''
1507 1595 );
1508 1596
1509 1597 printf(' <br> <span class="description"> Enter Bing Webmaster Tools verification code: </span> ');
@@ -1512,9 +1600,9 @@
1512 1600
1513 1601 public function yandex_site_verification_callback()
1514 1602 {
1515 1603 printf(
1516 - '<input type="text" id="yandex_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][yandex_site_verification]" value="%s" size="50" />',
1604 + '<input type="text" id="yandex_site_verification" name="' . esc_attr(Metasync_Admin::option_key) . '[searchengines][yandex_site_verification]" value="%s" size="50" />',
1517 1605 isset(Metasync::get_option('searchengines')['yandex_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['yandex_site_verification']) : ''
1518 1606 );
1519 1607
1520 1608 printf(' <br> <span class="description"> Enter Yandex verification code: </span>');
@@ -1523,9 +1611,9 @@
1523 1611
1524 1612 public function google_site_verification_callback()
1525 1613 {
1526 1614 printf(
1527 - '<input type="text" id="google_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][google_site_verification]" value="%s" size="50" />',
1615 + '<input type="text" id="google_site_verification" name="' . esc_attr(Metasync_Admin::option_key) . '[searchengines][google_site_verification]" value="%s" size="50" />',
1528 1616 isset(Metasync::get_option('searchengines')['google_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['google_site_verification']) : ''
1529 1617 );
1530 1618
1531 1619 printf(' <br> <span class="description"> Enter Google Search Console verification code: </span>');
@@ -1534,9 +1622,9 @@
1534 1622
1535 1623 public function pinterest_site_verification_callback()
1536 1624 {
1537 1625 printf(
1538 - '<input type="text" id="pinterest_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][pinterest_site_verification]" value="%s" size="50" />',
1626 + '<input type="text" id="pinterest_site_verification" name="' . esc_attr(Metasync_Admin::option_key) . '[searchengines][pinterest_site_verification]" value="%s" size="50" />',
1539 1627 isset(Metasync::get_option('searchengines')['pinterest_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['pinterest_site_verification']) : ''
1540 1628 );
1541 1629
1542 1630 printf(' <br> <span class="description"> Enter Pinterest verification code: </span>');
@@ -1545,12 +1633,16 @@
1545 1633
1546 1634 public function local_seo_person_organization_callback()
1547 1635 {
1548 1636 $person_organization = Metasync::get_option('localseo')['local_seo_person_organization'] ?? '';
1637 + $valid_person_organizations = array('Person', 'Organization');
1549 1638 ?>
1550 1639 <div class="metasync-type-select-wrap">
1551 1640 <select id="local_seo_person_organization" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_person_organization]') ?>">
1552 1641 <?php
1642 + if (!in_array($person_organization, $valid_person_organizations, true)) {
1643 + printf('<option value="0" selected="selected">Select Type</option>');
1644 + }
1553 1645 printf('<option value="Person" %s >Person</option>', selected('Person', esc_attr($person_organization)));
1554 1646 printf('<option value="Organization" %s >Organization</option>', selected('Organization', esc_attr($person_organization)));
1555 1647 ?>
1556 1648 </select>
@@ -1561,10 +1653,10 @@
1561 1653
1562 1654 public function local_seo_name_callback()
1563 1655 {
1564 1656 printf(
1565 - '<input type="text" id="local_seo_name" name="' . Metasync_Admin::option_key . '[localseo][local_seo_name]" value="%s" size="50" />',
1566 - isset(Metasync::get_option('localseo')['local_seo_name']) ? esc_attr(Metasync::get_option('localseo')['local_seo_name']) : get_bloginfo()
1657 + '<input type="text" id="local_seo_name" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_name]" value="%s" size="50" />',
1658 + isset(Metasync::get_option('localseo')['local_seo_name']) ? esc_attr(Metasync::get_option('localseo')['local_seo_name']) : esc_attr(get_bloginfo())
1567 1659 );
1568 1660
1569 1661 printf(' <br> <span class="description"> Your name or company name </span>');
1570 1662 }
@@ -1572,11 +1664,18 @@
1572 1664 public function local_seo_logo_callback()
1573 1665 {
1574 1666 $local_seo_logo = Metasync::get_option('localseo')['local_seo_logo'] ?? '';
1575 1667
1668 + // Rows saved by the legacy sanitizer hold the corrupted form of
1669 + // an attachment ID ("http://45589"). Repair it before anything reads
1670 + // the value — both so the preview resolves and so the hidden input
1671 + // re-submits the recovered ID instead of the corrupted string, which
1672 + // would otherwise survive every subsequent Save Settings.
1673 + $local_seo_logo = metasync_repair_scheme_prefixed_media_id($local_seo_logo);
1674 +
1576 1675 printf(
1577 - '<input type="hidden" id="local_seo_logo" name="' . Metasync_Admin::option_key . '[localseo][local_seo_logo]" value="%s" size="50" />',
1578 - isset(Metasync::get_option('localseo')['local_seo_logo']) ? esc_attr(Metasync::get_option('localseo')['local_seo_logo']) : ''
1676 + '<input type="hidden" id="local_seo_logo" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_logo]" value="%s" size="50" />',
1677 + esc_attr((string) $local_seo_logo)
1579 1678 );
1580 1679
1581 1680 printf(' <br> <input class="button-secondary no-loading" type="button" id="logo_upload_button" value="Add or Upload File">');
1582 1681
@@ -1582,23 +1681,65 @@
1582 1681
1583 1682 printf(' <br><br> <span class="description bold"> Min Size: 160Χ90px, Max Size: 1920X1080px. </span> <br> <span class="description"> A squared image is preferred by the search engines. </span> <br><br> ');
1584 1683
1585 1684 printf('<span class="metasync-logo-preview-wrap">');
1586 - printf('<img src="%s" id="local_seo_business_logo" width="300">', wp_get_attachment_image_src($local_seo_logo, 'medium')[0] ?? '');
1587 1685
1588 - $button_type = 'hidden';
1589 - if ($local_seo_logo) {
1590 - $button_type = 'button';
1686 + // The stored value may be an attachment ID or a raw URL, so resolve
1687 + // both the way the front-end schema output already does. Anything that
1688 + // resolves to nothing — a deleted attachment, a legacy value mangled by
1689 + // an earlier save — must render no preview at all, rather than an empty
1690 + // src next to an orphaned remove button.
1691 + $logo_url = $this->resolve_media_url($local_seo_logo, 'medium');
1692 +
1693 + printf(
1694 + '<img src="%s" id="local_seo_business_logo" width="300"%s>',
1695 + esc_url($logo_url),
1696 + $logo_url ? '' : ' style="display:none"'
1697 + );
1698 +
1699 + $button_type = $logo_url ? 'button' : 'hidden';
1700 + printf('<input type="%s" class="button-secondary no-loading metasync-logo-remove-btn" id="local_seo_logo_close_btn" value="X">', esc_attr($button_type));
1701 + printf('</span>');
1702 + }
1703 +
1704 + /**
1705 + * Resolve a stored media setting to a displayable URL.
1706 + *
1707 + * These settings accept either a media-library attachment ID or a raw URL;
1708 + * returns an empty string when the value resolves to neither, so callers
1709 + * can distinguish "no image" from "image we could not resolve".
1710 + *
1711 + * @param mixed $value Attachment ID or URL as stored in the options.
1712 + * @param string $size Registered image size to request for an attachment.
1713 + * @return string Resolved URL, or '' when there is nothing to show.
1714 + */
1715 + private function resolve_media_url($value, $size = 'medium')
1716 + {
1717 + if (empty($value)) {
1718 + return '';
1591 1719 }
1592 - printf('<input type="%s" class="button-secondary no-loading metasync-logo-remove-btn" id="local_seo_logo_close_btn" value="X">', $button_type);
1593 - printf('</span>');
1720 +
1721 + // A row saved by the legacy sanitizer stores a corrupted attachment ID ("http://45589"),
1722 + // which is neither resolvable as an ID nor a usable image URL.
1723 + $value = metasync_repair_scheme_prefixed_media_id($value);
1724 +
1725 + if (empty($value)) {
1726 + return '';
1727 + }
1728 +
1729 + if (is_numeric($value)) {
1730 + $url = wp_get_attachment_image_url((int) $value, $size);
1731 + return $url ? $url : '';
1732 + }
1733 +
1734 + return (string) $value;
1594 1735 }
1595 1736
1596 1737 public function local_seo_url_callback()
1597 1738 {
1598 1739 printf(
1599 - '<input type="text" id="local_seo_url" name="' . Metasync_Admin::option_key . '[localseo][local_seo_url]" value="%s" size="50" />',
1600 - isset(Metasync::get_option('localseo')['local_seo_url']) ? esc_attr(Metasync::get_option('localseo')['local_seo_url']) : home_url()
1740 + '<input type="text" id="local_seo_url" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_url]" value="%s" size="50" />',
1741 + isset(Metasync::get_option('localseo')['local_seo_url']) ? esc_attr(Metasync::get_option('localseo')['local_seo_url']) : esc_attr(home_url())
1601 1742 );
1602 1743
1603 1744 printf(' <br> <span class="description"> URL of the item. </span>');
1604 1745 }
@@ -1605,9 +1746,9 @@
1605 1746
1606 1747 public function local_seo_email_callback()
1607 1748 {
1608 1749 printf(
1609 - '<input type="text" id="local_seo_email" name="' . Metasync_Admin::option_key . '[localseo][local_seo_email]" value="%s" size="50" />',
1750 + '<input type="text" id="local_seo_email" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_email]" value="%s" size="50" />',
1610 1751 isset(Metasync::get_option('localseo')['local_seo_email']) ? esc_attr(Metasync::get_option('localseo')['local_seo_email']) : ''
1611 1752 );
1612 1753
1613 1754 printf(' <br> <span class="description"> Search engines display your email address. </span>');
@@ -1615,9 +1756,9 @@
1615 1756
1616 1757 public function local_seo_phone_callback()
1617 1758 {
1618 1759 printf(
1619 - '<input type="text" id="local_seo_phone" name="' . Metasync_Admin::option_key . '[localseo][local_seo_phone]" value="%s" size="50" />',
1760 + '<input type="text" id="local_seo_phone" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_phone]" value="%s" size="50" />',
1620 1761 isset(Metasync::get_option('localseo')['local_seo_phone']) ? esc_attr(Metasync::get_option('localseo')['local_seo_phone']) : ''
1621 1762 );
1622 1763
1623 1764 printf(' <br> <span class="description"> Search engines may prominently display your contact phone number for mobile users. </span>');
@@ -1625,29 +1766,29 @@
1625 1766
1626 1767 public function local_seo_address_callback()
1627 1768 {
1628 1769 printf(
1629 - '<input type="text" id="local_seo_address_street" name="' . Metasync_Admin::option_key . '[localseo][address][street]" value="%s" size="50" placeholder="Street Address"/> <br>',
1770 + '<input type="text" id="local_seo_address_street" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][address][street]" value="%s" size="50" placeholder="Street Address"/> <br>',
1630 1771 isset(Metasync::get_option('localseo')['address']['street']) ? esc_attr(Metasync::get_option('localseo')['address']['street']) : ''
1631 1772 );
1632 1773
1633 1774 printf(
1634 - '<input type="text" id="local_seo_address_locality" name="' . Metasync_Admin::option_key . '[localseo][address][locality]" value="%s" size="50" placeholder="Locality"/> <br>',
1775 + '<input type="text" id="local_seo_address_locality" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][address][locality]" value="%s" size="50" placeholder="Locality"/> <br>',
1635 1776 isset(Metasync::get_option('localseo')['address']['locality']) ? esc_attr(Metasync::get_option('localseo')['address']['locality']) : ''
1636 1777 );
1637 1778
1638 1779 printf(
1639 - '<input type="text" id="local_seo_address_region" name="' . Metasync_Admin::option_key . '[localseo][address][region]" value="%s" size="50" placeholder="Region"/> <br>',
1780 + '<input type="text" id="local_seo_address_region" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][address][region]" value="%s" size="50" placeholder="Region"/> <br>',
1640 1781 isset(Metasync::get_option('localseo')['address']['region']) ? esc_attr(Metasync::get_option('localseo')['address']['region']) : ''
1641 1782 );
1642 1783
1643 1784 printf(
1644 - '<input type="text" id="local_seo_address_postalcode" name="' . Metasync_Admin::option_key . '[localseo][address][postalcode]" value="%s" size="50" placeholder="Postal Code"/> <br>',
1785 + '<input type="text" id="local_seo_address_postalcode" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][address][postalcode]" value="%s" size="50" placeholder="Postal Code"/> <br>',
1645 1786 isset(Metasync::get_option('localseo')['address']['postalcode']) ? esc_attr(Metasync::get_option('localseo')['address']['postalcode']) : ''
1646 1787 );
1647 1788
1648 1789 printf(
1649 - '<input type="text" id="local_seo_address_country" name="' . Metasync_Admin::option_key . '[localseo][address][country]" value="%s" size="50" placeholder="Country"/> <br>',
1790 + '<input type="text" id="local_seo_address_country" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][address][country]" value="%s" size="50" placeholder="Country"/> <br>',
1650 1791 isset(Metasync::get_option('localseo')['address']['country']) ? esc_attr(Metasync::get_option('localseo')['address']['country']) : ''
1651 1792 );
1652 1793 }
1653 1794
@@ -1662,9 +1803,9 @@
1662 1803 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_business_type]') ?>">
1663 1804 <option value='0'>Select Business Type</option>
1664 1805 <?php
1665 1806 foreach ($types as $type) {
1666 - printf('<option value="%s" %s >%s</option>', $type, selected($type, esc_attr($business_type)), $type);
1807 + printf('<option value="%s" %s >%s</option>', esc_attr($type), selected($type, $business_type, false), esc_html($type));
1667 1808 }
1668 1809 ?>
1669 1810 </select>
1670 1811 <?php
@@ -1688,9 +1829,9 @@
1688 1829 <li>
1689 1830 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][days][]') ?>">
1690 1831 <?php
1691 1832 foreach ($days_name as $name) {
1692 - printf('<option value="%s" %s >%s</option>', $name, selected(esc_attr($name), esc_attr($day_name)), esc_attr($name));
1833 + printf('<option value="%s" %s >%s</option>', esc_attr($name), selected($name, $day_name, false), esc_html($name));
1693 1834 }
1694 1835 ?>
1695 1836 </select>
1696 1837 <input type="text" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][times][]') ?>" value="<?php echo esc_attr($day_time) ?>">
@@ -1769,9 +1910,9 @@
1769 1910
1770 1911 public function local_seo_price_range_callback()
1771 1912 {
1772 1913 printf(
1773 - '<input type="text" id="local_seo_price_range" name="' . Metasync_Admin::option_key . '[localseo][local_seo_price_range]" value="%s" size="50" />',
1914 + '<input type="text" id="local_seo_price_range" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_price_range]" value="%s" size="50" />',
1774 1915 isset(Metasync::get_option('localseo')['local_seo_price_range']) ? esc_attr(Metasync::get_option('localseo')['local_seo_price_range']) : ''
1775 1916 );
1776 1917 printf(' <br> <span class="description"> The price range of the business, for example $$$. </span>');
1777 1918 }
@@ -1784,9 +1925,9 @@
1784 1925 <?php
1785 1926 $about_page = Metasync::get_option('localseo')['local_seo_about_page'] ?? '';
1786 1927 $pages = get_pages();
1787 1928 foreach ($pages as $page) {
1788 - printf('<option value="%s" %s >%s</option>', $page->ID, selected($page->ID, esc_attr($about_page)), $page->post_title);
1929 + printf('<option value="%s" %s >%s</option>', absint($page->ID), selected($page->ID, $about_page, false), esc_html($page->post_title));
1789 1930 }
1790 1931 ?>
1791 1932 </select>
1792 1933 <?php
@@ -1801,9 +1942,9 @@
1801 1942 <?php
1802 1943 $contact_page = Metasync::get_option('localseo')['local_seo_contact_page'] ?? '';
1803 1944 $pages = get_pages();
1804 1945 foreach ($pages as $page) {
1805 - printf('<option value="%s" %s >%s</option>', $page->ID, selected($page->ID, esc_attr($contact_page)), $page->post_title);
1946 + printf('<option value="%s" %s >%s</option>', absint($page->ID), selected($page->ID, $contact_page, false), esc_html($page->post_title));
1806 1947 }
1807 1948 ?>
1808 1949 </select>
1809 1950 <?php
@@ -1812,9 +1953,9 @@
1812 1953
1813 1954 public function local_seo_map_key_callback()
1814 1955 {
1815 1956 printf(
1816 - '<input type="text" id="local_seo_map_key" name="' . Metasync_Admin::option_key . '[localseo][local_seo_map_key]" value="%s" size="50" />',
1957 + '<input type="text" id="local_seo_map_key" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_map_key]" value="%s" size="50" />',
1817 1958 isset(Metasync::get_option('localseo')['local_seo_map_key']) ? esc_attr(Metasync::get_option('localseo')['local_seo_map_key']) : ''
1818 1959 );
1819 1960
1820 1961 printf(' <br> <span class="description"> An API Key is required to display embedded Google Maps on your site. </span>');
@@ -1822,9 +1963,9 @@
1822 1963
1823 1964 public function local_seo_geo_coordinates_callback()
1824 1965 {
1825 1966 printf(
1826 - '<input type="text" id="local_seo_geo_coordinates" name="' . Metasync_Admin::option_key . '[localseo][local_seo_geo_coordinates]" value="%s" size="50" />',
1967 + '<input type="text" id="local_seo_geo_coordinates" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_geo_coordinates]" value="%s" size="50" />',
1827 1968 isset(Metasync::get_option('localseo')['local_seo_geo_coordinates']) ? esc_attr(Metasync::get_option('localseo')['local_seo_geo_coordinates']) : ''
1828 1969 );
1829 1970
1830 1971 printf(' <br> <span class="description"> Latitude and longitude values separated by comma. </span>');
@@ -1832,9 +1973,9 @@
1832 1973
1833 1974 public function header_snippets_callback()
1834 1975 {
1835 1976 printf(
1836 - '<textarea class="wide-text" id="header_snippets" rows="8" name="' . Metasync_Admin::option_key . '[codesnippets][header_snippet]" >%s</textarea>',
1977 + '<textarea class="wide-text" id="header_snippets" rows="8" name="' . esc_attr(Metasync_Admin::option_key) . '[codesnippets][header_snippet]" >%s</textarea>',
1837 1978 isset(Metasync::get_option('codesnippets')['header_snippet']) ? esc_attr(Metasync::get_option('codesnippets')['header_snippet']) : ''
1838 1979 );
1839 1980 }
1840 1981
@@ -1840,9 +1981,9 @@
1840 1981
1841 1982 public function footer_snippets_callback()
1842 1983 {
1843 1984 printf(
1844 - '<textarea class="wide-text" id="footer_snippets" rows="8" name="' . Metasync_Admin::option_key . '[codesnippets][footer_snippet]" >%s</textarea>',
1985 + '<textarea class="wide-text" id="footer_snippets" rows="8" name="' . esc_attr(Metasync_Admin::option_key) . '[codesnippets][footer_snippet]" >%s</textarea>',
1845 1986 isset(Metasync::get_option('codesnippets')['footer_snippet']) ? esc_attr(Metasync::get_option('codesnippets')['footer_snippet']) : ''
1846 1987 );
1847 1988 }
1848 1989
@@ -1979,9 +2120,9 @@
1979 2120
1980 2121 public function global_open_graph_meta_callback()
1981 2122 {
1982 2123 printf(
1983 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][open_graph_meta_tags]" value="true" %s />',
2124 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][open_graph_meta_tags]" value="true" %s />',
1984 2125 isset(Metasync::get_option('common_meta_settings')['open_graph_meta_tags']) && Metasync::get_option('common_meta_settings')['open_graph_meta_tags'] == 'true' ? 'checked' : ''
1985 2126 );
1986 2127 printf(' <br> <span class="description"> Automatically add the Open Graph meta tags in a page or post.</span>');
1987 2128 }
@@ -1988,9 +2129,9 @@
1988 2129
1989 2130 public function global_facebook_meta_callback()
1990 2131 {
1991 2132 printf(
1992 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][facebook_meta_tags]" value="true" %s />',
2133 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][facebook_meta_tags]" value="true" %s />',
1993 2134 isset(Metasync::get_option('common_meta_settings')['facebook_meta_tags']) && Metasync::get_option('common_meta_settings')['facebook_meta_tags'] == 'true' ? 'checked' : ''
1994 2135 );
1995 2136 printf(' <br> <span class="description"> Automatically add the Facebook meta tags in a page or post.</span>');
1996 2137 }
@@ -1997,9 +2138,9 @@
1997 2138
1998 2139 public function global_twitter_meta_callback()
1999 2140 {
2000 2141 printf(
2001 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][twitter_meta_tags]" value="true" %s />',
2142 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][twitter_meta_tags]" value="true" %s />',
2002 2143 isset(Metasync::get_option('common_meta_settings')['twitter_meta_tags']) && Metasync::get_option('common_meta_settings')['twitter_meta_tags'] == 'true' ? 'checked' : ''
2003 2144 );
2004 2145 printf(' <br> <span class="description"> Automatically add the Twitter meta tags in a page or post.</span>');
2005 2146 }
@@ -2007,9 +2148,9 @@
2007 2148 public function og_image_dimensions_callback()
2008 2149 {
2009 2150 $val = Metasync::get_option('common_meta_settings')['og_image_dimensions'] ?? 'true';
2010 2151 printf(
2011 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][og_image_dimensions]" value="true" %s />',
2152 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][og_image_dimensions]" value="true" %s />',
2012 2153 $val === 'true' ? 'checked' : ''
2013 2154 );
2014 2155 printf('<span class="description"> Output og:image:width, og:image:height, and og:image:type when OG image metadata is available.</span>');
2015 2156 }
@@ -2017,9 +2158,9 @@
2017 2158 public function article_timestamps_callback()
2018 2159 {
2019 2160 $val = Metasync::get_option('common_meta_settings')['article_timestamps'] ?? 'true';
2020 2161 printf(
2021 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_timestamps]" value="true" %s />',
2162 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][article_timestamps]" value="true" %s />',
2022 2163 $val === 'true' ? 'checked' : ''
2023 2164 );
2024 2165 printf('<span class="description"> Output article:published_time and article:modified_time from the post publish and modified dates (ISO 8601).</span>');
2025 2166 }
@@ -2027,9 +2168,9 @@
2027 2168 public function article_author_callback()
2028 2169 {
2029 2170 $val = Metasync::get_option('common_meta_settings')['article_author'] ?? 'true';
2030 2171 printf(
2031 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_author]" value="true" %s />',
2172 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][article_author]" value="true" %s />',
2032 2173 $val === 'true' ? 'checked' : ''
2033 2174 );
2034 2175 printf('<span class="description"> Output article:author using the post author website URL or archive URL (overridable per-post via _metasync_og_article_author meta).</span>');
2035 2176 }
@@ -2037,9 +2178,9 @@
2037 2178 public function article_section_callback()
2038 2179 {
2039 2180 $val = Metasync::get_option('common_meta_settings')['article_section'] ?? 'true';
2040 2181 printf(
2041 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_section]" value="true" %s />',
2182 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][article_section]" value="true" %s />',
2042 2183 $val === 'true' ? 'checked' : ''
2043 2184 );
2044 2185 printf('<span class="description"> Output article:section from the primary category (falls back to first category).</span>');
2045 2186 }
@@ -2047,9 +2188,9 @@
2047 2188 public function article_tags_callback()
2048 2189 {
2049 2190 $val = Metasync::get_option('common_meta_settings')['article_tags'] ?? 'true';
2050 2191 printf(
2051 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_tags]" value="true" %s />',
2192 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][article_tags]" value="true" %s />',
2052 2193 $val === 'true' ? 'checked' : ''
2053 2194 );
2054 2195 printf('<span class="description"> Output one article:tag meta tag per WordPress post tag.</span>');
2055 2196 }
@@ -2057,9 +2198,9 @@
2057 2198 public function twitter_image_alt_callback()
2058 2199 {
2059 2200 $val = Metasync::get_option('common_meta_settings')['twitter_image_alt'] ?? 'true';
2060 2201 printf(
2061 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][twitter_image_alt]" value="true" %s />',
2202 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][twitter_image_alt]" value="true" %s />',
2062 2203 $val === 'true' ? 'checked' : ''
2063 2204 );
2064 2205 printf('<span class="description"> Output twitter:image:alt using the stored alt text or the OG image attachment alt attribute.</span>');
2065 2206 }
@@ -2066,9 +2207,9 @@
2066 2207
2067 2208 public function facebook_page_url_callback()
2068 2209 {
2069 2210 $facebook_page_url = Metasync::get_option('social_meta')['facebook_page_url'] ?? '';
2070 - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_page_url]" value="%s" size="50" />', esc_attr($facebook_page_url));
2211 + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_page_url]" value="%s" size="50" />', esc_attr($facebook_page_url));
2071 2212 printf('<br><span class="description"> Enter your Facebook page URL. eg: <code>https://www.facebook.com/MetaSync/</code> </span>');
2072 2213 }
2073 2214
2074 2215 public function facebook_authorship_callback()
@@ -2073,9 +2214,9 @@
2073 2214
2074 2215 public function facebook_authorship_callback()
2075 2216 {
2076 2217 $facebook_authorship = Metasync::get_option('social_meta')['facebook_authorship'] ?? '';
2077 - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_authorship]" value="%s" size="50" />', esc_attr($facebook_authorship));
2218 + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_authorship]" value="%s" size="50" />', esc_attr($facebook_authorship));
2078 2219 printf('<br><span class="description"> Enter Facebook profile URL to show Facebook Authorship when your articles are being shared on Facebook. eg: <code>https://www.facebook.com/shahrukh/</code> </span>');
2079 2220 }
2080 2221
2081 2222 public function facebook_admin_callback()
@@ -2080,9 +2221,9 @@
2080 2221
2081 2222 public function facebook_admin_callback()
2082 2223 {
2083 2224 $facebook_admin = Metasync::get_option('social_meta')['facebook_admin'] ?? '';
2084 - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_admin]" value="%s" size="50" />', esc_attr($facebook_admin));
2225 + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_admin]" value="%s" size="50" />', esc_attr($facebook_admin));
2085 2226 printf(' <br> <span class="description"> Enter numeric user ID of Facebook. </span>');
2086 2227 }
2087 2228
2088 2229 public function facebook_app_callback()
@@ -2087,9 +2228,9 @@
2087 2228
2088 2229 public function facebook_app_callback()
2089 2230 {
2090 2231 $facebook_app = Metasync::get_option('social_meta')['facebook_app'] ?? '';
2091 - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_app]" value="%s" size="50" />', esc_attr($facebook_app));
2232 + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_app]" value="%s" size="50" />', esc_attr($facebook_app));
2092 2233 printf(' <br> <span class="description"> Enter numeric app ID of Facebook </span>');
2093 2234 }
2094 2235
2095 2236 public function facebook_secret_callback()
@@ -2094,9 +2235,9 @@
2094 2235
2095 2236 public function facebook_secret_callback()
2096 2237 {
2097 2238 $facebook_secret = Metasync::get_option('social_meta')['facebook_secret'] ?? '';
2098 - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_secret]" value="%s" size="50" />', esc_attr($facebook_secret));
2239 + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_secret]" value="%s" size="50" />', esc_attr($facebook_secret));
2099 2240 printf(' <br> <span class="description"> Enter alphanumeric access token from Facebook. </span>');
2100 2241 }
2101 2242
2102 2243 public function twitter_username_callback()
@@ -2101,9 +2242,9 @@
2101 2242
2102 2243 public function twitter_username_callback()
2103 2244 {
2104 2245 $twitter_username = Metasync::get_option('social_meta')['twitter_username'] ?? '';
2105 - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][twitter_username]" value="%s" size="50" />', esc_attr($twitter_username));
2246 + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][twitter_username]" value="%s" size="50" />', esc_attr($twitter_username));
2106 2247 printf(' <br> <span class="description"> Twitter username of the author to add <code>twitter:creator</code> tag to post. eg: <code>MetaSync</code> </span>');
2107 2248 }
2108 2249
2109 2250 public static function get_business_types()
@@ -2278,8 +2419,14 @@
2278 2419 }
2279 2420
2280 2421 /**
2281 2422 * Render breadcrumbs settings section
2423 + *
2424 + * Rendered by the dedicated Breadcrumbs admin page. The three checkbox
2425 + * fields each ship a hidden companion input so their keys are always
2426 + * present in the submitted payload, which lets the save handlers treat an
2427 + * absent key as "this form does not manage that field" instead of
2428 + * "unchecked".
2282 2429 */
2283 2430 public function render_breadcrumbs_section() {
2284 2431 $options = Metasync::get_option('breadcrumbs', array());
2285 2432
@@ -2284,9 +2431,9 @@
2284 2431 $options = Metasync::get_option('breadcrumbs', array());
2285 2432
2286 2433 $defaults = array(
2287 2434 'enabled' => true,
2288 - 'separator' => '&raquo;',
2435 + 'separator' => '»',
2289 2436 'home_label' => 'Home',
2290 2437 'home_url' => '',
2291 2438 'show_current_page' => true,
2292 2439 'prefix_text' => '',
@@ -2294,8 +2441,19 @@
2294 2441 'disable_schema' => false,
2295 2442 );
2296 2443
2297 2444 $options = wp_parse_args($options, $defaults);
2445 +
2446 + # Older builds stored the separator as an HTML entity (e.g. "&raquo;").
2447 + # Decode before matching so the saved choice still lights up its radio.
2448 + $separator_presets = array('»', '/', '>', '·');
2449 + $separator_current = html_entity_decode((string) $options['separator'], ENT_QUOTES, 'UTF-8');
2450 + if ($separator_current === '') {
2451 + $separator_current = '»';
2452 + }
2453 + # A separator that predates the radio picker (or came from the old free
2454 + # text field) gets its own option so it stays selected and round-trips.
2455 + $separator_custom = in_array($separator_current, $separator_presets, true) ? '' : $separator_current;
2298 2456 ?>
2299 2457 <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;">
2300 2458
2301 2459 <!-- Enable/Disable -->
@@ -2300,8 +2458,9 @@
2300 2458
2301 2459 <!-- Enable/Disable -->
2302 2460 <div style="margin-bottom: 24px;">
2303 2461 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2462 + <input type="hidden" name="metasync_options[breadcrumbs][enabled]" value="0">
2304 2463 <input type="checkbox"
2305 2464 name="metasync_options[breadcrumbs][enabled]"
2306 2465 value="1"
2307 2466 <?php checked($options['enabled'], 1); ?>
@@ -2316,8 +2475,9 @@
2316 2475
2317 2476 <!-- Disable Schema Markup -->
2318 2477 <div style="margin-bottom: 24px;">
2319 2478 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2479 + <input type="hidden" name="metasync_options[breadcrumbs][disable_schema]" value="0">
2320 2480 <input type="checkbox"
2321 2481 name="metasync_options[breadcrumbs][disable_schema]"
2322 2482 value="1"
2323 2483 <?php checked($options['disable_schema'], 1); ?>
@@ -2335,24 +2495,21 @@
2335 2495 <label for="breadcrumb_separator" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2336 2496 Separator Character
2337 2497 </label>
2338 2498 <div style="display: flex; gap: 12px; flex-wrap: wrap;">
2499 + <?php foreach ($separator_presets as $separator_option) : ?>
2339 2500 <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2340 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value="»" <?php checked($options['separator'], '»'); ?>>
2341 - <span style="color: var(--dashboard-text);">»</span>
2501 + <input type="radio" name="metasync_options[breadcrumbs][separator]" value="<?php echo esc_attr($separator_option); ?>" <?php checked($separator_current, $separator_option); ?>>
2502 + <span style="color: var(--dashboard-text);"><?php echo esc_html($separator_option); ?></span>
2342 2503 </label>
2343 - <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2344 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value="/" <?php checked($options['separator'], '/'); ?>>
2345 - <span style="color: var(--dashboard-text);">/</span>
2346 - </label>
2347 - <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2348 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value=">" <?php checked($options['separator'], '>'); ?>>
2349 - <span style="color: var(--dashboard-text);">></span>
2350 - </label>
2351 - <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2352 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value="·" <?php checked($options['separator'], '·'); ?>>
2353 - <span style="color: var(--dashboard-text);">·</span>
2354 - </label>
2504 + <?php endforeach; ?>
2505 + <div style="display: flex; align-items: center; gap: 8px;">
2506 + <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2507 + <input type="radio" id="metasync-breadcrumb-separator-custom" name="metasync_options[breadcrumbs][separator]" value="<?php echo esc_attr($separator_custom); ?>" <?php checked($separator_custom !== ''); ?>>
2508 + <span style="color: var(--dashboard-text);">Custom</span>
2509 + </label>
2510 + <input type="text" value="<?php echo esc_attr($separator_custom); ?>" placeholder="~" maxlength="16" aria-label="Custom separator character" oninput="var r=document.getElementById('metasync-breadcrumb-separator-custom');r.value=this.value;r.checked=true" style="width: 80px; padding: 8px 10px; background: var(--dashboard-input-bg); border: 1px solid var(--dashboard-border); border-radius: 6px; color: var(--dashboard-text); font-size: 14px;">
2511 + </div>
2355 2512 </div>
2356 2513 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2357 2514 Choose how to separate breadcrumb items
2358 2515 </p>
@@ -2392,8 +2549,9 @@
2392 2549
2393 2550 <!-- Show Current Page -->
2394 2551 <div style="margin-bottom: 24px;">
2395 2552 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2553 + <input type="hidden" name="metasync_options[breadcrumbs][show_current_page]" value="0">
2396 2554 <input type="checkbox"
2397 2555 name="metasync_options[breadcrumbs][show_current_page]"
2398 2556 value="1"
2399 2557 <?php checked($options['show_current_page'], 1); ?>