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

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

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