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

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

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