PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.11
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.11
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / includes / class-metasync-settings-fields.php

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

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