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

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

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