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
metasync / includes / class-metasync-settings-fields.php

class-metasync-settings-fields.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.1, at includes/class-metasync-settings-fields.php

2,569 lines 141.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // If this file is called directly, abort.
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * Settings field callbacks and accordion/section helpers extracted from Metasync_Admin.
9 *
10 * @package Metasync
11 * @subpackage Metasync/includes
12 */
13 class Metasync_Settings_Fields {
14
15 private static $instance = null;
16
17 /**
18 * Reference to the admin instance for callbacks to non-extracted admin methods.
19 *
20 * @var Metasync_Admin|null
21 */
22 private $admin_instance = null;
23
24 private function __construct() {}
25
26 /**
27 * @return self
28 */
29 public static function instance() {
30 if (null === self::$instance) {
31 self::$instance = new self();
32 }
33 return self::$instance;
34 }
35
36 /**
37 * Inject the admin instance so callbacks to non-extracted methods still work.
38 *
39 * @param Metasync_Admin $admin
40 */
41 public function set_admin_instance($admin) {
42 $this->admin_instance = $admin;
43 }
44
45 // ────────────────────────────────────────────────────────────────
46 // Accordion / Section helpers
47 // ────────────────────────────────────────────────────────────────
48
49 public function get_accordion_sections_config() {
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 'connection' => array(
58 'title' => 'Connection & Authentication',
59 'description' => 'Manage your API connection and authentication settings',
60 'icon' => 'admin-network',
61 'priority' => 10,
62 'default_open' => true,
63 'fields' => array(
64 'searchatlas_api_key',
65 'apikey'
66 )
67 ),
68 'otto_ssr' => array(
69 'title' => Metasync::get_whitelabel_otto_name() . ' Server-Side Rendering',
70 'description' => 'Configure ' . Metasync::get_whitelabel_otto_name() . ' rendering and display options',
71 'icon' => 'performance',
72 'priority' => 20,
73 'default_open' => false,
74 'fields' => array(
75 'otto_pixel_uuid',
76 'otto_disable_on_loggedin',
77 'otto_disable_preview_button',
78 'seo_priority',
79 'otto_wp_rocket_compat',
80 'otto_sg_optimizer_compat',
81 'bookingpress_session_compat',
82 )
83 ),
84 'edge_cache' => array(
85 'title' => 'Edge Cache / CDN',
86 'description' => 'Configure CDN credentials for cache-tag purging',
87 'icon' => 'admin-site-alt3',
88 'priority' => 22,
89 'default_open' => false,
90 'render_callback' => array('Metasync_Edge_Cache_Settings', 'render'),
91 ),
92 'seo_plugin_sync' => array(
93 'title' => 'Sync to Other SEO Plugins',
94 'description' => 'Control whether ' . Metasync::get_whitelabel_otto_name() . '\'s SEO values are written into Yoast, Rank Math or All in One SEO',
95 'icon' => 'update',
96 'priority' => 23,
97 'default_open' => false,
98 'render_callback' => array('Metasync_Seo_Sync_Settings', 'render'),
99 ),
100 'bot_detection' => array(
101 'title' => 'Bot Detection & Filtering',
102 'description' => 'Manage bot traffic and reduce unnecessary API calls',
103 'icon' => 'filter',
104 'priority' => 25,
105 'default_open' => false,
106 'fields' => array(
107 'otto_disable_for_bots',
108 'otto_bot_whitelist',
109 'otto_bot_blacklist',
110 'otto_bot_statistics_link'
111 )
112 ),
113 'open_graph' => array(
114 'title' => 'Open Graph',
115 'description' => 'Control which Open Graph and article meta tags are output',
116 'icon' => 'share',
117 'priority' => 38,
118 'default_open' => false,
119 'fields' => array(
120 'og_image_dimensions',
121 'article_timestamps',
122 'article_author',
123 'article_section',
124 'article_tags',
125 'twitter_image_alt'
126 )
127 ),
128 'editor_settings' => array(
129 'title' => 'Post/Page Editor Settings',
130 'description' => 'Customize meta boxes and editor functionality',
131 'icon' => 'edit',
132 'priority' => 40,
133 'default_open' => false,
134 'fields' => array(
135 'disable_common_robots_metabox',
136 'disable_advance_robots_metabox',
137 'disable_redirection_metabox',
138 'disable_canonical_metabox',
139 'disable_social_opengraph_metabox',
140 'disable_schema_markup_metabox',
141 'disable_language_alternates_metabox',
142 'disable_seo_metabox',
143 'open_external_links'
144 )
145 ),
146 'user_management' => array(
147 'title' => 'User Management for Content',
148 'description' => 'Configure which users are allowed to be authors of content synced.',
149 'icon' => 'groups',
150 'priority' => 50,
151 'default_open' => false,
152 'fields' => array(
153 'content_genius_sync_roles'
154 )
155 ),
156 'content_rendering' => array(
157 'title' => 'Content Rendering',
158 'description' => 'Configure how synced content is stored and rendered by your page builder',
159 'icon' => 'admin-appearance',
160 'priority' => 55,
161 'default_open' => false,
162 'fields' => array(
163 'default_page_builder'
164 )
165 ),
166 'llms_txt' => array(
167 'title' => 'LLMs.txt',
168 'description' => 'Publish a standards-compliant /llms.txt so AI search engines and assistants can discover your content',
169 'icon' => 'media-text',
170 'priority' => 56,
171 'default_open' => false,
172 'render_callback' => array($this, 'render_llms_txt_section'),
173 ),
174 'advanced' => array(
175 'title' => 'Plugin Settings',
176 'description' => 'System configuration and maintenance options',
177 'icon' => 'admin-settings',
178 'priority' => 60,
179 'default_open' => false,
180 'fields' => array(
181 'permalink_structure',
182 'hide_dashboard_framework',
183 'show_admin_bar_status',
184 'import_external_data'
185 )
186 )
187 );
188
189 // Hidden only when there is nothing to say: no SEO plugin installed, the
190 // switch off, and no saved originals. It is never hidden by the
191 // access-control or whitelabel flags that can conceal Advanced Settings
192 // — a switch that rewrites another plugin's data must stay reachable.
193 if (!Metasync_Seo_Sync_Settings::should_display()) {
194 unset($sections['seo_plugin_sync']);
195 }
196
197 return $sections;
198 }
199
200 public function get_advanced_accordion_config() {
201 $config = array();
202
203 $user = wp_get_current_user();
204 if (in_array('administrator', (array) $user->roles)) {
205 $config['plugin_access'] = array(
206 'title' => 'User Roles with Plugin Access',
207 'description' => 'Control which user roles can see and access this plugin',
208 'icon' => 'admin-users',
209 'priority' => 5,
210 'default_open' => false,
211 'render_callback' => array($this, 'render_plugin_access_roles_section')
212 );
213 }
214
215 $config['debug_mode'] = array(
216 'title' => 'Debug Mode',
217 'description' => 'Manage debug mode with automatic disable and safety limits',
218 'icon' => 'info',
219 'priority' => 8,
220 'default_open' => false,
221 'render_callback' => array($this->admin_instance, 'render_debug_mode_section')
222 );
223
224 $config['headless_mode'] = array(
225 'title' => 'Headless Mode',
226 'description' => 'Connect WordPress content to a separate public frontend',
227 'icon' => 'admin-site-alt3',
228 'priority' => 29,
229 'default_open' => false,
230 'render_callback' => array(Metasync_Settings_Registration::instance(), 'render_headless_mode_section')
231 );
232
233 $config['error_logs'] = array(
234 'title' => 'Error Logs',
235 'description' => 'View and manage error logs to troubleshoot issues',
236 'icon' => 'warning',
237 'priority' => 10,
238 'default_open' => true,
239 'render_callback' => array($this->admin_instance, 'render_error_log_content')
240 );
241
242 $config['execution_settings'] = array(
243 'title' => 'Execution Settings',
244 'description' => 'Configure resource limits and execution parameters',
245 'icon' => 'controls-play',
246 'priority' => 15,
247 'default_open' => false,
248 'render_callback' => array($this, 'render_execution_settings_section')
249 );
250
251 $config['otto_cache'] = array(
252 'title' => 'Cache Management',
253 'description' => 'Manage ' . Metasync::get_whitelabel_otto_name() . ' cache and clear all cache plugins',
254 'icon' => 'database',
255 'priority' => 20,
256 'default_open' => false,
257 'render_callback' => array($this->admin_instance, 'render_otto_cache_management')
258 );
259
260 $config['db_cleanup'] = array(
261 'title' => 'Database Cleanup',
262 'description' => 'Remove orphaned data and schedule automated weekly cleanup',
263 'icon' => 'trash',
264 'priority' => 25,
265 'default_open' => false,
266 'render_callback' => array($this->admin_instance, 'render_db_cleanup_section')
267 );
268
269 $config['performance'] = array(
270 'title' => 'Performance & CPU Load',
271 'description' => 'Configure CPU load thresholds to prevent processing during peak server load',
272 'icon' => 'chart-line',
273 'priority' => 27,
274 'default_open' => false,
275 'render_callback' => array($this->admin_instance, 'render_cpu_monitor_section')
276 );
277
278 $config['reset_settings'] = array(
279 'title' => 'Reset Plugin Settings',
280 'description' => 'Reset all plugin settings to default values',
281 'icon' => 'update',
282 'priority' => 30,
283 'default_open' => false,
284 'render_callback' => array($this, 'render_reset_settings_section')
285 );
286
287 return $config;
288 }
289
290 public function render_advanced_accordion() {
291 $sections_config = $this->get_advanced_accordion_config();
292
293 uasort($sections_config, function($a, $b) {
294 return $a['priority'] - $b['priority'];
295 });
296
297 echo '<div class="metasync-settings-accordion">';
298
299 foreach ($sections_config as $section_key => $section_data) {
300 $section_id = 'metasync-advanced-section-' . $section_key;
301 $is_open = $section_data['default_open'];
302 $aria_expanded = $is_open ? 'true' : 'false';
303 $content_state = $is_open ? 'open' : 'closed';
304
305 echo '<div class="metasync-accordion-section" data-section="' . esc_attr($section_key) . '">';
306
307 echo '<div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="' . esc_attr($aria_expanded) . '" aria-controls="' . esc_attr($section_id) . '">';
308 echo '<div class="metasync-accordion-title">';
309 echo '<span class="metasync-accordion-icon"><span class="dashicons dashicons-' . esc_attr($section_data['icon']) . '"></span></span>';
310 echo '<div class="metasync-accordion-text">';
311 echo '<h3>' . esc_html($section_data['title']) . '</h3>';
312 echo '<p class="metasync-accordion-description">' . esc_html($section_data['description']) . '</p>';
313 echo '</div>';
314 echo '</div>';
315 echo '<button type="button" class="metasync-accordion-toggle" aria-label="Toggle section">';
316 echo '<span class="toggle-icon">▼</span>';
317 echo '</button>';
318 echo '</div>';
319
320 echo '<div class="metasync-accordion-content" id="' . esc_attr($section_id) . '" data-state="' . esc_attr($content_state) . '">';
321
322 if (isset($section_data['render_callback']) && is_callable($section_data['render_callback'])) {
323 call_user_func($section_data['render_callback']);
324 }
325
326 echo '</div>';
327 echo '</div>';
328 }
329
330 echo '</div>';
331 }
332
333 public function render_reset_settings_section() {
334 ?>
335 <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;">
336 <div style="background: rgba(255, 243, 205, 0.1); border: 1px solid rgba(255, 234, 167, 0.3); border-radius: 8px; padding: 20px; margin-bottom: 20px;">
337 <h4 style="color: var(--dashboard-warning, #f59e0b); margin: 0 0 12px 0; display: flex; align-items: center; gap: 8px;">
338 <span class="dashicons dashicons-warning" style="color:var(--dashboard-warning,#f59e0b);font-size:18px;width:18px;height:18px;"></span>
339 <span>Important Warning</span>
340 </h4>
341 <p style="color: var(--dashboard-text-secondary); margin: 0 0 12px 0;">This action will permanently delete:</p>
342 <ul style="color: var(--dashboard-text-secondary); margin: 0 0 12px 20px; line-height: 1.8;">
343 <li>All API keys and authentication tokens</li>
344 <li>White label branding settings</li>
345 <li>Plugin configuration and preferences</li>
346 <li>Instant indexing settings</li>
347 <li>Media optimization settings and cache</li>
348 <li>Code minification settings</li>
349 <li>All cached data and crawl information</li>
350 </ul>
351 <p style="color: var(--dashboard-warning, #f59e0b); margin: 0; font-weight: 600;">You will need to reconfigure the plugin completely after this reset.</p>
352 </div>
353 <form method="post" action="" onsubmit="return confirmClearSettings(event)">
354 <?php wp_nonce_field('metasync_clear_all_settings_nonce', 'clear_all_settings_nonce'); ?>
355 <input type="hidden" name="clear_all_settings" value="yes" />
356 <button type="submit" class="metasync-btn-danger" style="background: var(--dashboard-error, #ef4444); color: #ffffff; border: none; padding: 10px 20px; border-radius: 8px; font-weight: 500; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 4px rgba(239, 68, 68, 0.2);" onmouseover="this.style.background='#dc2626'; this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 8px rgba(239, 68, 68, 0.3)';" onmouseout="this.style.background='var(--dashboard-error, #ef4444)'; this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 4px rgba(239, 68, 68, 0.2)';">
357 <span class="dashicons dashicons-trash" style="margin-top:3px;font-size:15px;width:15px;height:15px;"></span> Clear All Settings
358 </button>
359 </form>
360 </div>
361 <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px; margin-top: 20px;">
362 <h4 style="color: var(--dashboard-text-primary, #1e1e1e); margin: 0 0 12px 0; display: flex; align-items: center; gap: 8px;">
363 <span class="dashicons dashicons-universal-access-alt" style="font-size:18px;width:18px;height:18px;"></span>
364 <span>Delete data on uninstall</span>
365 </h4>
366 <p style="color: var(--dashboard-text-secondary); margin: 0 0 12px 0;">
367 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.
368 </p>
369 <p style="color: var(--dashboard-text-secondary); margin: 0 0 12px 0;">
370 The Google service account and the IndexNow API key are always deleted on uninstall, regardless of this setting.
371 </p>
372 <form method="post" action="">
373 <?php wp_nonce_field('metasync_uninstall_data_toggle_nonce', 'uninstall_data_toggle_nonce'); ?>
374 <input type="hidden" name="metasync_uninstall_data_toggle_form" value="1" />
375 <label style="color: var(--dashboard-text-primary, #1e1e1e); display: flex; align-items: center; gap: 8px; margin: 0 0 12px 0; cursor: pointer;">
376 <input type="checkbox" name="metasync_delete_data_on_uninstall" value="yes" <?php checked(get_option('metasync_delete_data_on_uninstall', 'no'), 'yes'); ?> />
377 Delete all plugin data (tables, settings, per-post SEO values, logs) when the plugin is uninstalled
378 </label>
379 <button type="submit" class="button button-secondary">
380 Save Uninstall Setting
381 </button>
382 </form>
383 </div>
384 <script>
385 function confirmClearSettings(event) {
386 event.preventDefault();
387
388 var firstConfirm = confirm("⚠️ WARNING: This will permanently delete ALL plugin settings!\n\nThis action cannot be undone. Are you sure you want to continue?");
389 if (!firstConfirm) {
390 return false;
391 }
392
393 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.");
394 if (!secondConfirm) {
395 return false;
396 }
397
398 var typeConfirm = prompt("Type 'DELETE' (in capital letters) to confirm you want to permanently clear all settings:");
399 if (typeConfirm !== 'DELETE') {
400 alert("Settings reset cancelled. Type 'DELETE' exactly to confirm.");
401 return false;
402 }
403
404 event.target.submit();
405 return false;
406 }
407 </script>
408 <?php
409 }
410
411 public function render_google_index_section() {
412 if (!function_exists('google_index_direct')) {
413 if (file_exists(plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php')) {
414 require_once plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php';
415 } else {
416 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
417 return;
418 }
419 }
420
421 $google_index = google_index_direct();
422 if (!$google_index) {
423 // Nothing to render against, and calling through would be fatal.
424 return;
425 }
426 $service_info = $google_index->get_service_account_info();
427 $is_configured = !isset($service_info['error']);
428
429 $saved_json_display = $is_configured ? $google_index->get_redacted_config_json() : '';
430
431 include plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-index-api-settings.php';
432
433 // Render post types selection
434 $options = get_option('metasync_options_instant_indexing', ['post_types' => []]);
435 $post_types_settings = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : [];
436 include plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-instant-post-types.php';
437 }
438
439 public function render_bing_index_section() {
440 require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
441 $bing_index = new Metasync_Bing_Instant_Index();
442 $api_key = $bing_index->get_setting('api_key');
443 $endpoint = $bing_index->get_setting('endpoint', 'indexnow');
444 $post_types_settings = $bing_index->get_setting('post_types', []);
445 $is_configured = !empty($api_key);
446
447 $post_types = get_post_types(['public' => true], 'objects');
448
449 ?>
450 <div style="padding: 20px;">
451 <!-- About IndexNow (Collapsible) -->
452 <details style="margin-bottom: 20px;">
453 <summary style="cursor: pointer; font-weight: 600; color: var(--dashboard-text-primary);">
454 About IndexNow Protocol
455 </summary>
456 <div style="margin-top: 10px; padding: 15px; background: rgba(255, 255, 255, 0.03); border-radius: 4px;">
457 <p style="color: var(--dashboard-text-secondary); margin: 0 0 10px 0;">
458 IndexNow is a simple protocol that allows websites to instantly notify search engines about URL changes.
459 It's supported by Bing, Yandex, Naver, Seznam, and other search engines.
460 </p>
461 <ul style="color: var(--dashboard-text-secondary); margin: 0 0 10px 20px;">
462 <li>✓ Instant notification to multiple search engines</li>
463 <li>✓ Simple API key authentication (no OAuth required)</li>
464 <li>✓ Supports batch URL submissions (up to 10,000 URLs)</li>
465 <li>✓ Free to use with no quotas</li>
466 <li>✓ Fire-and-forget protocol (no status checking needed)</li>
467 </ul>
468 <p style="color: var(--dashboard-text-secondary); margin: 0;">
469 <strong>Resources:</strong>
470 <a href="https://www.indexnow.org/documentation" target="_blank" style="color: var(--dashboard-accent);">IndexNow Documentation</a> |
471 <a href="https://www.bing.com/webmasters" target="_blank" style="color: var(--dashboard-accent);">Bing Webmaster Tools</a>
472 </p>
473 </div>
474 </details>
475
476 <!-- Configuration Status -->
477 <div style="margin-bottom: 20px;">
478 <?php if ($is_configured): ?>
479 <div style="padding: 12px; background: rgba(76, 175, 80, 0.1); border-left: 4px solid #4caf50; border-radius: 4px;">
480 <p style="margin: 0; color: var(--dashboard-text-primary);">
481 <strong style="color: #4caf50;">IndexNow API Configured</strong><br>
482 <span style="color: var(--dashboard-text-secondary); font-size: 0.95em;">
483 API Key: <code style="padding: 2px 6px; background: rgba(0,0,0,0.1); border-radius: 3px;"><?php echo esc_html(substr($api_key, 0, 8) . '...' . substr($api_key, -8)); ?></code>
484 </span>
485 </p>
486 </div>
487 <?php else: ?>
488 <div style="padding: 12px; background: rgba(255, 152, 0, 0.1); border-left: 4px solid #ff9800; border-radius: 4px;">
489 <p style="margin: 0; color: var(--dashboard-text-primary);">
490 <strong style="color: #ff9800;">Configuration Required</strong><br>
491 <span style="color: var(--dashboard-text-secondary); font-size: 0.95em;">
492 Configure your IndexNow API key below to enable instant indexing with Bing and other search engines.
493 </span>
494 </p>
495 </div>
496 <?php endif; ?>
497 </div>
498
499 <!-- Settings Fields -->
500 <table class="form-table" style="margin-top: 0;">
501 <tr>
502 <th scope="row" style="width: 200px; padding-top: 15px;">
503 <label for="metasync_bing_api_key_inline">IndexNow API Key <span style="color: #d63638;">*</span></label>
504 <?php Metasync::render_tooltip_icon('metasync_bing_api_key_inline', 'This key lets Bing trust indexing requests from your site. Click \'Generate Random API Key\' if you don\'t have one — the plugin publishes the matching verification file automatically, no manual upload needed.'); ?>
505 </th>
506 <td style="padding-top: 15px;">
507 <input type="text"
508 name="metasync_bing_api_key_inline"
509 id="metasync_bing_api_key_inline"
510 class="large-text"
511 value="<?php echo esc_attr($api_key); ?>"
512 placeholder="Enter your IndexNow API key (32+ character hexadecimal string)" />
513 <br>
514 <button type="button"
515 id="generate-bing-api-key-inline"
516 class="button button-secondary"
517 style="margin-top: 8px;">
518 Generate Random API Key
519 </button>
520 <p class="description" style="margin-top: 8px;">
521 Your IndexNow API key is required to submit URLs for instant indexing. You can generate a random key or use your own (32+ character hexadecimal string).
522 After saving, a verification file will be automatically created at your site root.
523 </p>
524 </td>
525 </tr>
526
527 <tr>
528 <th scope="row" style="padding-top: 15px;">
529 <label>API Endpoint</label>
530 </th>
531 <td style="padding-top: 15px;">
532 <fieldset>
533 <label style="display: block; margin-bottom: 8px;">
534 <input type="radio"
535 name="metasync_bing_endpoint_inline"
536 value="indexnow"
537 <?php checked($endpoint, 'indexnow'); ?>>
538 <strong>IndexNow.org</strong> (Recommended)
539 <span style="color: var(--dashboard-text-secondary); font-size: 0.9em; display: block; margin-left: 24px;">
540 Notifies Bing, Yandex, Naver, Seznam, and other participating search engines
541 </span>
542 </label>
543 <label style="display: block;">
544 <input type="radio"
545 name="metasync_bing_endpoint_inline"
546 value="bing"
547 <?php checked($endpoint, 'bing'); ?>>
548 <strong>Bing.com</strong> (Bing-specific)
549 <span style="color: var(--dashboard-text-secondary); font-size: 0.9em; display: block; margin-left: 24px;">
550 Direct submission to Bing only
551 </span>
552 </label>
553 </fieldset>
554 <p class="description">
555 Select which endpoint to use for submitting URLs. The IndexNow.org endpoint is recommended as it notifies multiple search engines at once.
556 </p>
557 </td>
558 </tr>
559
560 <tr>
561 <th scope="row" style="padding-top: 15px;">
562 <label>Auto-Submit Post Types</label>
563 </th>
564 <td style="padding-top: 15px;">
565 <fieldset>
566 <?php foreach ($post_types as $post_type): ?>
567 <label style="display: inline-block; margin-right: 20px; margin-bottom: 8px;">
568 <input type="checkbox"
569 name="metasync_bing_post_types_inline[<?php echo esc_attr($post_type->name); ?>]"
570 value="<?php echo esc_attr($post_type->name); ?>"
571 <?php checked(in_array($post_type->name, $post_types_settings, true)); ?>>
572 <?php echo esc_html($post_type->label); ?>
573 </label>
574 <?php endforeach; ?>
575 </fieldset>
576 <p class="description">
577 Selected post types will be automatically submitted to IndexNow when published or updated.
578 </p>
579 </td>
580 </tr>
581
582 <tr>
583 <th scope="row" style="padding-top: 15px;">
584 <label>Plugin Source Control</label>
585 </th>
586 <td style="padding-top: 15px;">
587 <fieldset>
588 <label style="display: flex; align-items: flex-start; gap: 8px;">
589 <input type="checkbox"
590 name="metasync_bing_disable_other_plugins_inline"
591 value="1"
592 <?php checked($bing_index->get_setting('disable_other_plugins', true), true); ?>
593 style="margin-top: 2px;">
594 <span>
595 <strong>Disable other IndexNow plugins</strong>
596 <span style="display: block; color: var(--dashboard-text-secondary); font-size: 0.9em; margin-top: 4px; font-weight: normal;">
597 When enabled, Our plugin will be the exclusive source for IndexNow submissions in Bing Webmaster Tools.
598 This disables IndexNow features in Yoast SEO, Rank Math, and other competing plugins to prevent duplicate submissions.
599 </span>
600 </span>
601 </label>
602 </fieldset>
603 </td>
604 </tr>
605 </table>
606
607 </div>
608
609 <script>
610 jQuery(document).ready(function($) {
611 $('#generate-bing-api-key-inline').on('click', function() {
612 const array = new Uint8Array(16);
613 crypto.getRandomValues(array);
614 const hexString = Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
615 $('#metasync_bing_api_key_inline').val(hexString);
616
617 $('#enable_binginstantindex').prop('checked', true);
618
619 $('#metasync_bing_api_key_inline').trigger('change');
620 });
621
622 $('#metasync_bing_api_key_inline').on('input', function() {
623 const apiKey = $(this).val().trim();
624 if (apiKey.length >= 8) {
625 $('#enable_binginstantindex').prop('checked', true);
626 }
627 });
628
629 function toggleBingConfig() {
630 const isEnabled = $('#enable_binginstantindex').is(':checked');
631 const $apiConfig = $('#enable_binginstantindex').closest('tr').nextAll('tr').first();
632 if (isEnabled) {
633 $apiConfig.show();
634 } else {
635 $apiConfig.hide();
636 }
637 }
638
639 toggleBingConfig();
640
641 $('#enable_binginstantindex').on('change', toggleBingConfig);
642 });
643 </script>
644
645 <style>
646 .form-table th {
647 color: var(--dashboard-text-primary);
648 }
649
650 .form-table td {
651 color: var(--dashboard-text-secondary);
652 }
653
654 .form-table code {
655 padding: 2px 6px;
656 background: rgba(0, 0, 0, 0.1);
657 border-radius: 3px;
658 font-size: 0.9em;
659 }
660
661 details summary {
662 transition: color 0.2s;
663 }
664
665 details summary:hover {
666 color: var(--dashboard-accent);
667 }
668
669 details[open] summary {
670 margin-bottom: 10px;
671 }
672 </style>
673 <?php
674 }
675
676 public function render_plugin_access_roles_section() {
677 $general_options = Metasync::get_option('general');
678
679 $setting_exists = isset($general_options['plugin_access_roles']);
680 $selected_roles = $general_options['plugin_access_roles'] ?? null;
681
682 if (is_string($selected_roles)) {
683 $selected_roles = array($selected_roles);
684 } elseif (!is_array($selected_roles)) {
685 $selected_roles = null;
686 }
687
688 if (!function_exists('wp_roles')) {
689 require_once(ABSPATH . 'wp-admin/includes/user.php');
690 }
691 global $wp_roles;
692
693 if (!isset($wp_roles) || !is_object($wp_roles)) {
694 $wp_roles = new WP_Roles();
695 }
696
697 $all_roles = $wp_roles->roles;
698
699 $all_roles_checked = is_array($selected_roles) && in_array('all', $selected_roles);
700 ?>
701 <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;">
702 <p style="color: var(--dashboard-text-secondary); margin: 0 0 20px 0;">
703 Select which user roles can see and access this plugin's menu, settings, and options in the WordPress admin area.
704 </p>
705
706 <form method="post" action="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '&tab=advanced')); ?>" id="plugin-access-roles-form">
707 <?php wp_nonce_field('metasync_plugin_access_roles_nonce', 'plugin_access_roles_nonce'); ?>
708 <input type="hidden" name="save_plugin_access_roles" value="yes" />
709
710 <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;">
711
712 <!-- All Roles Option -->
713 <label class="metasync-role-option-all" style="display: flex; align-items: center; gap: 10px; padding: 12px; cursor: pointer; border-radius: 6px; transition: background 0.2s ease;">
714 <input type="checkbox"
715 id="plugin-access-all-roles"
716 name="plugin_access_roles[]"
717 value="all"
718 style="width: 18px; height: 18px; cursor: pointer;"
719 <?php checked($all_roles_checked, true); ?> />
720 <strong style="color: var(--dashboard-text-primary);">All Roles</strong>
721 </label>
722
723 <!-- Divider -->
724 <hr style="border: none; border-top: 1px solid var(--dashboard-border); margin: 12px 0;">
725
726 <?php
727 if (!empty($all_roles) && is_array($all_roles)) {
728 foreach ($all_roles as $role_key => $role_details) {
729 if ($role_key === 'administrator') {
730 continue;
731 }
732
733 if (!isset($role_details['name'])) {
734 continue;
735 }
736
737 $is_checked = is_array($selected_roles) && in_array($role_key, $selected_roles);
738 $role_name = translate_user_role($role_details['name']);
739 ?>
740 <label class="metasync-role-option" style="display: flex; align-items: center; gap: 10px; padding: 10px 12px; cursor: pointer; border-radius: 6px; transition: background 0.2s ease;">
741 <input type="checkbox"
742 class="plugin-access-individual-role"
743 name="plugin_access_roles[]"
744 value="<?php echo esc_attr($role_key); ?>"
745 style="width: 18px; height: 18px; cursor: pointer;"
746 <?php checked($is_checked, true); ?> />
747 <span style="color: var(--dashboard-text-primary);"><?php echo esc_html($role_name); ?></span>
748 </label>
749 <?php
750 }
751 }
752 ?>
753
754 </div>
755
756 <!-- Description -->
757 <div style="background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.3); border-radius: 8px; padding: 16px; margin: 20px 0;">
758 <p style="color: var(--dashboard-text-secondary); margin: 0; font-size: 13px;">
759 <strong style="color: var(--dashboard-info, #3b82f6);">How it works:</strong><br>
760 <strong>Administrators always have access</strong> to this plugin regardless of settings.<br>
761 <strong>By default</strong>, only Administrators can access the plugin (no roles selected).<br>
762 If <strong>"All Roles"</strong> is selected, all users will see the plugin.<br>
763 Users with unchecked roles will not see the plugin in their WordPress admin area.
764 </p>
765 </div>
766
767 <button type="submit" class="metasync-btn-primary" style="background: var(--dashboard-gradient-primary); color: #ffffff; border: none; padding: 12px 24px; border-radius: 8px; font-weight: 500; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 8px rgba(0, 0, 0, 0.15)';" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 4px rgba(0, 0, 0, 0.1)';">
768 Save Access Settings
769 </button>
770 </form>
771 </div>
772 <script>
773 jQuery(document).ready(function($) {
774 var $allRolesCheckbox = $('#plugin-access-all-roles');
775 var $individualRoles = $('.plugin-access-individual-role');
776
777 $allRolesCheckbox.on('change', function() {
778 if ($(this).is(':checked')) {
779 $individualRoles.prop('checked', false);
780 }
781 });
782
783 $individualRoles.on('change', function() {
784 if ($(this).is(':checked')) {
785 $allRolesCheckbox.prop('checked', false);
786 }
787 });
788 });
789 </script>
790 <?php
791 }
792
793 public function get_default_execution_settings() {
794 return array(
795 'max_execution_time' => 30,
796 'max_memory_limit' => 256,
797 'log_batch_size' => 1000,
798 'action_scheduler_batches' => 1,
799 'otto_rate_limit' => 10,
800 'otto_cache_ttl' => 30,
801 'queue_cleanup_days' => 31
802 );
803 }
804
805 public function get_execution_setting($key, $default = null) {
806 $settings = get_option('metasync_execution_settings', array());
807 $defaults = $this->get_default_execution_settings();
808
809 if (empty($settings)) {
810 return isset($defaults[$key]) ? $defaults[$key] : $default;
811 }
812
813 return isset($settings[$key]) ? $settings[$key] : (isset($defaults[$key]) ? $defaults[$key] : $default);
814 }
815
816 public function get_all_execution_settings() {
817 $settings = get_option('metasync_execution_settings', array());
818 $defaults = $this->get_default_execution_settings();
819
820 return wp_parse_args($settings, $defaults);
821 }
822
823 public function can_change_memory_limit() {
824 $current_limit = ini_get('memory_limit');
825
826 $test_result = @ini_set('memory_limit', $current_limit); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- runtime PHP setting with no WordPress API
827
828 return $test_result !== false;
829 }
830
831 public function get_server_limits() {
832 $max_execution_time = ini_get('max_execution_time');
833 $memory_limit = ini_get('memory_limit');
834
835 // Prefer the server-configured value: WordPress (wp_raise_memory_limit)
836 // or this plugin (apply_memory_limit) may have already changed the
837 // runtime limit via ini_set, so ini_get no longer reflects the server
838 // configuration. get_cfg_var reads the php.ini value untouched.
839 $config_memory_limit = get_cfg_var('memory_limit');
840 if ($config_memory_limit !== false && $config_memory_limit !== '') {
841 $memory_limit = $config_memory_limit;
842 }
843
844 $memory_limit_mb = $this->parse_memory_limit_to_mb($memory_limit);
845
846 $can_change_memory = $this->can_change_memory_limit();
847
848 $memory_limit_display = $memory_limit_mb == -1 ? 'Unlimited' : $memory_limit_mb . ' MB';
849
850 return array(
851 'max_execution_time' => $max_execution_time == -1 ? 'Unlimited' : $max_execution_time . ' seconds',
852 'memory_limit' => $memory_limit_display,
853 'max_execution_time_raw' => $max_execution_time,
854 'memory_limit_raw' => $memory_limit_mb,
855 'can_change_memory' => $can_change_memory
856 );
857 }
858
859 /**
860 * Allowed range for the Max Memory Limit setting: 64 MB up to the
861 * server-configured memory limit (null max when the server is unlimited).
862 */
863 public function get_memory_limit_allowed_range() {
864 $server_limits = $this->get_server_limits();
865 $max = $server_limits['memory_limit_raw'] == -1 ? null : max(64, (int) $server_limits['memory_limit_raw']);
866
867 return array(
868 'min' => 64,
869 'max' => $max
870 );
871 }
872
873 public function apply_memory_limit() {
874 if (!$this->can_change_memory_limit()) {
875 return false;
876 }
877
878 $memory_limit_mb = $this->get_execution_setting('max_memory_limit');
879
880 $result = @ini_set('memory_limit', $memory_limit_mb . 'M'); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- runtime PHP setting with no WordPress API
881
882 return $result !== false;
883 }
884
885 public function parse_memory_limit_to_mb($memory_limit) {
886 if ($memory_limit == -1 || $memory_limit == '-1') {
887 return -1;
888 }
889
890 $memory_limit = trim($memory_limit);
891 $last = strtolower(substr($memory_limit, -1));
892 $value = (int) $memory_limit;
893
894 switch ($last) {
895 case 'g':
896 $value *= 1024;
897 break;
898 case 'm':
899 break;
900 case 'k':
901 $value /= 1024;
902 break;
903 }
904
905 return $value;
906 }
907
908 public function render_execution_settings_section() {
909 $settings = $this->get_all_execution_settings();
910 $server_limits = $this->get_server_limits();
911 $memory_range = $this->get_memory_limit_allowed_range();
912
913 $warnings = array();
914 if ((int)$server_limits['max_execution_time_raw'] !== -1 && (int)$settings['max_execution_time'] > (int)$server_limits['max_execution_time_raw']) {
915 $warnings['max_execution_time'] = true;
916 }
917 if ($server_limits['memory_limit_raw'] != -1 && $settings['max_memory_limit'] > $server_limits['memory_limit_raw']) {
918 $warnings['max_memory_limit'] = true;
919 }
920 ?>
921 <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;">
922 <p style="color: var(--dashboard-text-secondary); margin: 0 0 20px 0;">
923 Configure resource limits and execution parameters for plugin operations. Adjust these settings based on your server capabilities.
924 </p>
925
926 <form id="metasync-execution-settings-form" method="post">
927 <?php wp_nonce_field('metasync_execution_settings_nonce', 'execution_settings_nonce'); ?>
928
929 <!-- Execution & Memory Section -->
930 <div style="background: var(--dashboard-card-bg-alt, rgba(255,255,255,0.05)); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 20px; margin-bottom: 20px;">
931 <h3 style="color: var(--dashboard-text-primary); margin: 0 0 16px 0; font-size: 16px; font-weight: 600;">Execution & Memory</h3>
932
933 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;">
934 <div>
935 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
936 Max Execution Time:
937 <?php Metasync::render_tooltip_icon('max_execution_time', 'How many seconds a single plugin operation (like a bulk sync or scan) is allowed to run before PHP stops it. Raise this only if you see timeout errors, and only up to what your host allows.'); ?>
938 </label>
939 <div style="display: flex; align-items: center; gap: 10px;">
940 <input type="number"
941 id="max_execution_time"
942 name="max_execution_time"
943 value="<?php echo esc_attr($settings['max_execution_time']); ?>"
944 min="1"
945 max="300"
946 required
947 style="width: 100px; padding: 8px; border: 1px solid var(--dashboard-border); border-radius: 6px; background: var(--dashboard-card-bg); color: var(--dashboard-text-primary);" />
948 <span style="color: var(--dashboard-text-secondary);">seconds</span>
949 </div>
950 <p style="color: var(--dashboard-text-secondary); font-size: 12px; margin: 4px 0 0 0;">
951 Server Limit: <?php echo esc_html($server_limits['max_execution_time']); ?>
952 </p>
953 <p id="max_execution_time_warning" style="display: none; color: #f59e0b; font-size: 12px; margin: 4px 0 0 0;">
954 <span class="dashicons dashicons-warning" style="margin-right:4px;font-size:14px;width:14px;height:14px;color:#f59e0b;"></span>
955 <span>Configured value exceeds server limit</span>
956 </p>
957 <?php if (isset($warnings['max_execution_time'])): ?>
958 <script>
959 jQuery(document).ready(function($) {
960 $('#max_execution_time_warning').show();
961 });
962 </script>
963 <?php endif; ?>
964 </div>
965
966 <div>
967 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
968 Max Memory Limit:
969 <?php Metasync::render_tooltip_icon('max_memory_limit', 'How much memory (RAM) the plugin is allowed to use for a single operation before PHP stops it. Raise this only if you see "out of memory" errors, and only up to what your host allows.'); ?>
970 </label>
971 <div style="display: flex; align-items: center; gap: 10px;">
972 <input type="number"
973 id="max_memory_limit"
974 name="max_memory_limit"
975 value="<?php echo esc_attr($settings['max_memory_limit']); ?>"
976 min="<?php echo esc_attr($memory_range['min']); ?>"
977 <?php if ($memory_range['max'] !== null): ?>max="<?php echo esc_attr($memory_range['max']); ?>"<?php endif; ?>
978 <?php if (!$server_limits['can_change_memory']): ?>
979 readonly
980 disabled
981 <?php endif; ?>
982 required
983 style="width: 100px; padding: 8px; border: 1px solid var(--dashboard-border); border-radius: 6px; background: <?php echo $server_limits['can_change_memory'] ? 'var(--dashboard-card-bg)' : 'rgba(128, 128, 128, 0.1)'; ?>; color: <?php echo $server_limits['can_change_memory'] ? 'var(--dashboard-text-primary)' : 'var(--dashboard-text-secondary)'; ?>; cursor: <?php echo $server_limits['can_change_memory'] ? 'text' : 'not-allowed'; ?>;" />
984 <span style="color: var(--dashboard-text-secondary);">MB</span>
985 </div>
986 <p style="color: var(--dashboard-text-secondary); font-size: 12px; margin: 4px 0 0 0;">
987 Server Limit: <?php echo esc_html($server_limits['memory_limit']); ?> &middot; Allowed: <?php echo $memory_range['max'] !== null ? esc_html($memory_range['min'] . '–' . $memory_range['max'] . ' MB') : esc_html($memory_range['min'] . ' MB or higher'); ?>
988 </p>
989 <?php if (!$server_limits['can_change_memory']): ?>
990 <p style="color: #f59e0b; font-size: 12px; margin: 4px 0 0 0; display: flex; align-items: center; gap: 4px;">
991 <span class="dashicons dashicons-lock" style="font-size:14px;width:14px;height:14px;color:#f59e0b;"></span>
992 <span>Server does not allow changing memory limit. This setting is read-only.</span>
993 </p>
994 <?php else: ?>
995 <p id="max_memory_limit_warning" style="display: none; color: #f59e0b; font-size: 12px; margin: 4px 0 0 0;">
996 <span class="dashicons dashicons-warning" style="margin-right:4px;font-size:14px;width:14px;height:14px;color:#f59e0b;"></span>
997 <span>Configured value exceeds server limit</span>
998 </p>
999 <?php if (isset($warnings['max_memory_limit'])): ?>
1000 <script>
1001 jQuery(document).ready(function($) {
1002 $('#max_memory_limit_warning').show();
1003 });
1004 </script>
1005 <?php endif; ?>
1006 <?php endif; ?>
1007 </div>
1008 </div>
1009 </div>
1010
1011 <!-- Batch Processing Section -->
1012 <div style="background: var(--dashboard-card-bg-alt, rgba(255,255,255,0.05)); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 20px; margin-bottom: 20px;">
1013 <h3 style="color: var(--dashboard-text-primary); margin: 0 0 16px 0; font-size: 16px; font-weight: 600;">Batch Processing</h3>
1014
1015 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;">
1016 <div>
1017 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
1018 Log Processing Batch Size:
1019 <?php Metasync::render_tooltip_icon('log_batch_size', 'How many log lines the plugin processes at once when scanning its own logs. Lower it if this causes slowdowns on smaller hosting plans; most sites can leave the default.'); ?>
1020 </label>
1021 <div style="display: flex; align-items: center; gap: 10px;">
1022 <input type="number"
1023 id="log_batch_size"
1024 name="log_batch_size"
1025 value="<?php echo esc_attr($settings['log_batch_size']); ?>"
1026 min="100"
1027 max="5000"
1028 required
1029 style="width: 100px; padding: 8px; border: 1px solid var(--dashboard-border); border-radius: 6px; background: var(--dashboard-card-bg); color: var(--dashboard-text-primary);" />
1030 <span style="color: var(--dashboard-text-secondary);">lines</span>
1031 </div>
1032 </div>
1033
1034 <div>
1035 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
1036 Action Scheduler Concurrent Batches:
1037 <?php Metasync::render_tooltip_icon('action_scheduler_batches', 'How many background sync jobs the plugin is allowed to run at the same time. Higher values finish syncing faster but use more server resources at once — lower this if your site slows down during syncs.'); ?>
1038 </label>
1039 <div style="display: flex; align-items: center; gap: 10px;">
1040 <input type="number"
1041 id="action_scheduler_batches"
1042 name="action_scheduler_batches"
1043 value="<?php echo esc_attr($settings['action_scheduler_batches']); ?>"
1044 min="1"
1045 max="10"
1046 required
1047 style="width: 100px; padding: 8px; border: 1px solid var(--dashboard-border); border-radius: 6px; background: var(--dashboard-card-bg); color: var(--dashboard-text-primary);" />
1048 </div>
1049 <p style="color: #f59e0b; font-size: 12px; margin: 4px 0 0 0; display: flex; align-items: center; gap: 4px;">
1050 <span class="dashicons dashicons-warning" style="font-size:14px;width:14px;height:14px;color:#f59e0b;"></span>
1051 <span>Higher values increase server load</span>
1052 </p>
1053 </div>
1054 </div>
1055 </div>
1056
1057 <!-- API Rate Limiting Section -->
1058 <div style="background: var(--dashboard-card-bg-alt, rgba(255,255,255,0.05)); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 20px; margin-bottom: 20px;">
1059 <h3 style="color: var(--dashboard-text-primary); margin: 0 0 16px 0; font-size: 16px; font-weight: 600;">API Rate Limiting</h3>
1060
1061 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;">
1062 <div>
1063 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
1064 <?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> API Calls Per Minute:
1065 <?php Metasync::render_tooltip_icon('otto_rate_limit', 'The maximum number of requests per minute the plugin will send to ' . Metasync::get_whitelabel_otto_name() . '\'s servers. Lower it if you\'re seeing rate-limit errors; most sites can leave the default.'); ?>
1066 </label>
1067 <div style="display: flex; align-items: center; gap: 10px;">
1068 <input type="number"
1069 id="otto_rate_limit"
1070 name="otto_rate_limit"
1071 value="<?php echo esc_attr($settings['otto_rate_limit']); ?>"
1072 min="1"
1073 max="60"
1074 required
1075 style="width: 100px; padding: 8px; border: 1px solid var(--dashboard-border); border-radius: 6px; background: var(--dashboard-card-bg); color: var(--dashboard-text-primary);" />
1076 </div>
1077 </div>
1078
1079 <div>
1080 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
1081 Queue Auto-Cleanup:
1082 <?php Metasync::render_tooltip_icon('queue_cleanup_days', 'How many days a completed background job is kept before the plugin automatically deletes its record. Lower it to save database space; raise it if you need to look back further when troubleshooting.'); ?>
1083 </label>
1084 <div style="display: flex; align-items: center; gap: 10px;">
1085 <input type="number"
1086 id="queue_cleanup_days"
1087 name="queue_cleanup_days"
1088 value="<?php echo esc_attr($settings['queue_cleanup_days']); ?>"
1089 min="7"
1090 max="90"
1091 required
1092 style="width: 100px; padding: 8px; border: 1px solid var(--dashboard-border); border-radius: 6px; background: var(--dashboard-card-bg); color: var(--dashboard-text-primary);" />
1093 <span style="color: var(--dashboard-text-secondary);">days</span>
1094 </div>
1095 </div>
1096 </div>
1097 </div>
1098
1099 <!-- Save Button -->
1100 <div style="margin-top: 20px;">
1101 <button type="submit"
1102 id="metasync-execution-settings-save-btn"
1103 class="button button-primary"
1104 style="padding: 10px 20px; font-size: 14px; font-weight: 500;">
1105 <span class="save-text">Save Settings</span>
1106 <span class="save-spinner" style="display: none; margin-left: 8px;">⏳</span>
1107 </button>
1108 </div>
1109
1110 <!-- Success/Error Messages -->
1111 <div id="metasync-execution-settings-message" style="display: none; margin-top: 16px; padding: 12px; border-radius: 6px;"></div>
1112 </form>
1113 </div>
1114
1115 <?php
1116 // Real-time validation and AJAX save for this form live in
1117 // admin/js/metasync-execution-settings.js (enqueued on this page).
1118 // The previous inline duplicate was removed to avoid a double-bound
1119 // submit handler that fired the save request twice.
1120 ?>
1121 <?php
1122 }
1123
1124 public function get_field_tooltips() {
1125 $plugin_name = Metasync::get_effective_plugin_name();
1126 $otto_name = Metasync::get_whitelabel_otto_name();
1127
1128 return array(
1129 'searchatlas_api_key' => sprintf('Connect your WordPress site to your %1$s dashboard to retrieve your API key and %2$s UUID. This does not create a WordPress login session.', $plugin_name, $otto_name),
1130 '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),
1131 '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),
1132 '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),
1133 '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),
1134 'seo_priority' => sprintf('Choose which value appears first on a page when both your custom SEO value and an approved %s suggestion are available. This changes what visitors see; it does not delete or overwrite saved values.', $otto_name),
1135 '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),
1136 '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),
1137 '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.',
1138 '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),
1139 '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),
1140 '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),
1141 '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.',
1142 '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')),
1143 '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')),
1144 '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.',
1145 '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')),
1146 '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')),
1147 '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')),
1148 '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')),
1149 '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).',
1150 '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),
1151 '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.',
1152 '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),
1153 '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),
1154 '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),
1155 '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),
1156 '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),
1157 '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.',
1158 '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.',
1159 'article_timestamps' => 'Tells social platforms and search engines when each post was first published and last updated, so shares and rich results show accurate dates. Safe to leave enabled.',
1160 'article_author' => 'Adds a link to the author\'s profile or archive page to each post\'s social tags, so shared articles can show author attribution. You can override the author URL per-post. Turn off if you\'d rather not expose author pages.',
1161 'article_section' => 'Labels each post with its main topic (its primary category) in the social tags, helping platforms categorize your content. Falls back to the first category if no primary one is set.',
1162 'article_tags' => 'Adds each of your WordPress post tags as a separate social "tag" so platforms understand the topics a post covers. Turn off if you don\'t want your tags exposed in share metadata.',
1163 '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.'
1164 );
1165 }
1166
1167 public function get_field_section($field_id) {
1168 $sections = $this->get_accordion_sections_config();
1169
1170 foreach ($sections as $section_key => $section_data) {
1171 if (in_array($field_id, $section_data['fields'])) {
1172 return $section_key;
1173 }
1174 }
1175
1176 return null;
1177 }
1178
1179 public function render_accordion_sections($page) {
1180 global $wp_settings_sections, $wp_settings_fields;
1181
1182 if (!isset($wp_settings_fields[$page])) {
1183 return;
1184 }
1185
1186 $sections_config = $this->get_accordion_sections_config();
1187
1188 echo '<div class="metasync-settings-accordion">';
1189
1190 uasort($sections_config, function($a, $b) {
1191 return $a['priority'] - $b['priority'];
1192 });
1193
1194 foreach ($sections_config as $section_key => $section_data) {
1195 $section_id = 'metasync-section-' . $section_key;
1196 $is_open = $section_data['default_open'];
1197 $aria_expanded = $is_open ? 'true' : 'false';
1198 $content_state = $is_open ? 'open' : 'closed';
1199
1200 echo '<div class="metasync-accordion-section" data-section="' . esc_attr($section_key) . '">';
1201
1202 echo '<div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="' . esc_attr($aria_expanded) . '" aria-controls="' . esc_attr($section_id) . '">';
1203 echo '<div class="metasync-accordion-title">';
1204 echo '<span class="metasync-accordion-icon"><span class="dashicons dashicons-' . esc_attr($section_data['icon']) . '"></span></span>';
1205 echo '<div class="metasync-accordion-text">';
1206 echo '<h3>' . esc_html($section_data['title']) . '</h3>';
1207 echo '<p class="metasync-accordion-description">' . esc_html($section_data['description']) . '</p>';
1208 echo '</div>';
1209 echo '</div>';
1210 echo '<button type="button" class="metasync-accordion-toggle" aria-label="Toggle section">';
1211 echo '<span class="toggle-icon">▼</span>';
1212 echo '</button>';
1213 echo '</div>';
1214
1215 echo '<div class="metasync-accordion-content" id="' . esc_attr($section_id) . '" data-state="' . esc_attr($content_state) . '">';
1216
1217 if (isset($section_data['render_callback']) && is_callable($section_data['render_callback'])) {
1218 call_user_func($section_data['render_callback']);
1219 } elseif (isset($section_data['fields']) && is_array($section_data['fields'])) {
1220 echo '<table class="form-table" role="presentation">';
1221
1222 $tooltips = $this->get_field_tooltips();
1223
1224 foreach ($section_data['fields'] as $field_id) {
1225 if (isset($wp_settings_fields[$page]['metasync_settings'][$field_id])) {
1226 $field = $wp_settings_fields[$page]['metasync_settings'][$field_id];
1227
1228 echo '<tr>';
1229 echo '<th scope="row">';
1230 if (!empty($field['title'])) {
1231 echo '<div class="metasync-field-label-wrapper">';
1232 echo '<label for="' . esc_attr($field['id']) . '">' . esc_html($field['title']) . '</label>';
1233
1234 if (isset($tooltips[$field_id])) {
1235 echo '<button type="button" class="metasync-tooltip-trigger" data-tooltip-id="' . esc_attr($field_id) . '" aria-label="More information">';
1236 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">';
1237 echo '<circle cx="12" cy="12" r="10"></circle>';
1238 echo '<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>';
1239 echo '<line x1="12" y1="17" x2="12.01" y2="17"></line>';
1240 echo '</svg>';
1241 echo '</button>';
1242
1243 echo '<div class="metasync-tooltip" id="tooltip-' . esc_attr($field_id) . '" role="tooltip">';
1244 echo '<div class="metasync-tooltip-arrow"></div>';
1245 echo '<div class="metasync-tooltip-content">' . esc_html($tooltips[$field_id]) . '</div>';
1246 echo '</div>';
1247 }
1248
1249 echo '</div>';
1250 }
1251 echo '</th>';
1252 echo '<td>';
1253 call_user_func($field['callback'], $field['args']);
1254 echo '</td>';
1255 echo '</tr>';
1256 }
1257 }
1258
1259 echo '</table>';
1260 }
1261
1262 echo '</div>';
1263 echo '</div>';
1264 }
1265
1266 echo '</div>';
1267 }
1268
1269 public function render_llms_txt_section() {
1270 global $wp_settings_fields;
1271
1272 $page = Metasync_Admin::$page_slug . '_general';
1273 $section = Metasync_Admin::SECTION_LLMS_TXT;
1274
1275 if (empty($wp_settings_fields[$page][$section])) {
1276 return;
1277 }
1278
1279 echo '<table class="form-table" role="presentation">';
1280 $llms_tooltips = array(
1281 'llms_txt_enabled' => 'Publishes a file at yoursite.com/llms.txt listing your key pages so AI assistants like ChatGPT and Perplexity can find and cite your content. Safe to leave off if you don\'t want AI crawlers guided to your pages.',
1282 'llms_full_enabled' => 'Also publishes a larger file with the full text of your posts (not just links). This exposes complete article content and can be large — keep the post limit low on smaller hosting plans.',
1283 );
1284 foreach ($wp_settings_fields[$page][$section] as $field) {
1285 echo '<tr>';
1286 echo '<th scope="row">';
1287 if (!empty($field['title'])) {
1288 echo '<div class="metasync-field-label-wrapper">';
1289 echo '<label for="' . esc_attr($field['id']) . '">' . esc_html($field['title']) . '</label>';
1290 if (isset($llms_tooltips[$field['id']])) {
1291 Metasync::render_tooltip_icon($field['id'], $llms_tooltips[$field['id']]);
1292 }
1293 echo '</div>';
1294 }
1295 echo '</th>';
1296 echo '<td>';
1297 call_user_func($field['callback'], $field['args']);
1298 echo '</td>';
1299 echo '</tr>';
1300 }
1301 echo '</table>';
1302 ?>
1303 <script>
1304 (function(){
1305 function toggleLlms(){
1306 var enabled = document.getElementById('llms_txt_enabled');
1307 var fullEnabled = document.getElementById('llms_full_enabled');
1308 var deps = document.querySelectorAll('.llms-txt-dependent');
1309 var fullDeps = document.querySelectorAll('.llms-full-dependent');
1310 var i;
1311 for(i = 0; i < deps.length; i++){
1312 var row = deps[i].closest('tr');
1313 if(row) row.style.display = enabled && enabled.checked ? '' : 'none';
1314 }
1315 for(i = 0; i < fullDeps.length; i++){
1316 var row = fullDeps[i].closest('tr');
1317 if(row) row.style.display = (enabled && enabled.checked && fullEnabled && fullEnabled.checked) ? '' : 'none';
1318 }
1319 }
1320 document.addEventListener('DOMContentLoaded', function(){
1321 var enabled = document.getElementById('llms_txt_enabled');
1322 var fullEnabled = document.getElementById('llms_full_enabled');
1323 if(enabled) enabled.addEventListener('change', toggleLlms);
1324 if(fullEnabled) fullEnabled.addEventListener('change', toggleLlms);
1325 toggleLlms();
1326 });
1327 })();
1328 </script>
1329 <?php
1330 }
1331
1332 // ────────────────────────────────────────────────────────────────
1333 // Settings field callbacks
1334 // ────────────────────────────────────────────────────────────────
1335
1336 public function metasync_settings_genkey_callback()
1337 {
1338 $current_token = Metasync::get_option('general')['apikey'] ?? '';
1339
1340 if (!empty($current_token)) {
1341 $display_value = $current_token;
1342 $status_message = 'Plugin Auth Token is active and ready for authentication.';
1343 $refresh_help = 'Click refresh to generate a new token and update the heartbeat API.';
1344 } else {
1345 $display_value = 'Auto-generated when connecting to ' . Metasync::get_effective_plugin_name();
1346 $status_message = 'Plugin Auth Token will be automatically generated when you click "Connect to ' . Metasync::get_effective_plugin_name() . '".';
1347 $refresh_help = 'You can also manually generate a token by clicking refresh.';
1348 }
1349
1350 printf('<div class="metasync-auth-token-row">');
1351 printf(
1352 '<input type="text" id="apikey" name="' . esc_attr(Metasync_Admin::option_key) . '[general][apikey]" value="%s" size="40" class="regular-text" readonly="readonly" />',
1353 esc_attr($display_value)
1354 );
1355 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>');
1356 printf('</div>');
1357 printf('<p class="description">%s %s</p>', esc_html($status_message), esc_html($refresh_help));
1358 }
1359
1360 public function linkgraph_token_callback()
1361 {
1362 printf(
1363 '<input type="text" id="linkgraph_token" name="' . esc_attr(Metasync_Admin::option_key) . '[general][linkgraph_token]" value="%s" size="25" readonly="readonly" />',
1364 isset(Metasync::get_option('general')['linkgraph_token']) ? esc_attr(Metasync::get_option('general')['linkgraph_token']) : ''
1365 );
1366
1367 printf(
1368 '<input type="text" id="linkgraph_customer_id" name="' . esc_attr(Metasync_Admin::option_key) . '[general][linkgraph_customer_id]" value="%s" size="25" readonly="readonly" />',
1369 isset(Metasync::get_option('general')['linkgraph_customer_id']) ? esc_attr(Metasync::get_option('general')['linkgraph_customer_id']) : ''
1370 );
1371
1372 ?>
1373 <button type="button" class="button button-primary" id="lgloginbtn">Fetch Token</button>
1374 <input type="text" id="lgusername" class="input lguser hidden" placeholder="username" />
1375 <input type="text" id="lgpassword" class="input lguser hidden" placeholder="password" />
1376 <p id="lgerror" class="notice notice-error hidden" style="display: none;"></p>
1377 <?php
1378 }
1379
1380
1381 public function time_elapsed_string($datetime, $full = false)
1382 {
1383 if(empty($datetime)){
1384 return "";
1385 }
1386 # Stored timestamps use current_time('mysql') which is the WP site's
1387 # local timezone (no offset marker on the string). Constructing
1388 # DateTime without a timezone would default to UTC and produce a
1389 # bogus diff equal to the WP timezone offset (QA: "4 hours ago"
1390 # after a 4-minute sync on non-UTC sites).
1391 $wp_tz = function_exists('wp_timezone') ? wp_timezone() : new DateTimeZone('UTC');
1392 $now = new DateTime('now', $wp_tz);
1393 $ago = new DateTime($datetime, $wp_tz);
1394
1395 $diff = $now->diff($ago);
1396
1397 $string = [
1398 'y' => 'year',
1399 'm' => 'month',
1400 'w' => 'week',
1401 'd' => 'day',
1402 'h' => 'hour',
1403 'i' => 'minute',
1404 's' => 'second',
1405 ];
1406
1407 foreach ($string as $k => &$v) {
1408 if (isset($diff->$k) && $diff->$k) {
1409 $v = $diff->$k . ' ' . $v . ($diff->$k > 1 ? 's' : '');
1410 } else {
1411 unset($string[$k]);
1412 }
1413 }
1414 if (!$full) $string = array_slice($string, 0, 1);
1415 return $string ? implode(', ', $string) . ' ago' : 'just now';
1416 }
1417
1418 public function searchatlas_api_key_callback()
1419 {
1420 // Whether a key is stored at rest (encrypted or legacy plaintext).
1421 $stored_api_key = Metasync::get_option('general')['searchatlas_api_key'] ?? '';
1422 $has_api_key = ($stored_api_key !== '');
1423
1424 // Decrypted key for masking only. Never rendered in cleartext.
1425 // false => an encrypted key exists but cannot be decrypted (salts changed).
1426 $decrypted_api_key = Metasync::get_searchatlas_api_key();
1427 $decrypt_failed = ($decrypted_api_key === false);
1428
1429 // Build a masked representation: bullets + last 4 chars. The cleartext
1430 // key is never emitted into the page.
1431 $masked_api_key = '';
1432 if (!$decrypt_failed && $decrypted_api_key !== '') {
1433 $masked_api_key = str_repeat('*', 8) . substr($decrypted_api_key, -4);
1434 }
1435
1436 $otto_uuid = isset(Metasync::get_option('general')['otto_pixel_uuid']) ? Metasync::get_option('general')['otto_pixel_uuid'] : '';
1437
1438 $has_otto_uuid = !empty($otto_uuid);
1439
1440 $is_fully_connected = $this->admin_instance->is_heartbeat_connected();
1441
1442 printf('<div class="metasync-sa-connect-container">');
1443
1444 printf('<div class="metasync-sa-connect-title">');
1445 printf('One-Click Authentication');
1446 printf('</div>');
1447
1448 printf('<div class="metasync-sa-connect-description">');
1449 if ($is_fully_connected) {
1450 printf('Your %s account is fully synced with active heartbeat API. You can re-authenticate to refresh your connection or connect a different account.', esc_html(Metasync::get_effective_plugin_name()));
1451 } elseif ($has_api_key && !$has_otto_uuid) {
1452 printf('Your %s API key is configured, but %s UUID is missing. Please re-authenticate to complete the setup.', esc_html(Metasync::get_effective_plugin_name()), esc_html(Metasync::get_whitelabel_otto_name()));
1453 } else {
1454 printf('Connect your %s account with one click. This will automatically configure your API key and %s UUID below, enabling all plugin features.', esc_html(Metasync::get_effective_plugin_name()), esc_html(Metasync::get_whitelabel_otto_name()));
1455 }
1456 printf('</div>');
1457
1458 ?>
1459 <div class="metasync-mcp-consent" style="margin-top: 15px; padding: 12px 15px; background: #f0f9ff; border-left: 3px solid #0ea5e9; border-radius: 4px;">
1460 <p style="margin: 0; color: #334155; font-size: 13px; line-height: 1.6;">
1461 <strong>AI-Powered SEO Automation:</strong> By authenticating, you authorize <strong><?php echo esc_html(Metasync::get_effective_plugin_name()); ?> Brain</strong> (our AI assistant) to access your WordPress admin capabilities through the Model Context Protocol (MCP). This enables intelligent automation for SEO optimizations, content enhancements, and performance improvements.
1462 </p>
1463 </div>
1464 <?php
1465
1466 printf('<div class="metasync-sa-connect-buttons">');
1467
1468 printf('<button type="button" id="connect-searchatlas-btn" class="metasync-sa-connect-btn">');
1469 if ($is_fully_connected) {
1470 printf('Re-authenticate with %s', esc_html(Metasync::get_effective_plugin_name()));
1471 } elseif ($has_api_key && !$has_otto_uuid) {
1472 printf('Complete Authentication Setup');
1473 } else {
1474 printf('Connect to %s', esc_html(Metasync::get_effective_plugin_name()));
1475 }
1476 printf('</button>');
1477
1478 if ($has_api_key) {
1479 printf('<button type="button" id="reset-searchatlas-auth" class="metasync-sa-reset-btn" style="margin-left: 10px;">');
1480 printf('Disconnect Account');
1481 printf('</button>');
1482 }
1483
1484 printf('</div>');
1485
1486 printf('<div style="margin-top: 15px;">');
1487 printf('<details style="margin-top: 10px;">');
1488 printf('<summary style="cursor: pointer; color: #666; font-size: 13px;">Authentication Tips</summary>');
1489 printf('<div style="padding: 10px 0; color: #666; font-size: 13px; line-height: 1.5;">');
1490 printf('• Make sure you have a %s account before connecting<br/>', esc_html(Metasync::get_effective_plugin_name()));
1491 printf('• The authentication window will open in a popup - please allow popups<br/>');
1492 printf('• The process typically takes 15-30 seconds to complete<br/>');
1493 printf('• Your API key will be stored securely and shown masked below<br/>');
1494 printf('• If you encounter issues, try disabling ad blockers temporarily<br/>');
1495 printf('• Contact <a href="mailto:%s">%s</a> if you need assistance', esc_attr(Metasync::SUPPORT_EMAIL), esc_html(Metasync::SUPPORT_EMAIL));
1496 printf('</div>');
1497 printf('</details>');
1498 printf('</div>');
1499
1500 printf('</div>');
1501
1502 printf('<div style="margin-top: 20px;">');
1503 printf('<label for="searchatlas-api-key" style="font-weight: 600; display: block; margin-bottom: 8px;">');
1504 printf('%s API Key', esc_html(Metasync::get_effective_plugin_name()));
1505 if ($is_fully_connected) {
1506 printf('<span style="color: #46b450; margin-left: 10px; font-weight: normal;">✓ Synced</span>');
1507 } elseif ($has_api_key && !$has_otto_uuid) {
1508 printf('<span style="color: #ff8c00; margin-left: 10px; font-weight: normal;">Partial Connection (Missing %s UUID)</span>', esc_html(Metasync::get_whitelabel_otto_name()));
1509 }
1510 printf('</label>');
1511
1512 // Read-only masked display. The API key is managed via one-click
1513 // authentication only; the cleartext key is never rendered into the
1514 // page HTML (View Source / DOM). No form input is emitted, so saving
1515 // settings cannot overwrite or expose the stored key.
1516 $display_has_key = (!$decrypt_failed && $masked_api_key !== '') ? '1' : '0';
1517 printf(
1518 '<div id="searchatlas-api-key-display" class="metasync-api-key-masked" data-has-key="%s" style="display:inline-flex; align-items:center; min-width:320px; height:42px; padding:0 14px; font-family:monospace; font-size:15px; letter-spacing:4px; background:var(--dashboard-input-bg, #22272e); border:1px solid var(--dashboard-border, #3a424d); border-radius:6px; color:var(--dashboard-text-primary, #e6e8eb); cursor:default; user-select:none;">',
1519 esc_attr($display_has_key)
1520 );
1521 if ($decrypt_failed) {
1522 printf('<em style="color:#b32d2e; font-family:sans-serif; letter-spacing:normal; font-style:normal; font-size:14px;">Stored API key could not be read. Please re-authenticate above.</em>');
1523 } elseif ($masked_api_key !== '') {
1524 echo esc_html($masked_api_key);
1525 } else {
1526 printf('<em style="color:#646970;">Not configured</em>');
1527 }
1528 printf('</div>');
1529
1530 printf('<p class="description" style="margin-top: 8px;">');
1531 if ($decrypt_failed) {
1532 printf('Your stored API key could not be decrypted (your site security keys may have changed). Please re-authenticate above to reconnect.');
1533 } elseif ($is_fully_connected) {
1534 printf('Your %s API key is stored securely and used for communication with the platform. Use the authentication button above to refresh or change accounts.', esc_html(Metasync::get_effective_plugin_name()));
1535 } elseif ($has_api_key && !$has_otto_uuid) {
1536 printf('Your API key is configured but %s UUID is missing. Re-authenticate above to complete the setup and enable dashboard access.', esc_html(Metasync::get_whitelabel_otto_name()));
1537 } else {
1538 printf('Use the authentication button above to securely connect your account. Your API key is stored encrypted and shown masked here.');
1539 }
1540 printf('</p>');
1541
1542 printf('</div>');
1543
1544 if( isset(Metasync::get_option('general')['searchatlas_api_key'])&&Metasync::get_option('general')['searchatlas_api_key']!=''){
1545 $timestamp = @Metasync::get_option('general')['send_auth_token_timestamp'];
1546 printf(
1547 '<p id="sendAuthTokenTimestamp" class="descriptionValue">%s (%s)</p>',
1548 esc_attr($timestamp),
1549 esc_html($this->time_elapsed_string($timestamp))
1550 );
1551
1552
1553 }
1554 }
1555
1556 public function bing_site_verification_callback()
1557 {
1558 printf(
1559 '<input type="text" id="bing_site_verification" name="' . esc_attr(Metasync_Admin::option_key) . '[searchengines][bing_site_verification]" value="%s" size="50" />',
1560 isset(Metasync::get_option('searchengines')['bing_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['bing_site_verification']) : ''
1561 );
1562
1563 printf(' <br> <span class="description"> Enter Bing Webmaster Tools verification code: </span> ');
1564 printf(' <a href="https://www.bing.com/webmasters/about" target="_blank">Get from here</a> <br> ');
1565 }
1566
1567 public function yandex_site_verification_callback()
1568 {
1569 printf(
1570 '<input type="text" id="yandex_site_verification" name="' . esc_attr(Metasync_Admin::option_key) . '[searchengines][yandex_site_verification]" value="%s" size="50" />',
1571 isset(Metasync::get_option('searchengines')['yandex_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['yandex_site_verification']) : ''
1572 );
1573
1574 printf(' <br> <span class="description"> Enter Yandex verification code: </span>');
1575 printf(' <a href="https://passport.yandex.com/auth" target="_blank">Get from here</a> <br> ');
1576 }
1577
1578 public function google_site_verification_callback()
1579 {
1580 printf(
1581 '<input type="text" id="google_site_verification" name="' . esc_attr(Metasync_Admin::option_key) . '[searchengines][google_site_verification]" value="%s" size="50" />',
1582 isset(Metasync::get_option('searchengines')['google_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['google_site_verification']) : ''
1583 );
1584
1585 printf(' <br> <span class="description"> Enter Google Search Console verification code: </span>');
1586 printf(' <a href="https://www.google.com/webmasters/verification" target="_blank">Get from here</a> <br> ');
1587 }
1588
1589 public function pinterest_site_verification_callback()
1590 {
1591 printf(
1592 '<input type="text" id="pinterest_site_verification" name="' . esc_attr(Metasync_Admin::option_key) . '[searchengines][pinterest_site_verification]" value="%s" size="50" />',
1593 isset(Metasync::get_option('searchengines')['pinterest_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['pinterest_site_verification']) : ''
1594 );
1595
1596 printf(' <br> <span class="description"> Enter Pinterest verification code: </span>');
1597 printf(' <a href="https://in.pinterest.com/" target="_blank">Get from here</a> <br> ');
1598 }
1599
1600 public function local_seo_person_organization_callback()
1601 {
1602 $person_organization = Metasync::get_option('localseo')['local_seo_person_organization'] ?? '';
1603 $valid_person_organizations = array('Person', 'Organization');
1604 ?>
1605 <div class="metasync-type-select-wrap">
1606 <select id="local_seo_person_organization" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_person_organization]') ?>">
1607 <?php
1608 if (!in_array($person_organization, $valid_person_organizations, true)) {
1609 printf('<option value="0" selected="selected">Select Type</option>');
1610 }
1611 printf('<option value="Person" %s >Person</option>', selected('Person', esc_attr($person_organization)));
1612 printf('<option value="Organization" %s >Organization</option>', selected('Organization', esc_attr($person_organization)));
1613 ?>
1614 </select>
1615 </div>
1616 <?php
1617 printf(' <br> <span class="description"> Choose whether the site represents a person or an organization. </span>');
1618 }
1619
1620 public function local_seo_name_callback()
1621 {
1622 printf(
1623 '<input type="text" id="local_seo_name" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_name]" value="%s" size="50" />',
1624 isset(Metasync::get_option('localseo')['local_seo_name']) ? esc_attr(Metasync::get_option('localseo')['local_seo_name']) : esc_attr(get_bloginfo())
1625 );
1626
1627 printf(' <br> <span class="description"> Your name or company name </span>');
1628 }
1629
1630 public function local_seo_logo_callback()
1631 {
1632 $local_seo_logo = Metasync::get_option('localseo')['local_seo_logo'] ?? '';
1633
1634 // Rows saved by the legacy sanitizer hold the corrupted form of
1635 // an attachment ID ("http://45589"). Repair it before anything reads
1636 // the value — both so the preview resolves and so the hidden input
1637 // re-submits the recovered ID instead of the corrupted string, which
1638 // would otherwise survive every subsequent Save Settings.
1639 $local_seo_logo = metasync_repair_scheme_prefixed_media_id($local_seo_logo);
1640
1641 printf(
1642 '<input type="hidden" id="local_seo_logo" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_logo]" value="%s" size="50" />',
1643 esc_attr((string) $local_seo_logo)
1644 );
1645
1646 printf(' <br> <input class="button-secondary no-loading" type="button" id="logo_upload_button" value="Add or Upload File">');
1647
1648 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> ');
1649
1650 printf('<span class="metasync-logo-preview-wrap">');
1651
1652 // The stored value may be an attachment ID or a raw URL, so resolve
1653 // both the way the front-end schema output already does. Anything that
1654 // resolves to nothing — a deleted attachment, a legacy value mangled by
1655 // an earlier save — must render no preview at all, rather than an empty
1656 // src next to an orphaned remove button.
1657 $logo_url = $this->resolve_media_url($local_seo_logo, 'medium');
1658
1659 printf(
1660 '<img src="%s" id="local_seo_business_logo" width="300"%s>',
1661 esc_url($logo_url),
1662 $logo_url ? '' : ' style="display:none"'
1663 );
1664
1665 $button_type = $logo_url ? 'button' : 'hidden';
1666 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));
1667 printf('</span>');
1668 }
1669
1670 /**
1671 * Resolve a stored media setting to a displayable URL.
1672 *
1673 * These settings accept either a media-library attachment ID or a raw URL;
1674 * returns an empty string when the value resolves to neither, so callers
1675 * can distinguish "no image" from "image we could not resolve".
1676 *
1677 * @param mixed $value Attachment ID or URL as stored in the options.
1678 * @param string $size Registered image size to request for an attachment.
1679 * @return string Resolved URL, or '' when there is nothing to show.
1680 */
1681 private function resolve_media_url($value, $size = 'medium')
1682 {
1683 if (empty($value)) {
1684 return '';
1685 }
1686
1687 // A row saved by the legacy sanitizer stores a corrupted attachment ID ("http://45589"),
1688 // which is neither resolvable as an ID nor a usable image URL.
1689 $value = metasync_repair_scheme_prefixed_media_id($value);
1690
1691 if (empty($value)) {
1692 return '';
1693 }
1694
1695 if (is_numeric($value)) {
1696 $url = wp_get_attachment_image_url((int) $value, $size);
1697 return $url ? $url : '';
1698 }
1699
1700 return (string) $value;
1701 }
1702
1703 public function local_seo_url_callback()
1704 {
1705 printf(
1706 '<input type="text" id="local_seo_url" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_url]" value="%s" size="50" />',
1707 isset(Metasync::get_option('localseo')['local_seo_url']) ? esc_attr(Metasync::get_option('localseo')['local_seo_url']) : esc_attr(home_url())
1708 );
1709
1710 printf(' <br> <span class="description"> URL of the item. </span>');
1711 }
1712
1713 public function local_seo_email_callback()
1714 {
1715 printf(
1716 '<input type="text" id="local_seo_email" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_email]" value="%s" size="50" />',
1717 isset(Metasync::get_option('localseo')['local_seo_email']) ? esc_attr(Metasync::get_option('localseo')['local_seo_email']) : ''
1718 );
1719
1720 printf(' <br> <span class="description"> Search engines display your email address. </span>');
1721 }
1722
1723 public function local_seo_phone_callback()
1724 {
1725 printf(
1726 '<input type="text" id="local_seo_phone" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_phone]" value="%s" size="50" />',
1727 isset(Metasync::get_option('localseo')['local_seo_phone']) ? esc_attr(Metasync::get_option('localseo')['local_seo_phone']) : ''
1728 );
1729
1730 printf(' <br> <span class="description"> Search engines may prominently display your contact phone number for mobile users. </span>');
1731 }
1732
1733 public function local_seo_address_callback()
1734 {
1735 printf(
1736 '<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>',
1737 isset(Metasync::get_option('localseo')['address']['street']) ? esc_attr(Metasync::get_option('localseo')['address']['street']) : ''
1738 );
1739
1740 printf(
1741 '<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>',
1742 isset(Metasync::get_option('localseo')['address']['locality']) ? esc_attr(Metasync::get_option('localseo')['address']['locality']) : ''
1743 );
1744
1745 printf(
1746 '<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>',
1747 isset(Metasync::get_option('localseo')['address']['region']) ? esc_attr(Metasync::get_option('localseo')['address']['region']) : ''
1748 );
1749
1750 printf(
1751 '<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>',
1752 isset(Metasync::get_option('localseo')['address']['postalcode']) ? esc_attr(Metasync::get_option('localseo')['address']['postalcode']) : ''
1753 );
1754
1755 printf(
1756 '<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>',
1757 isset(Metasync::get_option('localseo')['address']['country']) ? esc_attr(Metasync::get_option('localseo')['address']['country']) : ''
1758 );
1759 }
1760
1761 public function local_seo_business_type_callback()
1762 {
1763 $types = self::get_business_types();
1764 sort($types);
1765
1766 $business_type = Metasync::get_option('localseo')['local_seo_business_type'] ?? '';
1767
1768 ?>
1769 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_business_type]') ?>">
1770 <option value='0'>Select Business Type</option>
1771 <?php
1772 foreach ($types as $type) {
1773 printf('<option value="%s" %s >%s</option>', esc_attr($type), selected($type, $business_type, false), esc_html($type));
1774 }
1775 ?>
1776 </select>
1777 <?php
1778 }
1779
1780 public function local_seo_opening_hours_callback()
1781 {
1782 $days_name = ['Monday', 'Tuseday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
1783 $days = isset(Metasync::get_option('localseo')['days']) ? Metasync::get_option('localseo')['days'] : '';
1784 $times = isset(Metasync::get_option('localseo')['times']) ? Metasync::get_option('localseo')['times'] : '';
1785
1786 ?>
1787 <ul id="daysTime">
1788 <?php
1789 $opening_days = [];
1790 if ($days && $times) {
1791 $opening_days = array_combine($days, $times);
1792 }
1793 foreach ($opening_days as $day_name => $day_time) {
1794 ?>
1795 <li>
1796 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][days][]') ?>">
1797 <?php
1798 foreach ($days_name as $name) {
1799 printf('<option value="%s" %s >%s</option>', esc_attr($name), selected($name, $day_name, false), esc_html($name));
1800 }
1801 ?>
1802 </select>
1803 <input type="text" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][times][]') ?>" value="<?php echo esc_attr($day_time) ?>">
1804 <button id="timeDelete">Delete</button>
1805 </li>
1806 <?php } ?>
1807 <?php if (empty($opening_days)) { ?>
1808 <li>
1809 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][days][]') ?>">
1810 <?php
1811 foreach ($days_name as $name) {
1812 printf('<option value="%s" >%s</option>', esc_attr($name), esc_attr($name));
1813 }
1814 ?>
1815 </select>
1816 <input type="text" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][times][]') ?>" value="">
1817 <button id="timeDelete">Delete</button>
1818 </li>
1819 <?php } ?>
1820 </ul>
1821 <?php
1822
1823 printf(' <input type="hidden" id="days_time_count" value="%s"/>', count($opening_days));
1824 printf(' <input class="button-secondary" type="button" id="addNewTime" value="Add Time">');
1825 printf(' <br> <span class="description"> Select opening hours. You can add multiple sets if you have different opening or closing hours on some days or if you have a mid-day break. Times are specified using 24:00 time. </span>');
1826 }
1827
1828 public function local_seo_phone_numbers_callback()
1829 {
1830 $number_types = ['Customer Service', 'Technical Support', 'Billing Support', 'Bill Payment', 'Sales', 'Reservations', 'Credit Card Support', 'Emergency', 'Baggage Tracking', 'Roadside Assistance', 'Package Tracking'];
1831 $types = isset(Metasync::get_option('localseo')['phonetype']) ? Metasync::get_option('localseo')['phonetype'] : '';
1832 $numbers = isset(Metasync::get_option('localseo')['phonenumber']) ? Metasync::get_option('localseo')['phonenumber'] : '';
1833
1834 ?>
1835
1836 <ul id="phone-numbers">
1837 <?php
1838 $phone_numbers = [];
1839 if ($types && $numbers) {
1840 $phone_numbers = array_combine($types, $numbers);
1841 }
1842 foreach ($phone_numbers as $phone_type => $phone_number) {
1843 ?>
1844 <li>
1845 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][phonetype][]') ?>">
1846 <?php
1847 foreach ($number_types as $type) {
1848 printf('<option value="%s" %s >%s</option>', esc_attr($type), selected(esc_attr($type), esc_attr($phone_type)), esc_attr($type));
1849 }
1850 ?>
1851 </select>
1852 <input type="text" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][phonenumber][]') ?>" value="<?php echo esc_attr($phone_number) ?>">
1853 <button id="number-delete">Delete</button>
1854 </li>
1855 <?php } ?>
1856 <?php if (empty($phone_numbers)) { ?>
1857 <li>
1858 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][phonetype][]') ?>">
1859 <?php
1860 foreach ($number_types as $type) {
1861 printf('<option value="%s" >%s</option>', esc_attr($type), esc_attr($type));
1862 }
1863 ?>
1864 </select>
1865 <input type="text" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][phonenumber][]') ?>" value="">
1866 <button id="number-delete">Delete</button>
1867 </li>
1868 <?php } ?>
1869 </ul>
1870 <?php
1871
1872 printf(' <input type="hidden" id="phone_number_count" value="%s"/>', count($phone_numbers));
1873 printf(' <input class="button-secondary" type="button" id="addNewNumber" value="Add Number">');
1874 printf(' <br> <span class="description"> Search engines may prominently display your contact phone number for mobile users. </span>');
1875 }
1876
1877 public function local_seo_price_range_callback()
1878 {
1879 printf(
1880 '<input type="text" id="local_seo_price_range" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_price_range]" value="%s" size="50" />',
1881 isset(Metasync::get_option('localseo')['local_seo_price_range']) ? esc_attr(Metasync::get_option('localseo')['local_seo_price_range']) : ''
1882 );
1883 printf(' <br> <span class="description"> The price range of the business, for example $$$. </span>');
1884 }
1885
1886 public function local_seo_about_page_callback()
1887 {
1888 ?>
1889 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_about_page]') ?>">
1890 <option value='0'>Select About Page</option>
1891 <?php
1892 $about_page = Metasync::get_option('localseo')['local_seo_about_page'] ?? '';
1893 $pages = get_pages();
1894 foreach ($pages as $page) {
1895 printf('<option value="%s" %s >%s</option>', absint($page->ID), selected($page->ID, $about_page, false), esc_html($page->post_title));
1896 }
1897 ?>
1898 </select>
1899 <?php
1900 printf(' <br> <span class="description"> Search engines tag your about us page. </span>');
1901 }
1902
1903 public function local_seo_contact_page_callback()
1904 {
1905 ?>
1906 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_contact_page]') ?>">
1907 <option value='0'>Select Contact Page</option>
1908 <?php
1909 $contact_page = Metasync::get_option('localseo')['local_seo_contact_page'] ?? '';
1910 $pages = get_pages();
1911 foreach ($pages as $page) {
1912 printf('<option value="%s" %s >%s</option>', absint($page->ID), selected($page->ID, $contact_page, false), esc_html($page->post_title));
1913 }
1914 ?>
1915 </select>
1916 <?php
1917 printf(' <br> <span class="description"> Search engines tag your contact page. </span>');
1918 }
1919
1920 public function local_seo_map_key_callback()
1921 {
1922 printf(
1923 '<input type="text" id="local_seo_map_key" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_map_key]" value="%s" size="50" />',
1924 isset(Metasync::get_option('localseo')['local_seo_map_key']) ? esc_attr(Metasync::get_option('localseo')['local_seo_map_key']) : ''
1925 );
1926
1927 printf(' <br> <span class="description"> An API Key is required to display embedded Google Maps on your site. </span>');
1928 }
1929
1930 public function local_seo_geo_coordinates_callback()
1931 {
1932 printf(
1933 '<input type="text" id="local_seo_geo_coordinates" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_geo_coordinates]" value="%s" size="50" />',
1934 isset(Metasync::get_option('localseo')['local_seo_geo_coordinates']) ? esc_attr(Metasync::get_option('localseo')['local_seo_geo_coordinates']) : ''
1935 );
1936
1937 printf(' <br> <span class="description"> Latitude and longitude values separated by comma. </span>');
1938 }
1939
1940 public function header_snippets_callback()
1941 {
1942 printf(
1943 '<textarea class="wide-text" id="header_snippets" rows="8" name="' . esc_attr(Metasync_Admin::option_key) . '[codesnippets][header_snippet]" >%s</textarea>',
1944 isset(Metasync::get_option('codesnippets')['header_snippet']) ? esc_attr(Metasync::get_option('codesnippets')['header_snippet']) : ''
1945 );
1946 }
1947
1948 public function footer_snippets_callback()
1949 {
1950 printf(
1951 '<textarea class="wide-text" id="footer_snippets" rows="8" name="' . esc_attr(Metasync_Admin::option_key) . '[codesnippets][footer_snippet]" >%s</textarea>',
1952 isset(Metasync::get_option('codesnippets')['footer_snippet']) ? esc_attr(Metasync::get_option('codesnippets')['footer_snippet']) : ''
1953 );
1954 }
1955
1956 public function common_robot_meta_tags_callback()
1957 {
1958 $common_robots_meta = Metasync::get_option('common_robots_meta') ?? Metasync::get_option('common_robots_mata') ?? '';
1959
1960 ?>
1961 <ul class="checkbox-list">
1962 <li>
1963 <input type="checkbox" name="<?php echo esc_attr(Metasync_Admin::option_key . '[common_robots_meta][noindex]') ?>" id="robots_common2" value="noindex" <?php isset($common_robots_meta['noindex']) ? checked('noindex', $common_robots_meta['noindex']) : '' ?>>
1964 <label for="robots_common2">No Index </br>
1965 <span class="description">
1966 <span>Search engines not indexed and displayed this pages in search engine results</span>
1967 </span>
1968 </label>
1969 </li>
1970 <li>
1971 <input type="checkbox" name="<?php echo esc_attr(Metasync_Admin::option_key . '[common_robots_meta][nofollow]') ?>" id="robots_common3" value="nofollow" <?php isset($common_robots_meta['nofollow']) ? checked('nofollow', $common_robots_meta['nofollow']) : '' ?>>
1972 <label for="robots_common3">No Follow </br>
1973 <span class="description">
1974 <span>Search engines not follow the links on the pages</span>
1975 </span>
1976 </label>
1977 </li>
1978 <li>
1979 <input type="checkbox" name="<?php echo esc_attr(Metasync_Admin::option_key . '[common_robots_meta][noarchive]') ?>" id="robots_common4" value="noarchive" <?php isset($common_robots_meta['noarchive']) ? checked('noarchive', $common_robots_meta['noarchive']) : '' ?>>
1980 <label for="robots_common4">No Archive </br>
1981 <span class="description">
1982 <span>Search engines not showing Cached links for pages</span>
1983 </span>
1984 </label>
1985 </li>
1986 <li>
1987 <input type="checkbox" name="<?php echo esc_attr(Metasync_Admin::option_key . '[common_robots_meta][noimageindex]') ?>" id="robots_common5" value="noimageindex" <?php isset($common_robots_meta['noimageindex']) ? checked('noimageindex', $common_robots_meta['noimageindex']) : '' ?>>
1988 <label for="robots_common5">No Image Index </br>
1989 <span class="description">
1990 <span>If you do not want to apear your pages as the referring page for images that appear in image search results</span>
1991 </span>
1992 </label>
1993 </li>
1994 <li>
1995 <input type="checkbox" name="<?php echo esc_attr(Metasync_Admin::option_key . '[common_robots_meta][nosnippet]') ?>" id="robots_common6" value="nosnippet" <?php isset($common_robots_meta['nosnippet']) ? checked('nosnippet', $common_robots_meta['nosnippet']) : '' ?>>
1996 <label for="robots_common6">No Snippet </br>
1997 <span class="description">
1998 <span>Search engines not snippet to show in the search results</span>
1999 </span>
2000 </label>
2001 </li>
2002 </ul>
2003 <?php
2004 }
2005
2006 /**
2007 * @deprecated Use common_robot_meta_tags_callback() instead
2008 */
2009 public function common_robot_mata_tags_callback()
2010 {
2011 return $this->common_robot_meta_tags_callback();
2012 }
2013
2014 public function advance_robot_meta_tags_callback()
2015 {
2016 $advance_robots_meta = Metasync::get_option('advance_robots_meta') ?? Metasync::get_option('advance_robots_mata') ?? '';
2017
2018 $snippet_advance_robots_enable = $advance_robots_meta['max-snippet']['enable'] ?? '';
2019 $snippet_advance_robots_length = $advance_robots_meta['max-snippet']['length'] ?? '-1';
2020 $video_advance_robots_enable = $advance_robots_meta['max-video-preview']['enable'] ?? '';
2021 $video_advance_robots_length = $advance_robots_meta['max-video-preview']['length'] ?? '-1';
2022 $image_advance_robots_enable = $advance_robots_meta['max-image-preview']['enable'] ?? '';
2023 $image_advance_robots_length = $advance_robots_meta['max-image-preview']['length'] ?? '';
2024
2025 ?>
2026 <ul class="checkbox-list">
2027 <li>
2028 <label for="advanced_robots_snippet">
2029 <input type="checkbox" name="<?php echo esc_attr(Metasync_Admin::option_key . '[advance_robots_meta][max-snippet][enable]') ?>" id="advanced_robots_snippet" value="1" <?php checked('1', esc_attr($snippet_advance_robots_enable)) ?>>
2030 Snippet </br>
2031 <input type="number" class="input-length" name="<?php echo esc_attr(Metasync_Admin::option_key . '[advance_robots_meta][max-snippet][length]') ?>" id="advanced_robots_snippet_value" value="<?php echo esc_attr($snippet_advance_robots_length); ?>" min="-1"> </br>
2032 <span class="description">
2033 <span>Add maximum text-length, in characters, of a snippet for your page.</span>
2034 </span>
2035 </label>
2036 </li>
2037 <li>
2038 <label for="advanced_robots_video">
2039 <input type="checkbox" name="<?php echo esc_attr(Metasync_Admin::option_key . '[advance_robots_meta][max-video-preview][enable]') ?>" id="advanced_robots_video" value="1" <?php checked('1', esc_attr($video_advance_robots_enable)) ?>>
2040 Video Preview </br>
2041 <input type="number" class="input-length" name="<?php echo esc_attr(Metasync_Admin::option_key . '[advance_robots_meta][max-video-preview][length]') ?>" id="advanced_robots_video_value" value="<?php echo esc_attr($video_advance_robots_length); ?>" min="-1"> </br>
2042 <span class="description">
2043 <span>Add maximum duration in seconds of an animated video preview.</span>
2044 </span>
2045 </label>
2046 </li>
2047 <li>
2048 <label for="advanced_robots_image">
2049 <input type="checkbox" name="<?php echo esc_attr(Metasync_Admin::option_key . '[advance_robots_meta][max-image-preview][enable]') ?>" id="advanced_robots_image" value="1" <?php checked('1', esc_attr($image_advance_robots_enable)); ?>>
2050 Image Preview </br>
2051 <select class="input-length" name="<?php echo esc_attr(Metasync_Admin::option_key . '[advance_robots_meta][max-image-preview][length]') ?>" id="advanced_robots_image_value">
2052 <option value="large" <?php selected('large', esc_attr($image_advance_robots_length)) ?>>Large</option>
2053 <option value="standard" <?php selected('standard', esc_attr($image_advance_robots_length)) ?>>Standard</option>
2054 <option value="none" <?php selected('none', esc_attr($image_advance_robots_length)) ?>>None</option>
2055 </select>
2056 </br>
2057 <span class="description">
2058 <span>Add maximum size of image preview to show the images on this page.</span>
2059 </span>
2060 </label>
2061 </li>
2062 </ul>
2063 <?php
2064 }
2065
2066 /**
2067 * @deprecated Use advance_robot_meta_tags_callback() instead
2068 */
2069 public function advance_robot_mata_tags_callback()
2070 {
2071 return $this->advance_robot_meta_tags_callback();
2072 }
2073
2074 public function global_twitter_card_type_callback()
2075 {
2076 $twitter_card_type = Metasync::get_option('twitter_card_type') ?? '';
2077 ?>
2078
2079 <select class="input-length" name="<?php echo esc_attr(Metasync_Admin::option_key . '[twitter_card_type]') ?>" id="twitter_card_type">
2080 <option value="summary_large_image" <?php selected('summary_large_image', esc_attr($twitter_card_type)) ?>>Summary Large Image</option>
2081 <option value="summary_card" <?php selected('summary_card', esc_attr($twitter_card_type)) ?>>Summary Card</option>
2082 </select>
2083
2084 <?php
2085 }
2086
2087 public function global_open_graph_meta_callback()
2088 {
2089 printf(
2090 '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][open_graph_meta_tags]" value="true" %s />',
2091 isset(Metasync::get_option('common_meta_settings')['open_graph_meta_tags']) && Metasync::get_option('common_meta_settings')['open_graph_meta_tags'] == 'true' ? 'checked' : ''
2092 );
2093 printf(' <br> <span class="description"> Automatically add the Open Graph meta tags in a page or post.</span>');
2094 }
2095
2096 public function global_facebook_meta_callback()
2097 {
2098 printf(
2099 '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][facebook_meta_tags]" value="true" %s />',
2100 isset(Metasync::get_option('common_meta_settings')['facebook_meta_tags']) && Metasync::get_option('common_meta_settings')['facebook_meta_tags'] == 'true' ? 'checked' : ''
2101 );
2102 printf(' <br> <span class="description"> Automatically add the Facebook meta tags in a page or post.</span>');
2103 }
2104
2105 public function global_twitter_meta_callback()
2106 {
2107 printf(
2108 '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][twitter_meta_tags]" value="true" %s />',
2109 isset(Metasync::get_option('common_meta_settings')['twitter_meta_tags']) && Metasync::get_option('common_meta_settings')['twitter_meta_tags'] == 'true' ? 'checked' : ''
2110 );
2111 printf(' <br> <span class="description"> Automatically add the Twitter meta tags in a page or post.</span>');
2112 }
2113
2114 public function og_image_dimensions_callback()
2115 {
2116 $val = Metasync::get_option('common_meta_settings')['og_image_dimensions'] ?? 'true';
2117 printf(
2118 '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][og_image_dimensions]" value="true" %s />',
2119 $val === 'true' ? 'checked' : ''
2120 );
2121 printf('<span class="description"> Output og:image:width, og:image:height, and og:image:type when OG image metadata is available.</span>');
2122 }
2123
2124 public function article_timestamps_callback()
2125 {
2126 $val = Metasync::get_option('common_meta_settings')['article_timestamps'] ?? 'true';
2127 printf(
2128 '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][article_timestamps]" value="true" %s />',
2129 $val === 'true' ? 'checked' : ''
2130 );
2131 printf('<span class="description"> Output article:published_time and article:modified_time from the post publish and modified dates (ISO 8601).</span>');
2132 }
2133
2134 public function article_author_callback()
2135 {
2136 $val = Metasync::get_option('common_meta_settings')['article_author'] ?? 'true';
2137 printf(
2138 '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][article_author]" value="true" %s />',
2139 $val === 'true' ? 'checked' : ''
2140 );
2141 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>');
2142 }
2143
2144 public function article_section_callback()
2145 {
2146 $val = Metasync::get_option('common_meta_settings')['article_section'] ?? 'true';
2147 printf(
2148 '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][article_section]" value="true" %s />',
2149 $val === 'true' ? 'checked' : ''
2150 );
2151 printf('<span class="description"> Output article:section from the primary category (falls back to first category).</span>');
2152 }
2153
2154 public function article_tags_callback()
2155 {
2156 $val = Metasync::get_option('common_meta_settings')['article_tags'] ?? 'true';
2157 printf(
2158 '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][article_tags]" value="true" %s />',
2159 $val === 'true' ? 'checked' : ''
2160 );
2161 printf('<span class="description"> Output one article:tag meta tag per WordPress post tag.</span>');
2162 }
2163
2164 public function twitter_image_alt_callback()
2165 {
2166 $val = Metasync::get_option('common_meta_settings')['twitter_image_alt'] ?? 'true';
2167 printf(
2168 '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][twitter_image_alt]" value="true" %s />',
2169 $val === 'true' ? 'checked' : ''
2170 );
2171 printf('<span class="description"> Output twitter:image:alt using the stored alt text or the OG image attachment alt attribute.</span>');
2172 }
2173
2174 public function facebook_page_url_callback()
2175 {
2176 $facebook_page_url = Metasync::get_option('social_meta')['facebook_page_url'] ?? '';
2177 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));
2178 printf('<br><span class="description"> Enter your Facebook page URL. eg: <code>https://www.facebook.com/MetaSync/</code> </span>');
2179 }
2180
2181 public function facebook_authorship_callback()
2182 {
2183 $facebook_authorship = Metasync::get_option('social_meta')['facebook_authorship'] ?? '';
2184 printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_authorship]" value="%s" size="50" />', esc_attr($facebook_authorship));
2185 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>');
2186 }
2187
2188 public function facebook_admin_callback()
2189 {
2190 $facebook_admin = Metasync::get_option('social_meta')['facebook_admin'] ?? '';
2191 printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_admin]" value="%s" size="50" />', esc_attr($facebook_admin));
2192 printf(' <br> <span class="description"> Enter numeric user ID of Facebook. </span>');
2193 }
2194
2195 public function facebook_app_callback()
2196 {
2197 $facebook_app = Metasync::get_option('social_meta')['facebook_app'] ?? '';
2198 printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_app]" value="%s" size="50" />', esc_attr($facebook_app));
2199 printf(' <br> <span class="description"> Enter numeric app ID of Facebook </span>');
2200 }
2201
2202 public function facebook_secret_callback()
2203 {
2204 $facebook_secret = Metasync::get_option('social_meta')['facebook_secret'] ?? '';
2205 printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_secret]" value="%s" size="50" />', esc_attr($facebook_secret));
2206 printf(' <br> <span class="description"> Enter alphanumeric access token from Facebook. </span>');
2207 }
2208
2209 public function twitter_username_callback()
2210 {
2211 $twitter_username = Metasync::get_option('social_meta')['twitter_username'] ?? '';
2212 printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][twitter_username]" value="%s" size="50" />', esc_attr($twitter_username));
2213 printf(' <br> <span class="description"> Twitter username of the author to add <code>twitter:creator</code> tag to post. eg: <code>MetaSync</code> </span>');
2214 }
2215
2216 public static function get_business_types()
2217 {
2218 $business_type = [
2219 'Airline',
2220 'Consortium',
2221 'Corporation',
2222 'Educational Organization',
2223 'College Or University',
2224 'Elementary School',
2225 'High School',
2226 'Middle School',
2227 'Preschool',
2228 'School',
2229 'Funding Scheme',
2230 'Government Organization',
2231 'Library System',
2232 'Local Business',
2233 'Animal Shelter',
2234 'Archive Organization',
2235 'Automotive Business',
2236 'Auto Body Shop',
2237 'Auto Dealer',
2238 'Auto Parts Store',
2239 'Auto Rental',
2240 'Auto Repair',
2241 'Auto Wash',
2242 'Gas Station',
2243 'Motorcycle Dealer',
2244 'Motorcycle Repair',
2245 'Child Care',
2246 'Dry Cleaning Or Laundry',
2247 'Emergency Service',
2248 'Fire Station',
2249 'Hospital',
2250 'Police Station',
2251 'Employment Agency',
2252 'Entertainment Business',
2253 'Adult Entertainment',
2254 'Amusement Park',
2255 'Art Gallery',
2256 'Casino',
2257 'Comedy Club',
2258 'Movie Theater',
2259 'Night Club',
2260 'Financial Service',
2261 'Accounting Service',
2262 'Automated Teller',
2263 'Bank Or CreditUnion',
2264 'Insurance Agency',
2265 'Food Establishment',
2266 'Bakery',
2267 'Bar Or Pub',
2268 'Brewery',
2269 'Cafe Or CoffeeShop',
2270 'Distillery',
2271 'Fast Food Restaurant',
2272 'IceCream Shop',
2273 'Restaurant',
2274 'Winery',
2275 'Government Office',
2276 'Post Office',
2277 'Health And Beauty Business',
2278 'Beauty Salon',
2279 'Day Spa',
2280 'Hair Salon',
2281 'Health Club',
2282 'Nail Salon',
2283 'Tattoo Parlor',
2284 'Home And Construction Business',
2285 'Electrician',
2286 'General Contractor',
2287 'HVAC Business',
2288 'House Painter',
2289 'Locksmith',
2290 'Moving Company',
2291 'Plumber',
2292 'Roofing Contractor',
2293 'Internet Cafe',
2294 'Legal Service',
2295 'Attorney',
2296 'Notary',
2297 'Library',
2298 'Lodging Business',
2299 'Bed And Breakfast',
2300 'Campground',
2301 'Hostel',
2302 'Hotel',
2303 'Motel',
2304 'Resort',
2305 'Ski Resort',
2306 'Medical Business',
2307 'Community Health',
2308 'Dentist',
2309 'Dermatology',
2310 'Diet Nutrition',
2311 'Emergency',
2312 'Geriatric',
2313 'Gynecologic',
2314 'Medical Clinic',
2315 'Optician',
2316 'Pharmacy',
2317 'Physician',
2318 'Professional Service',
2319 'Radio Station',
2320 'Real Estate Agent',
2321 'Recycling Center',
2322 'Self Storage',
2323 'Shopping Center',
2324 'Sports Activity Location',
2325 'Bowling Alley',
2326 'Exercise Gym',
2327 'Golf Course',
2328 'Public Swimming Pool',
2329 'Ski Resort',
2330 'Sports Club',
2331 'Stadium Or Arena',
2332 'Tennis Complex',
2333 'Store',
2334 'Bike Store',
2335 'Book Store',
2336 'Clothing Store',
2337 'Computer Store',
2338 'Convenience Store',
2339 'Department Store',
2340 'Electronics Store',
2341 'Florist',
2342 'Furniture Store',
2343 'Garden Store',
2344 'Grocery Store',
2345 'Hardware Store',
2346 'Hobby Shop',
2347 'Home Goods Store',
2348 'Jewelry Store',
2349 'Liquor Store',
2350 'Mens Clothing Store',
2351 'Mobile Phone Store',
2352 'Movie Rental Store',
2353 'Music Store',
2354 'Office Equipment Store',
2355 'Outlet Store',
2356 'Pawn Shop',
2357 'Pet Store',
2358 'Shoe Store',
2359 'Sporting GoodsStore',
2360 'Tire Shop',
2361 'Toy Store',
2362 'Wholesale Store',
2363 'Television Station',
2364 'Tourist Information Center',
2365 'Travel Agency',
2366 'Tree Services',
2367 'Medical Organization',
2368 'Diagnostic Lab',
2369 'Veterinary Care',
2370 'NGO',
2371 'News Media Organization',
2372 'Performing Group',
2373 'Dance Group',
2374 'Music Group',
2375 'Theater Group',
2376 'Project',
2377 'Funding Agency',
2378 'Research Project',
2379 'Sports Organization',
2380 'Sports Team',
2381 'Workers Union',
2382 ];
2383
2384 return $business_type;
2385 }
2386
2387 /**
2388 * Render breadcrumbs settings section
2389 *
2390 * Rendered by the dedicated Breadcrumbs admin page. The three checkbox
2391 * fields each ship a hidden companion input so their keys are always
2392 * present in the submitted payload, which lets the save handlers treat an
2393 * absent key as "this form does not manage that field" instead of
2394 * "unchecked".
2395 */
2396 public function render_breadcrumbs_section() {
2397 $options = Metasync::get_option('breadcrumbs', array());
2398
2399 $defaults = array(
2400 'enabled' => true,
2401 'separator' => '»',
2402 'home_label' => 'Home',
2403 'home_url' => '',
2404 'show_current_page' => true,
2405 'prefix_text' => '',
2406 'archive_label_format' => '{name}',
2407 'disable_schema' => false,
2408 );
2409
2410 $options = wp_parse_args($options, $defaults);
2411
2412 # Older builds stored the separator as an HTML entity (e.g. "&raquo;").
2413 # Decode before matching so the saved choice still lights up its radio.
2414 $separator_presets = array('»', '/', '>', '·');
2415 $separator_current = html_entity_decode((string) $options['separator'], ENT_QUOTES, 'UTF-8');
2416 if ($separator_current === '') {
2417 $separator_current = '»';
2418 }
2419 # A separator that predates the radio picker (or came from the old free
2420 # text field) gets its own option so it stays selected and round-trips.
2421 $separator_custom = in_array($separator_current, $separator_presets, true) ? '' : $separator_current;
2422 ?>
2423 <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;">
2424
2425 <!-- Enable/Disable -->
2426 <div style="margin-bottom: 24px;">
2427 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2428 <input type="hidden" name="metasync_options[breadcrumbs][enabled]" value="0">
2429 <input type="checkbox"
2430 name="metasync_options[breadcrumbs][enabled]"
2431 value="1"
2432 <?php checked($options['enabled'], 1); ?>
2433 style="width: 18px; height: 18px; cursor: pointer;">
2434 <span style="font-weight: 500; color: var(--dashboard-text);">Enable breadcrumbs globally</span>
2435 <?php Metasync::render_tooltip_icon('breadcrumbs_enabled', 'Shows a trail of links (e.g. Home > Blog > This Post) near the top of your pages so visitors can see where they are and navigate back up a level. Turn off to hide breadcrumbs everywhere.'); ?>
2436 </label>
2437 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2438 Enable or disable breadcrumb navigation on your site
2439 </p>
2440 </div>
2441
2442 <!-- Disable Schema Markup -->
2443 <div style="margin-bottom: 24px;">
2444 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2445 <input type="hidden" name="metasync_options[breadcrumbs][disable_schema]" value="0">
2446 <input type="checkbox"
2447 name="metasync_options[breadcrumbs][disable_schema]"
2448 value="1"
2449 <?php checked($options['disable_schema'], 1); ?>
2450 style="width: 18px; height: 18px; cursor: pointer;">
2451 <span style="font-weight: 500; color: var(--dashboard-text);">Disable breadcrumb schema markup (JSON-LD)</span>
2452 <?php Metasync::render_tooltip_icon('breadcrumbs_disable_schema', 'Leave off. Only check this if another SEO plugin (Yoast, Rank Math, etc.) already sends breadcrumb data to Google, so you don\'t send it twice.'); ?>
2453 </label>
2454 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2455 When checked, MetaSync will not output the <code style="background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px;">BreadcrumbList</code> JSON-LD in <code style="background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px;">&lt;head&gt;</code>. Use this if another SEO plugin already handles breadcrumb schema.
2456 </p>
2457 </div>
2458
2459 <!-- Separator Character -->
2460 <div style="margin-bottom: 24px;">
2461 <label for="breadcrumb_separator" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2462 Separator Character
2463 </label>
2464 <div style="display: flex; gap: 12px; flex-wrap: wrap;">
2465 <?php foreach ($separator_presets as $separator_option) : ?>
2466 <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2467 <input type="radio" name="metasync_options[breadcrumbs][separator]" value="<?php echo esc_attr($separator_option); ?>" <?php checked($separator_current, $separator_option); ?>>
2468 <span style="color: var(--dashboard-text);"><?php echo esc_html($separator_option); ?></span>
2469 </label>
2470 <?php endforeach; ?>
2471 <div style="display: flex; align-items: center; gap: 8px;">
2472 <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2473 <input type="radio" id="metasync-breadcrumb-separator-custom" name="metasync_options[breadcrumbs][separator]" value="<?php echo esc_attr($separator_custom); ?>" <?php checked($separator_custom !== ''); ?>>
2474 <span style="color: var(--dashboard-text);">Custom</span>
2475 </label>
2476 <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;">
2477 </div>
2478 </div>
2479 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2480 Choose how to separate breadcrumb items
2481 </p>
2482 </div>
2483
2484 <!-- Home Label -->
2485 <div style="margin-bottom: 24px;">
2486 <label for="breadcrumb_home_label" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2487 Home Label
2488 </label>
2489 <input type="text"
2490 id="breadcrumb_home_label"
2491 name="metasync_options[breadcrumbs][home_label]"
2492 value="<?php echo esc_attr($options['home_label']); ?>"
2493 placeholder="Home"
2494 style="width: 100%; padding: 10px 12px; background: var(--dashboard-input-bg); border: 1px solid var(--dashboard-border); border-radius: 6px; color: var(--dashboard-text); font-size: 14px; box-sizing: border-box;">
2495 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2496 Label for the home/root breadcrumb item
2497 </p>
2498 </div>
2499
2500 <!-- Home URL -->
2501 <div style="margin-bottom: 24px;">
2502 <label for="breadcrumb_home_url" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2503 Home URL
2504 </label>
2505 <input type="url"
2506 id="breadcrumb_home_url"
2507 name="metasync_options[breadcrumbs][home_url]"
2508 value="<?php echo esc_attr($options['home_url']); ?>"
2509 placeholder="<?php echo esc_attr(home_url('/')); ?>"
2510 style="width: 100%; padding: 10px 12px; background: var(--dashboard-input-bg); border: 1px solid var(--dashboard-border); border-radius: 6px; color: var(--dashboard-text); font-size: 14px; box-sizing: border-box;">
2511 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2512 Leave empty to use your site home URL
2513 </p>
2514 </div>
2515
2516 <!-- Show Current Page -->
2517 <div style="margin-bottom: 24px;">
2518 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2519 <input type="hidden" name="metasync_options[breadcrumbs][show_current_page]" value="0">
2520 <input type="checkbox"
2521 name="metasync_options[breadcrumbs][show_current_page]"
2522 value="1"
2523 <?php checked($options['show_current_page'], 1); ?>
2524 style="width: 18px; height: 18px; cursor: pointer;">
2525 <span style="font-weight: 500; color: var(--dashboard-text);">Show current page</span>
2526 </label>
2527 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2528 Include the current page as the last (unlinked) breadcrumb item
2529 </p>
2530 </div>
2531
2532 <!-- Prefix Text -->
2533 <div style="margin-bottom: 24px;">
2534 <label for="breadcrumb_prefix" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2535 Prefix Text
2536 </label>
2537 <input type="text"
2538 id="breadcrumb_prefix"
2539 name="metasync_options[breadcrumbs][prefix_text]"
2540 value="<?php echo esc_attr($options['prefix_text']); ?>"
2541 placeholder='e.g., "You are here:"'
2542 style="width: 100%; padding: 10px 12px; background: var(--dashboard-input-bg); border: 1px solid var(--dashboard-border); border-radius: 6px; color: var(--dashboard-text); font-size: 14px; box-sizing: border-box;">
2543 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2544 Optional text to display before the breadcrumbs (leave empty for none)
2545 </p>
2546 </div>
2547
2548 <!-- Archive Label Format -->
2549 <div style="margin-bottom: 24px;">
2550 <label for="breadcrumb_archive_format" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2551 Archive Label Format
2552 <?php Metasync::render_tooltip_icon('breadcrumb_archive_format', 'On category/archive pages, {name} is replaced with that section\'s name. Type {name} alone for just the name, or something like "Category: {name}".'); ?>
2553 </label>
2554 <input type="text"
2555 id="breadcrumb_archive_format"
2556 name="metasync_options[breadcrumbs][archive_label_format]"
2557 value="<?php echo esc_attr($options['archive_label_format']); ?>"
2558 placeholder="{name}"
2559 style="width: 100%; padding: 10px 12px; background: var(--dashboard-input-bg); border: 1px solid var(--dashboard-border); border-radius: 6px; color: var(--dashboard-text); font-size: 14px; box-sizing: border-box;">
2560 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2561 Use <code style="background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px;">{name}</code> as a placeholder for the archive name. Example: <code style="background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px;">Category: {name}</code>
2562 </p>
2563 </div>
2564
2565 </div>
2566 <?php
2567 }
2568 }
2569