PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.0
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.0
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.0, at includes/class-metasync-settings-fields.php

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