PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.10
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.10
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 2.6.4 2.6.3 2.5.23 All 138 releases
metasync / includes / class-metasync-settings-fields.php

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

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