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

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

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