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

class-metasync-admin-navigation.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.22, at includes/class-metasync-admin-navigation.php

1,983 lines 101.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 exit;
4 }
5
6 /**
7 * Admin navigation header, nav tabs, and admin-bar status indicator.
8 *
9 * Extracted from Metasync_Admin to keep the admin class focused on
10 * non-UI concerns. All header/nav rendering and admin-bar badge
11 * logic lives here.
12 *
13 * @package Metasync
14 * @subpackage Metasync/includes
15 */
16 class Metasync_Admin_Navigation
17 {
18 /** @var self|null */
19 private static $instance = null;
20
21 const CACHE_KEY = 'metasync_admin_bar_status';
22
23 /**
24 * Menu item keys that retain a WordPress admin sidebar row in short mode.
25 *
26 * Everything else is registered with an empty parent slug
27 * (add_submenu_page('', ...)) so the page stays reachable at its
28 * existing URL but does not claim a sidebar row. The in-page sidebar
29 * (render_sidenav) continues to show every item.
30 */
31 const WP_MENU_KEEP_LIST = [
32 'dashboard', // Dashboard
33 'seo_controls', // Indexation Control
34 'redirections', // Redirections
35 'monitor_404', // 404 Monitor
36 'xml_sitemap', // XML Sitemap
37 'robots_txt', // Robots.txt
38 'schema_markup', // Schema Markup
39 'seo_health', // SEO Health
40 'general', // Settings
41 ];
42
43 /**
44 * Whether the Settings page (general) is reachable for the current user.
45 *
46 * Settings is the floor — the top-level "Search Atlas" menu item is
47 * registered with the Settings callback, so it must always point at a
48 * page the user is permitted to see. When Settings is hidden by access
49 * control, the top-level callback falls back to the first reachable
50 * keep-list page (see resolve_top_level_callback()).
51 *
52 * @return bool
53 */
54 public static function is_settings_reachable()
55 {
56 return Metasync_Access_Control::user_can_access('hide_settings');
57 }
58
59 /**
60 * Resolve the callback the top-level menu item should use.
61 *
62 * The top-level "Search Atlas" menu item is the floor — the plugin must
63 * always present at least one working entry point. When Settings is
64 * reachable it is the floor (its callback also renders the Settings
65 * page). When Settings is hidden, fall back to the first reachable
66 * keep-list page callback so the parent never leads to a permission
67 * error or blank page.
68 *
69 * @param Metasync_Admin $admin The admin instance whose callbacks are used.
70 * @return array WordPress-style callback array ([$admin, 'method']).
71 */
72 public static function resolve_top_level_callback($admin)
73 {
74 if (self::is_settings_reachable()) {
75 return array($admin, 'create_admin_settings_page');
76 }
77
78 // Fall back to the first reachable keep-list item callback. The
79 // keep-list is ordered by priority (Dashboard, Indexation Control,
80 // Redirections, 404 Monitor, XML Sitemap, Robots.txt, SEO Health,
81 // Settings). We reuse get_available_menu_items() so access-control
82 // and connection-state gates are respected identically to the
83 // sidebar.
84 $available = self::instance()->get_available_menu_items();
85 foreach (self::WP_MENU_KEEP_LIST as $key) {
86 if (isset($available[$key])) {
87 return array($admin, $available[$key]['callback']);
88 }
89 }
90
91 // No keep-list item is reachable — keep Settings as the last resort
92 // so the menu never leads to a blank page. Settings itself handles
93 // its own access-control rendering.
94 return array($admin, 'create_admin_settings_page');
95 }
96
97 /**
98 * The ordered keep-list of menu item keys that always retain a
99 * WordPress admin sidebar row (in short mode).
100 *
101 * @return string[]
102 */
103 public static function get_wp_menu_keep_list()
104 {
105 return self::WP_MENU_KEEP_LIST;
106 }
107
108 /**
109 * Whether a given menu item key retains a WordPress admin sidebar row.
110 *
111 * Only keep-list items retain a WordPress sidebar row. All other pages
112 * remain registered and available from the in-page navigation.
113 *
114 * @param string $item_key Menu item key from get_available_menu_items().
115 * @return bool
116 */
117 public static function item_keeps_wp_sidebar_row($item_key)
118 {
119 return in_array($item_key, self::WP_MENU_KEEP_LIST, true);
120 }
121
122 /**
123 * Get singleton instance.
124 *
125 * @return self
126 */
127 public static function instance()
128 {
129 if (self::$instance === null) {
130 self::$instance = new self();
131 }
132 return self::$instance;
133 }
134
135 private function __construct() {}
136
137 /**
138 * Whether the Dashboard is hidden by the "Hide Dashboard" general setting
139 * (hide_dashboard_framework).
140 *
141 * When enabled this removes the Dashboard from the WordPress admin submenu
142 * and from the in-plugin SEO feature navigation, matching the setting's
143 * description "Hide the main dashboard from the WordPress admin menu".
144 *
145 * @return bool
146 */
147 public static function is_dashboard_hidden_by_framework()
148 {
149 $general_options = Metasync::get_option('general') ?? [];
150 return filter_var($general_options['hide_dashboard_framework'] ?? false, FILTER_VALIDATE_BOOLEAN);
151 }
152
153 // ------------------------------------------------------------------
154 // Logo resolution helper
155 // ------------------------------------------------------------------
156
157 /**
158 * Resolve which logo(s) to display based on whitelabel settings.
159 *
160 * @return array{show_logo: bool, use_dual: bool, url: string, light_url: string, dark_url: string, is_default: bool}
161 */
162 private function resolve_logo_data()
163 {
164 $wl_settings = Metasync::get_whitelabel_settings();
165 $is_whitelabel = !empty($wl_settings['is_whitelabel']);
166
167 $light_raw = Metasync::get_whitelabel_logo_light();
168 $dark_raw = Metasync::get_whitelabel_logo_dark();
169 $has_light = !empty($light_raw) && filter_var($light_raw, FILTER_VALIDATE_URL);
170 $has_dark = !empty($dark_raw) && filter_var($dark_raw, FILTER_VALIDATE_URL);
171 $use_dual = ($has_light && $has_dark && $light_raw !== $dark_raw);
172
173 $data = [
174 'show_logo' => false,
175 'use_dual' => false,
176 'url' => '',
177 'light_url' => '',
178 'dark_url' => '',
179 'is_default' => false,
180 ];
181
182 if ($use_dual) {
183 $data['show_logo'] = true;
184 $data['use_dual'] = true;
185 $data['light_url'] = $light_raw;
186 $data['dark_url'] = $dark_raw;
187 } else {
188 $single = $has_light ? $light_raw : ($has_dark ? $dark_raw : null);
189 if ($single) {
190 $data['show_logo'] = true;
191 $data['url'] = $single;
192 } elseif (!$is_whitelabel) {
193 $data['show_logo'] = true;
194 $data['url'] = plugin_dir_url(dirname(__FILE__)) . 'assets/images/searchatlas-logo.svg';
195 $data['is_default'] = true;
196 }
197 }
198
199 return $data;
200 }
201
202 // ------------------------------------------------------------------
203 // Header + Nav rendering
204 // ------------------------------------------------------------------
205
206 /**
207 * Render the standard header followed by the navigation tabs.
208 */
209 public function render_standard_header_nav($page_title = null, $current_page = null)
210 {
211 $this->render_static_header($page_title);
212 $this->render_static_navigation($current_page);
213 }
214
215 /**
216 * Render the page header (logo, connection badge, theme toggle).
217 */
218 public function render_static_header($page_title = null)
219 {
220 $effective_plugin_name = Metasync::get_effective_plugin_name();
221 $display_title = $page_title ?: $effective_plugin_name;
222 $logo = $this->resolve_logo_data();
223
224 $current_theme = get_option('metasync_theme', 'dark');
225 $general_settings = Metasync::get_option('general');
226 // derive the badge from confirmed heartbeat health, not from
227 // mere API-key presence (which stays set after Search Atlas revokes it).
228 $badge = Metasync_Heartbeat_Manager::instance()->get_connection_badge($general_settings);
229 ?>
230 <div class="metasync-header" data-current-theme="<?php echo esc_attr($current_theme); ?>">
231 <div class="metasync-header-left">
232 <?php if ($logo['show_logo'] && $logo['use_dual']): ?>
233 <div class="metasync-logo-container">
234 <img src="<?php echo esc_url($logo['light_url']); ?>" alt="Logo" class="metasync-logo metasync-logo-light" />
235 <img src="<?php echo esc_url($logo['dark_url']); ?>" alt="Logo" class="metasync-logo metasync-logo-dark" />
236 </div>
237 <?php elseif ($logo['show_logo'] && !empty($logo['url'])): ?>
238 <div class="metasync-logo-container">
239 <img src="<?php echo esc_url($logo['url']); ?>" alt="Logo" class="metasync-logo<?php echo !empty($logo['is_default']) ? ' metasync-logo-default' : ''; ?>" />
240 </div>
241 <?php endif; ?>
242 </div>
243 <div class="metasync-header-right">
244 <div class="metasync-status <?php echo esc_attr($badge['class']); ?>">
245 <span class="status-dot"></span>
246 <span class="status-text"><?php echo esc_html($badge['text']); ?></span>
247 </div>
248 <button type="button" class="metasync-theme-toggle" onclick="toggleMetasyncTheme()" title="Toggle theme">
249 <span class="theme-icon-light">&#9728;</span>
250 <span class="theme-icon-dark">&#9790;</span>
251 </button>
252 </div>
253 </div>
254 <?php
255 }
256
257 /**
258 * Render the horizontal navigation tabs.
259 */
260 public function render_static_navigation($current_page = null)
261 {
262 $general_options = Metasync::get_option('general') ?? [];
263 $whitelabel_settings = Metasync::get_whitelabel_settings();
264 $page_slug = Metasync_Admin::$page_slug;
265
266 $menu_items = [];
267 $menu_icons = [
268 'dashboard' => 'dashboard',
269 'seo_controls' => 'search',
270 'instant_index' => 'performance',
271 'google_console' => 'chart-area',
272 'compatibility' => 'admin-tools',
273 'sync_log' => 'list-view',
274 'redirections' => 'undo',
275 'robots_txt' => 'shield',
276 'xml_sitemap' => 'networking',
277 'custom_pages' => 'admin-page',
278 'report_issue' => 'sos',
279 'general' => 'admin-settings',
280 ];
281
282 if (empty($whitelabel_settings['hide_dashboard']) && !self::is_dashboard_hidden_by_framework()) {
283 $menu_items['dashboard'] = ['title' => 'Dashboard', 'slug_suffix' => '-dashboard'];
284 }
285
286 if (empty($whitelabel_settings['hide_indexation_control'])) {
287 $menu_items['seo_controls'] = ['title' => 'Indexation Control', 'slug_suffix' => '-seo-controls'];
288 }
289
290 if ($general_options['enable_googleinstantindex'] ?? false) {
291 $menu_items['instant_index'] = ['title' => 'Instant Indexing', 'slug_suffix' => '-instant-index'];
292 }
293
294 if ($general_options['enable_google_console'] ?? false) {
295 $menu_items['google_console'] = ['title' => 'Google Console', 'slug_suffix' => '-google-console'];
296 }
297
298 if (empty($whitelabel_settings['hide_compatibility'])) {
299 $menu_items['compatibility'] = ['title' => 'Compatibility', 'slug_suffix' => '-compatibility'];
300 }
301
302 if (empty($whitelabel_settings['hide_sync_log'])) {
303 $menu_items['sync_log'] = ['title' => 'Changes Log', 'slug_suffix' => '-sync-log'];
304 }
305
306 if (empty($whitelabel_settings['hide_redirections'])) {
307 $menu_items['redirections'] = ['title' => 'Redirections', 'slug_suffix' => '-redirections'];
308 }
309
310 if (empty($whitelabel_settings['hide_robots'])) {
311 $menu_items['robots_txt'] = ['title' => 'Robots.txt', 'slug_suffix' => '-robots-txt'];
312 }
313
314 $menu_items['xml_sitemap'] = ['title' => 'XML Sitemap', 'slug_suffix' => '-xml-sitemap'];
315 ?>
316 <div class="metasync-nav-wrapper">
317 <div class="metasync-nav-tabs">
318 <div class="metasync-nav-left">
319 <?php foreach ($menu_items as $key => $menu_item):
320 $is_active = ($current_page === $key);
321 $icon = $menu_icons[$key];
322 $page_url = '?page=' . $page_slug . $menu_item['slug_suffix'];
323 ?>
324 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-tab <?php echo $is_active ? 'active' : ''; ?>">
325 <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
326 <span class="tab-text"><?php echo esc_html($menu_item['title']); ?></span>
327 </a>
328 <?php endforeach; ?>
329 </div>
330 <div class="metasync-nav-right">
331 <a href="?page=<?php echo esc_attr( $page_slug ); ?>-custom-pages" class="metasync-nav-tab <?php echo $current_page === 'custom_pages' ? 'active' : ''; ?>" style="margin-right: 10px;">
332 <span class="tab-icon"><span class="dashicons dashicons-admin-page"></span></span>
333 <span class="tab-text">Custom Pages</span>
334 </a>
335 <a href="?page=<?php echo esc_attr( $page_slug ); ?>-report-issue" class="metasync-nav-tab <?php echo $current_page === 'report_issue' ? 'active' : ''; ?>">
336 <span class="tab-icon"><span class="dashicons dashicons-sos"></span></span>
337 <span class="tab-text">Report Issue</span>
338 </a>
339 <div class="metasync-simple-dropdown">
340 <button type="button" class="metasync-seo-btn" id="metasync-seo-btn" onclick="toggleSeoMenuPortal(event)" aria-expanded="false">
341 <span class="tab-icon"><span class="dashicons dashicons-search"></span></span>
342 <span class="tab-text">SEO</span>
343 <span class="dropdown-arrow"></span>
344 </button>
345 </div>
346 <div class="metasync-simple-dropdown">
347 <button type="button" class="metasync-settings-btn" id="metasync-settings-btn" onclick="toggleSettingsMenuPortal(event)" aria-expanded="false">
348 <span class="tab-icon"><span class="dashicons dashicons-admin-settings"></span></span>
349 <span class="tab-text">Settings</span>
350 <span class="dropdown-arrow"></span>
351 </button>
352 </div>
353 </div>
354 </div>
355 </div>
356
357 <script>
358 function toggleSeoMenuPortal(event) {
359 event.preventDefault();
360 event.stopPropagation();
361 var button = event.currentTarget;
362 var existingMenu = document.getElementById('metasync-seo-portal-menu');
363 if (existingMenu) {
364 existingMenu.remove();
365 button.classList.remove('active');
366 button.setAttribute('aria-expanded', 'false');
367 return;
368 }
369 var menu = document.createElement('div');
370 menu.id = 'metasync-seo-portal-menu';
371 menu.className = 'metasync-portal-menu';
372
373 var pageSlug = '<?php echo esc_js( $page_slug ); ?>';
374 var seoLinks = [
375 { href: '?page=' + pageSlug + '-seo-controls', text: 'Indexation Control' },
376 { href: '?page=' + pageSlug + '-xml-sitemap', text: 'XML Sitemap' },
377 { href: '?page=' + pageSlug + '-robots-txt', text: 'Robots.txt' },
378 { href: '?page=' + pageSlug + '-redirections', text: 'Redirections' },
379 ];
380 seoLinks.forEach(function(item) {
381 var link = document.createElement('a');
382 link.href = item.href;
383 link.className = 'metasync-portal-item';
384 link.textContent = item.text;
385 menu.appendChild(link);
386 });
387
388 var rect = button.getBoundingClientRect();
389 menu.style.position = 'fixed';
390 menu.style.top = (rect.bottom + 8) + 'px';
391 menu.style.right = (window.innerWidth - rect.right) + 'px';
392 menu.style.zIndex = '999999999';
393 document.body.appendChild(menu);
394 button.classList.add('active');
395 button.setAttribute('aria-expanded', 'true');
396 }
397
398 function toggleSettingsMenuPortal(event) {
399 event.preventDefault();
400 event.stopPropagation();
401 var button = event.currentTarget;
402 var existingMenu = document.getElementById('metasync-portal-menu');
403 if (existingMenu) {
404 existingMenu.remove();
405 button.classList.remove('active');
406 button.setAttribute('aria-expanded', 'false');
407 return;
408 }
409 var menu = document.createElement('div');
410 menu.id = 'metasync-portal-menu';
411 menu.className = 'metasync-portal-menu';
412
413 var hideAdvanced = <?php echo !empty($whitelabel_settings['hide_advanced']) ? 'true' : 'false'; ?>;
414 var showGeneral = <?php echo Metasync_Access_Control::user_can_access('hide_settings') ? 'true' : 'false'; ?>;
415
416 if (showGeneral) {
417 var generalLink = document.createElement('a');
418 generalLink.href = '?page=<?php echo esc_js( $page_slug ); ?>&tab=general';
419 generalLink.className = 'metasync-portal-item';
420 generalLink.textContent = 'General';
421 menu.appendChild(generalLink);
422 }
423
424 var whitelabelLink = document.createElement('a');
425 whitelabelLink.href = '?page=<?php echo esc_js( $page_slug ); ?>&tab=whitelabel';
426 whitelabelLink.className = 'metasync-portal-item';
427 whitelabelLink.textContent = 'White Label';
428 menu.appendChild(whitelabelLink);
429
430 if (!hideAdvanced) {
431 var advancedLink = document.createElement('a');
432 advancedLink.href = '?page=<?php echo esc_js( $page_slug ); ?>&tab=advanced';
433 advancedLink.className = 'metasync-portal-item';
434 advancedLink.textContent = 'Advanced';
435 menu.appendChild(advancedLink);
436 }
437
438
439 var rect = button.getBoundingClientRect();
440 menu.style.position = 'fixed';
441 menu.style.top = (rect.bottom + 8) + 'px';
442 menu.style.right = (window.innerWidth - rect.right) + 'px';
443 menu.style.zIndex = '999999999';
444 document.body.appendChild(menu);
445 button.classList.add('active');
446 button.setAttribute('aria-expanded', 'true');
447 }
448 document.addEventListener('click', function(event) {
449 var seoButton = document.getElementById('metasync-seo-btn');
450 var seoMenu = document.getElementById('metasync-seo-portal-menu');
451 if (seoMenu && seoButton && !seoButton.contains(event.target) && !seoMenu.contains(event.target)) {
452 seoMenu.remove();
453 seoButton.classList.remove('active');
454 seoButton.setAttribute('aria-expanded', 'false');
455 }
456
457 var button = document.getElementById('metasync-settings-btn');
458 var menu = document.getElementById('metasync-portal-menu');
459 if (menu && button && !button.contains(event.target) && !menu.contains(event.target)) {
460 menu.remove();
461 button.classList.remove('active');
462 button.setAttribute('aria-expanded', 'false');
463 }
464 });
465 </script>
466 <?php
467 }
468
469 // ------------------------------------------------------------------
470 // Menu registration + enhanced navigation (moved from Metasync_Admin)
471 // ------------------------------------------------------------------
472
473 /**
474 * Helper method to get available menu items based on configuration.
475 * Items are grouped into 'seo' (SEO Features) and 'plugin' (Plugin) categories.
476 */
477 public function get_available_menu_items()
478 {
479 $general_options = Metasync::get_option('general') ?? [];
480 $has_api_key = !empty($general_options['searchatlas_api_key'] ?? '');
481 $has_uuid = !empty($general_options['otto_pixel_uuid'] ?? '');
482 $is_fully_connected = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general_options);
483
484 $menu_items = [];
485
486 // === SEO FEATURES GROUP ===
487
488 // Dashboard (check access control + "Hide Dashboard" general setting)
489 if (Metasync_Access_Control::user_can_access('hide_dashboard') && !self::is_dashboard_hidden_by_framework()) {
490 $menu_items['dashboard'] = [
491 'title' => 'Dashboard',
492 'slug_suffix' => '-dashboard',
493 'callback' => 'create_admin_dashboard_iframe',
494 'internal_nav' => 'Dashboard',
495 'group' => 'seo'
496 ];
497 }
498
499 // Indexation Control (check access control)
500 if (Metasync_Access_Control::user_can_access('hide_indexation_control')) {
501 $menu_items['seo_controls'] = [
502 'title' => 'Indexation',
503 'slug_suffix' => '-seo-controls',
504 'callback' => 'create_admin_seo_controls_page',
505 'internal_nav' => 'Indexation Control',
506 'group' => 'seo'
507 ];
508 }
509
510 // Instant Indexing - setting now stored in seo_controls (moved from Settings to Indexation Control)
511 $seo_controls = Metasync::get_option('seo_controls');
512 if ($seo_controls['enable_googleinstantindex'] ?? false) {
513 $menu_items['instant_index'] = [
514 'title' => 'Instant Indexing',
515 'slug_suffix' => '-instant-index',
516 'callback' => 'create_admin_google_instant_index_page',
517 'internal_nav' => 'Instant Indexing',
518 'group' => 'seo'
519 ];
520 }
521
522 if ($general_options['enable_google_console'] ?? false) {
523 $menu_items['google_console'] = [
524 'title' => 'Google Console',
525 'slug_suffix' => '-google-console',
526 'callback' => 'create_admin_google_console_page',
527 'internal_nav' => 'Google Console',
528 'group' => 'seo'
529 ];
530 }
531
532 if ($general_options['enable_bing_console'] ?? false) {
533 $menu_items['bing_console'] = [
534 'title' => 'Bing Console',
535 'slug_suffix' => '-bing-console',
536 'callback' => 'create_admin_bing_console_page',
537 'internal_nav' => 'Bing Console',
538 'group' => 'seo'
539 ];
540 }
541
542 // Redirections page (check access control)
543 if (Metasync_Access_Control::user_can_access('hide_redirections')) {
544 $menu_items['redirections'] = [
545 'title' => 'Redirections',
546 'slug_suffix' => '-redirections',
547 'callback' => 'create_admin_redirections_page',
548 'internal_nav' => 'Redirections',
549 'group' => 'seo'
550 ];
551 }
552
553 // Robots.txt page (check access control)
554 if (Metasync_Access_Control::user_can_access('hide_robots')) {
555 $menu_items['robots_txt'] = [
556 'title' => 'Robots.txt',
557 'slug_suffix' => '-robots-txt',
558 'callback' => 'create_admin_robots_txt_page',
559 'internal_nav' => 'Robots.txt',
560 'group' => 'seo'
561 ];
562 }
563
564 // XML Sitemap page (check access control)
565 if (Metasync_Access_Control::user_can_access('hide_xml_sitemap')) {
566 $menu_items['xml_sitemap'] = [
567 'title' => 'XML Sitemap',
568 'slug_suffix' => '-xml-sitemap',
569 'callback' => 'create_admin_xml_sitemap_page',
570 'internal_nav' => 'XML Sitemap',
571 'group' => 'seo'
572 ];
573 }
574
575 // Schema Markup
576 $menu_items['schema_markup'] = [
577 'title' => 'Schema Markup',
578 'slug_suffix' => '-schema-markup',
579 'callback' => 'create_admin_schema_markup_page',
580 'internal_nav' => 'Schema Markup',
581 'group' => 'seo'
582 ];
583
584 // 404 Monitor (always available — SEO monitoring tool)
585 $menu_items['monitor_404'] = [
586 'title' => '404 Monitor',
587 'slug_suffix' => '-404-monitor',
588 'callback' => 'create_admin_404_monitor_page',
589 'internal_nav' => '404 Monitor',
590 'group' => 'seo'
591 ];
592
593 // Site Verification
594 $menu_items['site_verification'] = [
595 'title' => 'Site Verification',
596 'slug_suffix' => '-search-engine-verify',
597 'callback' => 'create_admin_search_engine_verification_page',
598 'internal_nav' => 'Site Verification',
599 'group' => 'seo'
600 ];
601
602 // SEO Health dashboard (always available)
603 $menu_items['seo_health'] = [
604 'title' => 'SEO Health',
605 'slug_suffix' => '-seo-health',
606 'callback' => 'create_admin_seo_health_page',
607 'internal_nav' => 'SEO Health',
608 'group' => 'seo'
609 ];
610
611 // Import SEO Data page (check access control)
612 if (Metasync_Access_Control::user_can_access('hide_import_seo')) {
613 $menu_items['import_seo'] = [
614 'title' => 'Import SEO Data',
615 'slug_suffix' => '-import-external',
616 'callback' => 'render_import_external_data_page',
617 'internal_nav' => 'Import SEO Data',
618 'group' => 'seo'
619 ];
620 }
621
622 // === PLUGIN GROUP ===
623
624 // Settings (check access control)
625 if (Metasync_Access_Control::user_can_access('hide_settings')) {
626 $menu_items['general'] = [
627 'title' => 'Settings',
628 'slug_suffix' => '',
629 'callback' => 'create_admin_settings_page',
630 'internal_nav' => 'General Settings',
631 'group' => 'plugin'
632 ];
633 }
634
635 // Local Business
636 $menu_items['local_business'] = [
637 'title' => 'Local Business',
638 'slug_suffix' => '-local-business',
639 'callback' => 'create_admin_local_business_page',
640 'internal_nav' => 'Local Business',
641 'group' => 'seo'
642 ];
643
644 // Breadcrumbs
645 $menu_items['breadcrumbs'] = [
646 'title' => 'Breadcrumbs',
647 'slug_suffix' => '-breadcrumbs',
648 'callback' => 'create_admin_breadcrumbs_page',
649 'internal_nav' => 'Breadcrumbs',
650 'group' => 'seo'
651 ];
652
653 // Code Snippets
654 $menu_items['code_snippets'] = [
655 'title' => 'Code Snippets',
656 'slug_suffix' => '-code-snippets',
657 'callback' => 'create_admin_code_snippets_page',
658 'internal_nav' => 'Code Snippets',
659 'group' => 'plugin'
660 ];
661
662 // Code Minification
663 $menu_items['code_minification'] = [
664 'title' => 'Code Minification',
665 'slug_suffix' => '-code-minification',
666 'callback' => 'create_admin_code_minification_page',
667 'internal_nav' => 'Code Minification',
668 'group' => 'plugin'
669 ];
670
671 // Media Optimization
672 $menu_items['media_optimization'] = [
673 'title' => 'Media Optimization',
674 'slug_suffix' => '-media-optimization',
675 'callback' => 'create_admin_media_optimization_page',
676 'internal_nav' => 'Media Optimization',
677 'group' => 'plugin'
678 ];
679
680 // Custom HTML Pages (check access control)
681 if (Metasync_Access_Control::user_can_access('hide_custom_pages')) {
682 $menu_items['custom_pages'] = [
683 'title' => 'Custom Pages',
684 'slug_suffix' => '-custom-pages',
685 'callback' => 'create_admin_custom_pages_page',
686 'internal_nav' => 'Custom HTML Pages',
687 'group' => 'plugin'
688 ];
689 }
690
691 // Compatibility page (check access control)
692 if (Metasync_Access_Control::user_can_access('hide_compatibility')) {
693 $menu_items['compatibility'] = [
694 'title' => 'Compatibility',
695 'slug_suffix' => '-compatibility',
696 'callback' => 'create_admin_compatibility_page',
697 'internal_nav' => 'Compatibility',
698 'group' => 'plugin'
699 ];
700 }
701
702 // Sync Log page (check access control)
703 if (Metasync_Access_Control::user_can_access('hide_sync_log')) {
704 $menu_items['sync_log'] = [
705 'title' => 'Changes Log',
706 'slug_suffix' => '-sync-log',
707 'callback' => 'create_admin_sync_log_page',
708 'internal_nav' => 'Changes Log',
709 'group' => 'plugin'
710 ];
711 }
712
713 // Bot Statistics
714 $menu_items['bot_statistics'] = [
715 'title' => 'Bot Statistics',
716 'slug_suffix' => '-bot-statistics',
717 'callback' => 'create_admin_bot_statistics_page',
718 'internal_nav' => 'Bot Statistics',
719 'group' => 'plugin'
720 ];
721
722 // Report Issue page (check access control)
723 if (Metasync_Access_Control::user_can_access('hide_report_issue')) {
724 $menu_items['report_issue'] = [
725 'title' => 'Report Issue',
726 'slug_suffix' => '-report-issue',
727 'callback' => 'create_admin_report_issue_page',
728 'internal_nav' => 'Report Issue',
729 'group' => 'plugin'
730 ];
731 }
732
733 return $menu_items;
734 }
735
736 /**
737 * Register all WordPress admin menu / submenu pages.
738 *
739 * @param Metasync_Admin $admin The admin instance whose callbacks WordPress will invoke.
740 */
741 public function add_plugin_settings_page($admin)
742 {
743 if (!Metasync::current_user_has_plugin_access()) {
744 return;
745 }
746
747 $data = Metasync::get_option('general');
748 $plugin_name = Metasync::get_effective_plugin_name();
749 $menu_name = $plugin_name;
750 $menu_title = $plugin_name;
751 // Sanitize so a URL-shaped legacy value still yields a valid WP menu slug
752 $menu_slug = !isset($data['white_label_plugin_menu_slug']) || $data['white_label_plugin_menu_slug'] == "" ? Metasync_Admin::$page_slug : sanitize_title($data['white_label_plugin_menu_slug']);
753 $menu_slug = $menu_slug === '' ? Metasync_Admin::$page_slug : $menu_slug;
754 $menu_icon = !isset($data['white_label_plugin_menu_icon']) || $data['white_label_plugin_menu_icon'] == "" ? 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzQiIHZpZXdCb3g9IjAgMCAzMiAzNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzE4NzlfMTcyNzcpIj4KPHBhdGggZD0iTTI5LjAyMTUgMi4xNjc2NkwzMC4wMTAxIDIuMTIyNDFMMjkuMTYyNyA4LjcyNzA1TDI5LjExNTcgOC44MTc0M0w1LjI5NTA0IDMzLjI0NTJMNC43MzAxMiAzMy4yOTA1TDQuMDcxMDQgMzMuOTY5MUgxLjk1MjYyTDIuMjM1MDggMzIuMjk1M0wyLjA5Mzg0IDMyLjM0MDZMMi4xODggMzEuNjYySDEuNjIzMDhDMS41NzYgMzEuNjYyIDEuNDgxODUgMzEuNjYyIDEuNDgxODUgMzEuNjE2N0MxLjQzNDc4IDMxLjU3MTYgMS4zODc3IDMxLjQ4MSAxLjM4NzcgMzEuNDM1OEwyLjUxNzU0IDI1LjEwMjdDMi41MTc1NCAyNS4wNTc1IDIuNTY0NiAyNS4wMTIyIDIuNTY0NiAyNS4wMTIyTDI2LjE5NjkgMC42Mjk2MDdDMjYuMjQ0IDAuNTg0MzY2IDI2LjI5MTEgMC41MzkxMjUgMjYuMzM4MSAwLjUzOTEyNUgyOC4zNjI1QzI4LjQwOTUgMC40OTM4ODQgMjguNDU2NiAwLjUzOTEyNSAyOC41MDM3IDAuNTg0MzY2QzI4LjU1MDcgMC42Mjk2MDcgMjguNTk3OSAwLjcyMDA3MSAyOC41OTc5IDAuNzY1MzExTDI4LjUwMzcgMS4zNTMzOUgyOS4xMTU3TDI5LjAyMTUgMi4xNjc2NlpNMS45MDU1NCAzMS4yMDk2SDIuMjgyMTZMMy4yMjM2OCAyNS43ODEySDMuMjcwNzZMNi44MDE1IDIyLjI1MjhMNi44NDg1MSAyMi4yOTgxTDIwLjIxODMgOC40NTU1N0wyNi45MDMxIDEuMzUzMzlIMjguMDMyOUwyOC4wNzk5IDAuOTkxNDk5SDI2LjQ3OTNMMi45ODgzIDI1LjIzODRMMS45MDU1NCAzMS4yMDk2Wk0yOC42OTE5IDguNTQ1OTVMMjkuNDQ1MiAyLjYyMDAxSDI4Ljk3NDVMMjguMzE1MyA3Ljc3Njk2TDI4LjI2ODMgNy44MjIyNEwyOC4xMjcxIDcuOTU3ODlMMjcuOTM4NyA5LjMxNTExTDI4LjY5MTkgOC41NDU5NVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0zMS41NTQ0IDE4LjE5MzdDMzEuNzM0OSAxOC42NjYyIDMxLjgwMjggMTkuMjA2NCAzMS42ODk4IDE5Ljc2OUwyOS42NTgyIDMyLjEwMTNIMjkuMDkzOUwyOS4wMjYyIDMyLjQzODhIMjUuOTMzNkwyNi4wNjkxIDMxLjYwNjJIMjYuMDAxM0wyNi4wNjkxIDMxLjI2ODZIMjUuNzk4MUMyNS43NTMgMzEuMjY4NiAyNS43MzA1IDMxLjI2ODYgMjUuNzA3OSAzMS4yNDYxQzI1LjY4NTIgMzEuMjIzNyAyNS42ODUyIDMxLjE3ODYgMjUuNjg1MiAzMS4xNTZMMjYuMzYyNCAyNy4zOThIMTguNDE2N0wxNy40MjM0IDMyLjA3ODdIMTYuODM2NUwxNi43Njg3IDMyLjQxNjNIMTMuNjk4OEwxMy45MDE5IDMxLjU4MzZIMTMuODM0M0wxMy45MDE5IDMxLjI2ODZIMTMuNjMxQzEzLjYwODQgMzEuMjY4NiAxMy41NjMzIDMxLjI2ODYgMTMuNTQwOCAzMS4yNDYxQzEzLjU0MDggMzEuMjAxMSAxMy41MTgyIDMxLjE3ODYgMTMuNTQwOCAzMS4xMzM2TDE2LjM2MjQgMTguOTEzOEMxNi40OTc5IDE4LjM1MTIgMTYuNzQ2MiAxNy44MzM2IDE3LjEyOTkgMTcuMzM4NUMxNy40OTEyIDE2Ljg2NiAxNy45NDI2IDE2LjQ4MzUgMTguNDYxOCAxNi4yMTM0QzE4Ljk4MSAxNS45MjA3IDE5LjUwMDIgMTUuNzg1OCAyMC4wNDE5IDE1Ljc4NThIMjguNTk3MkMyOS4xMzkgMTUuNzg1OCAyOS42MTMxIDE1LjkyMDcgMzAuMDE5MyAxNi4yMTM0QzMwLjM1NzkgMTYuNDYwOSAzMC42Mjg5IDE2Ljc5ODUgMzAuODA5NSAxNy4xODFDMzEuMTI1NSAxNy40NTExIDMxLjM5NjMgMTcuNzg4NiAzMS41NTQ0IDE4LjE5MzdaTTI3LjQ5MTIgMjMuMzY5N0wyOC4wNTU1IDIwLjI2NDFDMjguMDMyOSAyMC4yNDE1IDI4LjAzMjkgMjAuMjE5MSAyOC4wMTA0IDIwLjIxOTFIMjcuODk3NUwyNy4zNzgzIDIzLjA5OTZDMjcuMzc4MyAyMy4xNDQ3IDI3LjMxMDYgMjMuMTg5NiAyNy4yNjU1IDIzLjE4OTZIMTkuMzQyMUwxOS4yOTcgMjMuMzY5N0gyNy40OTEyWk0xOS4wOTM5IDIzLjE4OTZIMTguODQ1NUwxOC44MjI5IDIzLjM2OTdIMTkuMDcxM0wxOS4wOTM5IDIzLjE4OTZaTTE5LjUwMDIgMjAuMjY0MUwxOC45MTMzIDIyLjk2NDZIMTkuMTYxNUwxOS43NDg0IDIwLjIxOTFIMTkuNTQ1M0MxOS41NDUzIDIwLjIxOTEgMTkuNTIyNyAyMC4yNDE1IDE5LjUwMDIgMjAuMjY0MVpNMjcuNjcxOCAyMC4yMTkxSDE5Ljk3NDJMMTkuMzg3MiAyMi45NjQ2SDI3LjE3NTFMMjcuNjcxOCAyMC4yMTkxWk0xMy43ODkgMzEuMDQzNkgxMy45NDdMMTUuMDk4NCAyNi4xMTUySDE1LjE2NkwxNi43MjM2IDE5LjI5NjRDMTYuODU5MSAxOC43NTYzIDE3LjEwNzMgMTguMjM4OCAxNy40Njg1IDE3Ljc2NjFDMTcuODI5NiAxNy4zMTYxIDE4LjI4MTIgMTYuOTMzNCAxOC43Nzc4IDE2LjY2MzNDMTkuMDI2MSAxNi41Mjg0IDE5LjI3NDUgMTYuNDM4NCAxOS41MjI3IDE2LjM0ODNMMTkuNTAwMiAxNi4zMDM0QzE5Ljc0ODQgMTYuMjEzNCAyMC4wMTkzIDE2LjE5MDggMjAuMjkwMyAxNi4xOTA4SDI4Ljg2ODJDMjkuMjI5NCAxNi4xOTA4IDI5LjU5MDUgMTYuMjU4MyAyOS44ODQgMTYuMzkzNEMyOS41MjI5IDE2LjEyMzMgMjkuMDkzOSAxNi4wMTA4IDI4LjU5NzIgMTYuMDEwOEgyMC4wNDE5QzE5LjU0NTMgMTYuMDEwOCAxOS4wNDg2IDE2LjE0NTkgMTguNTc0NyAxNi4zOTM0QzE4LjA3OCAxNi42NjMzIDE3LjY0OTEgMTcuMDIzNSAxNy4zMTA0IDE3LjQ5NkMxNi45NDkzIDE3Ljk0NjIgMTYuNzAxIDE4LjQ0MTIgMTYuNTg4MSAxOC45NTg5TDEzLjc4OSAzMS4wNDM2Wk0xNy4yMjAyIDMxLjg1MzdMMTguMTkxIDI3LjM5OEgxNy45NDI2TDE3LjAxNzEgMzEuNTgzNkgxNi45NDkzTDE2LjkwNDIgMzEuODUzN0gxNy4yMjAyWk0yNS45MzM2IDMxLjA0MzZIMjYuMTE0MkwyNi43Njg5IDI3LjM5OEgyNi41ODgzTDI1LjkzMzYgMzEuMDQzNlpNMzEuNDg2NyAxOS43NDY0QzMxLjU3NjkgMTkuMjA2NCAzMS41MDkzIDE4LjcxMTMgMzEuMzI4NyAxOC4yNjEyQzMxLjI4MzYgMTguMTQ4OCAzMS4yMTU4IDE4LjAzNjIgMzEuMTQ4MSAxNy45MjM2QzMxLjI4MzYgMTguMzUxMiAzMS4zMjg3IDE4LjgwMTQgMzEuMjM4MyAxOS4yOTY0TDMwLjA4NzIgMjYuMTE1MkwzMC4xNTQ4IDI2LjEzNzdMMjkuMjUxOSAzMS42MDYySDI5LjE4NDNMMjkuMTM5IDMxLjg3NjNIMjkuNDU1TDMxLjQ4NjcgMTkuNzQ2NFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xNy42ODQ3IDIuNDI3MTdIMTcuNjYxOEMxNy44NjgyIDIuODk5MTIgMTcuOTE0IDMuNDM4NDkgMTcuODIyMiA0LjAwMDMzTDE3LjU5MjkgNS4zOTM3SDE3LjA0MjNMMTYuOTczNSA1LjczMDhIMTMuOTY4NEwxNC4xMjkxIDQuODk5MjhIMTQuMDM3M0wxNC4xMDYyIDQuNTg0NjRIMTMuODUzOEMxMy44MDggNC41ODQ2NCAxMy43ODUxIDQuNTYyMTcgMTMuNzYyIDQuNTM5NjlDMTMuNzM5MSA0LjUxNzIzIDEzLjczOTEgNC40OTQ3NSAxMy43MzkxIDQuNDQ5OEg1LjkxNjg2TDUuNTQ5ODcgNi4wOTAzOUgxMy41Nzg1QzE0LjEyOTEgNi4wOTAzOSAxNC42MzM3IDYuMjQ3NzEgMTUuMDQ2NyA2LjUzOTg3QzE1LjM2NzggNi43NDIxMyAxNS41OTcxIDcuMDU2NzcgMTUuODAzNiA3LjM3MTQxQzE1Ljg0OTUgNy40Mzg4OSAxNS44NzI0IDcuNDgzODIgMTUuOTE4NCA3LjU1MTEyQzE2LjIxNjYgNy43OTgzMiAxNi40Njg4IDguMTEyOTkgMTYuNjI5NSA4LjQ5NTE0QzE2LjgzNTkgOC45NjY5OCAxNi45MDQ4IDkuNTA2NDcgMTYuNzkwMSAxMC4wOTA3TDE2LjI4NTMgMTMuMTY5NkMxNi4xOTM1IDEzLjczMTUgMTUuOTQxMyAxNC4yNDg0IDE1LjU3NDMgMTQuNzQyOEMxNS4yMDcyIDE1LjIxNDcgMTQuNzQ4NCAxNS41OTY4IDE0LjIyMDggMTUuODg4OUMxNC4xNTIgMTUuOTExNSAxNC4wNjAyIDE1Ljk1NjQgMTMuOTkxNSAxNS45Nzg4QzEzLjg3NjcgMTYuMDY4OCAxMy43NjIgMTYuMTU4NyAxMy42MjQ1IDE2LjIyNkMxMy4wOTY4IDE2LjQ5NTcgMTIuNTY5MiAxNi42NTMxIDExLjk5NTcgMTYuNjUzMUgyLjYzNjYxQzIuMDg2MDcgMTYuNjUzMSAxLjU4MTQxIDE2LjQ5NTcgMS4xNjg1IDE2LjIyNkMwLjc1NTYxIDE1Ljk1NjQgMC40ODAzMzEgMTUuNTc0MyAwLjMxOTc1IDE1LjEyNDhDMC4xODIxMiAxNC43NjUyIDAuMTU5MTg3IDE0LjQwNTggMC4xODIxMTkgMTQuMDAxMkMwLjE4MjExOSAxMy45NTYxIDAuMTM2MjU0IDEzLjkzMzggMC4xMzYyNTQgMTMuODg4OEMtMC4wMjQzMjYxIDEzLjQxNjggLTAuMDQ3MjU4MSAxMi44Nzc1IDAuMDkwMzcyNSAxMi4zMTU2TDAuMzg4NTgzIDExLjAxMjJDMC4zODg1ODMgMTAuOTY3MyAwLjQ1NzM5OCAxMC45MjIzIDAuNTAzMjY0IDEwLjkyMjNIMy41NTQxN0MzLjU3NzExIDEwLjkyMjMgMy42MDAwNCAxMC45NDQ3IDMuNjIyOTkgMTAuOTY3M0MzLjY0NTkyIDEwLjk4OTYgMy42Njg4NyAxMS4wMzQ2IDMuNjQ1OTIgMTEuMDU3MUwzLjYwMDA0IDExLjMyNjlIMy44OTgyNUwzLjgwNjUgMTEuNzMxNEg0LjMxMTE2TDQuMjE5MzkgMTIuMjAzMkgxMi4yNzFDMTIuMjcxIDEyLjIwMzIgMTIuMjcxIDEyLjIwMzIgMTIuMjkzOSAxMi4xODA4QzEyLjI5MzkgMTIuMTU4MyAxMi4zMTY4IDEyLjE1ODMgMTIuMzE2OCAxMi4xNTgzTDEyLjU5MjEgMTAuNTYyN0gzLjk5MDAyQzMuNDM5NDggMTAuNTYyNyAyLjk1Nzc1IDEwLjQyNzggMi41Njc3OSAxMC4xMzU2QzIuMTU0ODggOS44NjU5IDEuODc5NjIgOS41MDY0NyAxLjcxOTA0IDkuMDM0NDZDMS42MDQzNCA4LjY5NzQxIDEuNTU4NDYgOC4zMzc4MSAxLjYwNDM0IDcuOTMzMjhDMS42MDQzNCA3Ljg4ODM1IDEuNTU4NDYgNy44NjU4IDEuNTU4NDYgNy44MjA4N0MxLjM5NzkgNy4zNDg4NiAxLjM3NDk3IDYuODA5NTYgMS41MTI2IDYuMjI1MjNMMi4yNDY2NSAzLjE0NjM0QzIuMzg0MjggMi41ODQ0OSAyLjYzNjYxIDIuMDY3NTggMy4wMDM2MyAxLjU3MzE2QzMuMzcwNjYgMS4xMDEyMiAzLjgyOTQ0IDAuNzE5MTUyIDQuMzU3MDQgMC40NDk0NzZDNC44ODQ2MyAwLjE1NzMxOSA1LjQxMjI0IDAgNS45NjI4MyAwSDE0LjcwMjZDMTUuMjMwMSAwIDE1LjcxMTggMC4xNTczMTkgMTYuMTI0OCAwLjQ0OTQ3NkMxNi40NDU5IDAuNjc0MjA2IDE2LjY3NTMgMC45NjYzOCAxNi44NTg4IDEuMzAzNDhDMTYuOTA0OCAxLjM0ODQzIDE2LjkyNzcgMS4zOTMzNyAxNi45NzM1IDEuNDYwOEMxNy4yNzE4IDEuNzMwNDggMTcuNTI0IDIuMDIyNjQgMTcuNjg0NyAyLjQyNzE3Wk0zLjY0NTkyIDEyLjU4NTRWMTIuNjA3OEgzLjg5ODI1TDMuOTIxMiAxMi40MjhIMy42Njg4N0wzLjY0NTkyIDEyLjU2MjhDMy42MjI5OSAxMi41NjI4IDMuNjIyOTkgMTIuNTg1NCAzLjY0NTkyIDEyLjU4NTRaTTQuOTk5MzMgNi41NjIzNUg1LjIwNTc4TDUuMjc0NjEgNi4zMTUxNEg0Ljk1MzQ1TDQuOTA3NTggNi40NDk5N0M0LjkwNzU4IDYuNDk0OTIgNC45MDc1OCA2LjUxNzQgNC45MzA1MSA2LjUzOTg3QzQuOTUzNDUgNi41NjIzNSA0Ljk3NjQgNi41NjIzNSA0Ljk5OTMzIDYuNTYyMzVaTTEuNzQxOTcgNi4yNzAxN0MxLjY1MDIzIDYuNjc0NyAxLjY1MDIzIDcuMDU2NzcgMS43MTkwNCA3LjM5Mzc5TDEuNzQxOTcgNy4yMzY1NUMxLjc2NDkyIDcuMDExODEgMS43ODc4NiA2LjgwOTU2IDEuODMzNzQgNi41ODQ4MUwyLjU0NDg0IDMuNTI4MzlDMi42ODI0OSAyLjk2NjU0IDIuOTM0ODIgMi40NDk2NSAzLjMwMTg1IDEuOTc3NjlDMy4zNDc3MSAxLjkzMjc0IDMuMzcwNjYgMS44ODc4IDMuMzkzNTkgMS44NDI4NUwzLjQ2MjQxIDEuOTEwMjhDMy44MDY1IDEuNDgzMjcgNC4xOTY0NiAxLjE0NjE2IDQuNjc4MTkgMC44OTg5NTNDNS4xODI4NCAwLjYyOTI2IDUuNjg3NSAwLjQ5NDQyMyA2LjIxNTA2IDAuNDk0NDIzSDE0Ljk3NzlDMTUuMTg0MyAwLjQ5NDQyMyAxNS4zOTA3IDAuNTE2OTA0IDE1LjU5NzEgMC41NjE4NUwxNS42MiAwLjQ5NDQyM0MxNS43MzQ5IDAuNTE2OTA0IDE1Ljg0OTUgMC41NjE4NSAxNS45NjQyIDAuNjA2Nzk2QzE1LjU5NzEgMC4zNTk1ODUgMTUuMTg0MyAwLjI0NzIxMSAxNC43MDI2IDAuMjQ3MjExSDUuOTYyODNDNS40NTgxMiAwLjI0NzIxMSA0Ljk1MzQ1IDAuMzU5NTg0IDQuNDcxNzQgMC42MjkyNkMzLjk2NzA3IDAuODk4OTUzIDMuNTU0MTcgMS4yNTg1NCAzLjE4NzE1IDEuNzA4MDFDMi44NDMwNSAyLjE3OTk1IDIuNTkwNzIgMi42NzQzOCAyLjQ3NjAzIDMuMTkxMjhMMS43NDE5NyA2LjI3MDE3Wk0xMi4yNzEgMTIuNDI4SDQuMTczNTNMNC4xMjc2NSAxMi42MDc4SDcuNzI5MVYxMi42NzUySDEyLjU2OTJDMTIuNTkyMSAxMi42NzUyIDEyLjYxNSAxMi42NTI3IDEyLjY2MSAxMi42MzAzQzEyLjY4MzkgMTIuNjA3OCAxMi43MDY4IDEyLjU2MjggMTIuNzA2OCAxMi41NDAzTDEyLjg0NDUgMTEuODIxMkwxMy4wNTEgMTAuNjc1QzEzLjA3MzkgMTAuNjMgMTMuMDUxIDEwLjYwNzcgMTMuMDI4MSAxMC41ODUxQzEzLjAwNSAxMC41NjI3IDEyLjk4MjEgMTAuNTYyNyAxMi45NTkyIDEwLjU2MjdIMTIuODQ0NUwxMi41MjMzIDEyLjIwMzJDMTIuNTIzMyAxMi4yNDgyIDEyLjUwMDQgMTIuMzE1NiAxMi40MzE3IDEyLjM2MDZDMTIuMzg1NyAxMi40MDU1IDEyLjMxNjggMTIuNDI4IDEyLjI3MSAxMi40MjhaTTQuMDM1OSAxMS45NTZIMy43NjA2MkwzLjcxNDc0IDEyLjIwMzJIMy45NjcwN0w0LjAzNTkgMTEuOTU2Wk0wLjI5NjgxNyAxMi4zNjA2QzAuMjA1MDY5IDEyLjc2NTEgMC4yMDUwNyAxMy4xNjk2IDAuMjczODg2IDEzLjUyOTJMMC4zMTk3NSAxMy4zMDQ0QzAuMzE5NzUgMTMuMTAyMSAwLjM0MjcgMTIuODk5OSAwLjQxMTUxNiAxMi42NzUyTDAuNzA5NzI3IDExLjMyNjlIMy4zNzA2NkwzLjM5MzU5IDExLjE0N0gwLjU5NTAyOUwwLjI5NjgxNyAxMi4zNjA2Wk0xNi41ODM1IDEwLjA0NThDMTYuNjc1MyA5LjUwNjQ3IDE2LjYwNjYgOS4wMTE5MSAxNi40MjMgOC41ODVDMTYuNDAwMSA4LjU0MDA3IDE2LjM3NzEgOC40OTUxNCAxNi4zNTQyIDguNDUwMjFDMTYuNDAwMSA4LjY1MjQ4IDE2LjQyMyA4Ljg1NDc0IDE2LjQyMyA5LjA3OTM5QzE2LjQyMyA5LjI1OTI3IDE2LjQyMyA5LjQzODk5IDE2LjM3NzEgOS42MTg3TDE1Ljg3MjQgMTIuNjk3NkMxNS44MjY2IDEyLjkyMjQgMTUuNzU3OCAxMy4xMjQ3IDE1LjY4ODkgMTMuMzI3TDE1LjY0MzEgMTMuNTk2N0MxNS41NTE0IDE0LjA2ODUgMTUuMzQ0OSAxNC41MTggMTUuMDQ2NyAxNC45NDUxQzE1LjE2MTQgMTQuODMyNyAxNS4yOTkgMTQuNzIwMyAxNS4zOTA3IDE0LjYwOEMxNS43MzQ5IDE0LjE1ODQgMTUuOTY0MiAxMy42NDE2IDE2LjA1NiAxMy4xMjQ3TDE2LjU4MzUgMTAuMDQ1OFpNMTMuNTc4NSA2LjMxNTE0SDUuNTAzOTlMNS40NTgxMiA2LjU2MjM1SDEwLjA5MTdWNi40OTQ5MkgxMy44NTM4QzE0LjI0MzcgNi40OTQ5MiAxNC41ODc5IDYuNTYyMzUgMTQuOTA5IDYuNzE5NjVDMTQuNTE5IDYuNDQ5OTcgMTQuMDgzMyA2LjMxNTE0IDEzLjU3ODUgNi4zMTUxNFpNNS4zMjA0NyA2LjA5MDM5TDUuNjg3NSA0LjQ0OThINS40NTgxMkM1LjQzNTE3IDQuNDQ5OCA1LjQxMjI0IDQuNDcyMjggNS4zODkyOSA0LjQ5NDc1QzUuMzQzNDIgNC41MTcyMyA1LjM0MzQyIDQuNTYyMTggNS4zMjA0NyA0LjU4NDY0TDQuOTk5MzMgNi4wOTAzOUg1LjMyMDQ3Wk0xNy41OTI5IDMuOTc3ODZDMTcuNjg0NyAzLjQzODQ5IDE3LjYzODcgMi45NDQwNyAxNy40NTUyIDIuNDk0NTlDMTcuNDU1MiAyLjQ3MjExIDE3LjQwOTQgMi40NDk2NSAxNy40MDk0IDIuNDA0NjhDMTcuNDc4MyAyLjc2NDI3IDE3LjUwMTEgMy4xNDYzNCAxNy40MzIzIDMuNTUwODVMMTcuMjAzIDQuODk5MjhIMTcuMTM0MUwxNy4wODgzIDUuMTY4OTdIMTcuNDA5NEwxNy41OTI5IDMuOTc3ODZaIiBmaWxsPSJ3aGl0ZSIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzE4NzlfMTcyNzciPgo8cmVjdCB3aWR0aD0iMzEuNzQ0OSIgaGVpZ2h0PSIzNCIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K' : $data['white_label_plugin_menu_icon'];
755
756 // Use 'read' capability since actual access is controlled by current_user_has_plugin_access() check above
757 $menu_capability = 'read';
758
759 // Separator just before the plugin entry, after all other plugins (Yoast etc. ~99)
760 add_action('admin_menu', function() {
761 global $menu;
762 $menu['100'] = array('', 'read', 'separator-metasync-before', '', 'wp-menu-separator');
763 }, 5);
764
765 // Main menu page at position 100.1 — bottom of the menu after all standard items.
766 // The top-level callback is the floor: when Settings is reachable it renders
767 // Settings; when Settings is hidden it falls back to the first reachable
768 // keep-list page so the menu never leads to a blank page.
769 $top_level_callback = self::resolve_top_level_callback($admin);
770 add_menu_page(
771 $menu_name,
772 $menu_title,
773 $menu_capability,
774 $menu_slug,
775 $top_level_callback,
776 $menu_icon,
777 '100.1'
778 );
779
780 // Check connection status for submenu availability
781 $general_options = Metasync::get_option('general');
782 $has_api_key = !empty($general_options['searchatlas_api_key']);
783 $has_uuid = !empty($general_options['otto_pixel_uuid']);
784 $is_fully_connected = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general_options);
785
786 $seo_controls = Metasync::get_option('seo_controls');
787
788 // ── Connect slug ──────────────────────────────────────────────────
789 $connect_slug = $menu_slug . '-connect';
790
791 // Local helper: register a submenu page under the plugin parent slug
792 // when the item keeps its WordPress admin sidebar row, or under an
793 // empty parent slug (hidden from the sidebar but still reachable)
794 // otherwise. The Settings page (general) is registered against the
795 // bare $menu_slug so it doubles as the parent menu callback.
796 $register_submenu = function($item_key, $page_title, $menu_title, $capability, $menu_page_slug, $callback) use ($menu_slug) {
797 $parent = self::item_keeps_wp_sidebar_row($item_key) ? $menu_slug : '';
798 add_submenu_page($parent, $page_title, $menu_title, $capability, $menu_page_slug, $callback);
799 };
800
801 // Dashboard (check access control + "Hide Dashboard" general setting)
802 if (Metasync_Access_Control::user_can_access('hide_dashboard') && !self::is_dashboard_hidden_by_framework()) {
803 $register_submenu('dashboard', 'Dashboard', 'Dashboard', $menu_capability, $menu_slug . '-dashboard', array($admin, 'create_admin_dashboard_iframe'));
804 }
805
806 // Indexation Control
807 if (Metasync_Access_Control::user_can_access('hide_indexation_control')) {
808 $register_submenu('seo_controls', 'Indexation Control', 'Indexation Control', $menu_capability, $menu_slug . '-seo-controls', array($admin, 'create_admin_seo_controls_page'));
809 }
810
811 // 404 Monitor
812 $register_submenu('monitor_404', '404 Monitor', '404 Monitor', $menu_capability, $menu_slug . '-404-monitor', array($admin, 'create_admin_404_monitor_page'));
813
814 // Redirections
815 if (Metasync_Access_Control::user_can_access('hide_redirections')) {
816 $register_submenu('redirections', 'Redirections', 'Redirections', $menu_capability, $menu_slug . '-redirections', array($admin, 'create_admin_redirections_page'));
817 }
818
819 // XML Sitemap
820 if (Metasync_Access_Control::user_can_access('hide_xml_sitemap')) {
821 $register_submenu('xml_sitemap', 'XML Sitemap', 'XML Sitemap', $menu_capability, $menu_slug . '-xml-sitemap', array($admin, 'create_admin_xml_sitemap_page'));
822 }
823
824 // Robots.txt
825 if (Metasync_Access_Control::user_can_access('hide_robots')) {
826 $register_submenu('robots_txt', 'Robots.txt', 'Robots.txt', $menu_capability, $menu_slug . '-robots-txt', array($admin, 'create_admin_robots_txt_page'));
827 }
828
829 // Site Verification (in-page sidebar only in short mode)
830 $register_submenu('site_verification', 'Site Verification', 'Site Verification', $menu_capability, $menu_slug . '-search-engine-verify', array($admin, 'create_admin_search_engine_verification_page'));
831
832 // Instant Indexing (conditional)
833 if ($seo_controls['enable_googleinstantindex'] ?? false) {
834 $register_submenu('instant_index', 'Instant Indexing', 'Instant Indexing', $menu_capability, $menu_slug . '-instant-index', array($admin, 'create_admin_google_instant_index_page'));
835 }
836
837 // Google Console (conditional)
838 if ($general_options['enable_google_console'] ?? false) {
839 $register_submenu('google_console', 'Google Console', 'Google Console', $menu_capability, $menu_slug . '-google-console', array($admin, 'create_admin_google_console_page'));
840 }
841
842 // Bing Console (conditional)
843 if ($seo_controls['enable_binginstantindex'] ?? false) {
844 $register_submenu('bing_console', 'Bing Console', 'Bing Console', $menu_capability, $menu_slug . '-bing-console', array($admin, 'create_admin_bing_console_page'));
845 }
846
847 // Schema Markup
848 $register_submenu('schema_markup', 'Schema Markup', 'Schema Markup', $menu_capability, $menu_slug . '-schema-markup', array($admin, 'create_admin_schema_markup_page'));
849
850 // Import SEO Data
851 if (Metasync_Access_Control::user_can_access('hide_import_seo')) {
852 $register_submenu('import_seo', 'Import SEO Data', 'Import SEO Data', $menu_capability, $menu_slug . '-import-external', array($admin, 'render_import_external_data_page'));
853 }
854
855 // Settings — registered against the bare $menu_slug (parent menu floor).
856 // It always keeps its sidebar row so the plugin always has at least
857 // one working entry point (see is_settings_reachable()).
858 if (Metasync_Access_Control::user_can_access('hide_settings')) {
859 $register_submenu('general', 'Settings', 'Settings', $menu_capability, $menu_slug, array($admin, 'create_admin_settings_page'));
860 }
861
862 // Local Business (in-page sidebar only in short mode)
863 $register_submenu('local_business', 'Local Business', 'Local Business', $menu_capability, $menu_slug . '-local-business', array($admin, 'create_admin_local_business_page'));
864
865 // Breadcrumbs (in-page sidebar only in short mode)
866 $register_submenu('breadcrumbs', 'Breadcrumbs', 'Breadcrumbs', $menu_capability, $menu_slug . '-breadcrumbs', array($admin, 'create_admin_breadcrumbs_page'));
867
868 // Code Snippets (in-page sidebar only in short mode)
869 $register_submenu('code_snippets', 'Code Snippets', 'Code Snippets', $menu_capability, $menu_slug . '-code-snippets', array($admin, 'create_admin_code_snippets_page'));
870
871 // Code Minification (in-page sidebar only in short mode)
872 $register_submenu('code_minification', 'Code Minification', 'Code Minification', $menu_capability, $menu_slug . '-code-minification', array($admin, 'create_admin_code_minification_page'));
873
874 // Media Optimization (in-page sidebar only in short mode)
875 $register_submenu('media_optimization', 'Media Optimization', 'Media Optimization', $menu_capability, $menu_slug . '-media-optimization', array($admin, 'create_admin_media_optimization_page'));
876
877 // Custom Pages
878 if (Metasync_Access_Control::user_can_access('hide_custom_pages')) {
879 $register_submenu('custom_pages', 'Custom Pages', 'Custom Pages', $menu_capability, $menu_slug . '-custom-pages', array($admin, 'create_admin_custom_pages_page'));
880 }
881
882 // Bot Statistics (in-page sidebar only in short mode)
883 $register_submenu('bot_statistics', 'Bot Statistics', 'Bot Statistics', $menu_capability, $menu_slug . '-bot-statistics', array($admin, 'create_admin_bot_statistics_page'));
884
885 // SEO Health dashboard
886 $register_submenu('seo_health', 'SEO Health', 'SEO Health', $menu_capability, $menu_slug . '-seo-health', array($admin, 'create_admin_seo_health_page'));
887
888 // Compatibility
889 if (Metasync_Access_Control::user_can_access('hide_compatibility')) {
890 $register_submenu('compatibility', 'Compatibility', 'Compatibility', $menu_capability, $menu_slug . '-compatibility', array($admin, 'create_admin_compatibility_page'));
891 }
892
893 // Changes Log
894 if (Metasync_Access_Control::user_can_access('hide_sync_log')) {
895 $register_submenu('sync_log', 'Changes Log', 'Changes Log', $menu_capability, $menu_slug . '-sync-log', array($admin, 'create_admin_sync_log_page'));
896 }
897
898 // Report Issue
899 if (Metasync_Access_Control::user_can_access('hide_report_issue')) {
900 $register_submenu('report_issue', 'Report Issue', 'Report Issue', $menu_capability, $menu_slug . '-report-issue', array($admin, 'create_admin_report_issue_page'));
901 }
902
903 // ── Connect CTA (shown when not authenticated) ────────────────────
904 // Kept as a sidebar row so an unconnected site always has a visible
905 // entry point alongside the top-level menu item.
906 if (!$is_fully_connected) {
907 $connect_label = sprintf('Connect to %s', Metasync::get_effective_plugin_name());
908 add_submenu_page($menu_slug, $connect_label, $connect_label, $menu_capability, $connect_slug, array($admin, 'create_admin_settings_page'));
909 }
910
911 // ── Hidden pages (no sidebar entry needed) ────────────────────────
912 add_submenu_page('', 'Setup Wizard', 'Setup Wizard', $menu_capability, $menu_slug . '-setup-wizard', array($admin->setup_wizard, 'render_wizard_page'));
913
914
915 // Rename auto-generated first submenu from plugin name to "Settings" and reorder
916 add_action('admin_menu', function() use ($menu_slug) {
917 global $submenu;
918 if (!isset($submenu[$menu_slug])) {
919 return;
920 }
921 // Remove the auto-duplicate of the main menu item (same slug as parent)
922 foreach ($submenu[$menu_slug] as $key => $item) {
923 if ($item[2] === $menu_slug) {
924 unset($submenu[$menu_slug][$key]);
925 break;
926 }
927 }
928 }, 999);
929
930 }
931
932 /**
933 * Render the grouped navigation menu with Dashboard + SEO/Plugin dropdowns.
934 */
935 public function render_navigation_menu($current_page = null)
936 {
937 $page_slug = Metasync_Admin::$page_slug;
938 $available_menu_items = $this->get_available_menu_items();
939
940 $menu_icons = [
941 'general' => 'admin-settings',
942 'dashboard' => 'dashboard',
943 'compatibility' => 'admin-tools',
944 'sync_log' => 'list-view',
945 'seo_controls' => 'search',
946 'instant_index' => 'performance',
947 'google_console' => 'chart-area',
948 'bing_console' => 'chart-bar',
949 'redirections' => 'undo',
950 'robots_txt' => 'shield',
951 'xml_sitemap' => 'networking',
952 'schema_markup' => 'tag',
953 'site_verification'=> 'yes-alt',
954 'import_seo' => 'download',
955 'custom_pages' => 'admin-page',
956 'code_minification'=> 'media-code',
957 'bot_statistics' => 'visibility',
958 'breadcrumbs' => 'menu',
959 'monitor_404' => 'warning',
960 'local_business' => 'building',
961 'code_snippets' => 'editor-code',
962 'report_issue' => 'sos',
963 'error_log' => 'warning',
964 'seo_health' => 'heart'
965
966 ];
967
968 $seo_items = [];
969 $plugin_items = [];
970
971 foreach ($available_menu_items as $key => $menu_item) {
972 $group = $menu_item['group'] ?? 'plugin';
973 if ($group === 'seo') {
974 $seo_items[$key] = $menu_item;
975 } else {
976 $plugin_items[$key] = $menu_item;
977 }
978 }
979 ?>
980 <!-- Plugin Navigation Menu with Dashboard + Grouped Dropdowns -->
981 <div class="metasync-nav-wrapper">
982 <div class="metasync-nav-tabs metasync-nav-grouped">
983 <?php
984 // Dashboard tab (standalone)
985 if (isset($seo_items['dashboard'])) {
986 $is_active = ($current_page === 'dashboard');
987 $icon = $menu_icons['dashboard'];
988 $page_url = '?page=' . $page_slug . $seo_items['dashboard']['slug_suffix'];
989 ?>
990 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-tab <?php echo $is_active ? 'active' : ''; ?>">
991 <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
992 <span class="tab-text"><?php echo esc_html($seo_items['dashboard']['title']); ?></span>
993 </a>
994 <?php
995 }
996 ?>
997
998 <!-- SEO Features Dropdown (excluding Dashboard) -->
999 <div class="metasync-nav-dropdown">
1000 <?php
1001 $has_active_seo = false;
1002 foreach ($seo_items as $key => $menu_item) {
1003 if ($key !== 'dashboard' && $current_page === $key) {
1004 $has_active_seo = true;
1005 break;
1006 }
1007 }
1008 ?>
1009 <button type="button" class="metasync-nav-dropdown-btn <?php echo $has_active_seo ? 'active' : ''; ?>" aria-haspopup="true" aria-expanded="false">
1010 <span class="tab-icon"><span class="dashicons dashicons-search"></span></span>
1011 <span class="tab-text">SEO</span>
1012 <span class="dropdown-arrow"></span>
1013 </button>
1014 <div class="metasync-nav-dropdown-menu">
1015 <?php
1016 foreach ($seo_items as $key => $menu_item) {
1017 if ($key === 'dashboard') {
1018 continue;
1019 }
1020
1021 $is_active = ($current_page === $key);
1022 $icon = $menu_icons[$key] ?? 'admin-generic';
1023 $page_url = '?page=' . $page_slug . $menu_item['slug_suffix'];
1024 ?>
1025 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-dropdown-item <?php echo $is_active ? 'active' : ''; ?>">
1026 <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
1027 <span class="tab-text"><?php echo esc_html($menu_item['title']); ?></span>
1028 </a>
1029 <?php
1030 }
1031 ?>
1032 </div>
1033 </div>
1034
1035 <!-- Plugin Dropdown -->
1036 <div class="metasync-nav-dropdown">
1037 <?php
1038 $has_active_plugin = false;
1039 foreach ($plugin_items as $key => $menu_item) {
1040 if ($key !== 'report_issue' && $current_page === $key) {
1041 $has_active_plugin = true;
1042 break;
1043 }
1044 }
1045 ?>
1046 <button type="button" class="metasync-nav-dropdown-btn <?php echo $has_active_plugin ? 'active' : ''; ?>" aria-haspopup="true" aria-expanded="false">
1047 <span class="tab-icon"><span class="dashicons dashicons-admin-settings"></span></span>
1048 <span class="tab-text">Plugin</span>
1049 <span class="dropdown-arrow"></span>
1050 </button>
1051 <div class="metasync-nav-dropdown-menu">
1052 <?php
1053 foreach ($plugin_items as $key => $menu_item) {
1054 if ($key === 'report_issue') {
1055 continue;
1056 }
1057
1058 $is_active = ($current_page === $key);
1059 $icon = $menu_icons[$key] ?? 'admin-generic';
1060 $page_url = '?page=' . $page_slug . $menu_item['slug_suffix'];
1061 ?>
1062 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-dropdown-item <?php echo $is_active ? 'active' : ''; ?>">
1063 <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
1064 <span class="tab-text"><?php echo esc_html($menu_item['title']); ?></span>
1065 </a>
1066 <?php
1067 }
1068 ?>
1069 </div>
1070 </div>
1071
1072 <!-- Right side - Report Issue and Settings dropdown -->
1073 <div class="metasync-nav-right">
1074 <?php
1075 if (isset($available_menu_items['report_issue'])) {
1076 $is_active = ($current_page === 'report_issue');
1077 $page_url = '?page=' . $page_slug . $available_menu_items['report_issue']['slug_suffix'];
1078 ?>
1079 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-tab <?php echo $is_active ? 'active' : ''; ?>">
1080 <span class="tab-icon"><span class="dashicons dashicons-sos"></span></span>
1081 <span class="tab-text">Report Issue</span>
1082 </a>
1083 <?php } ?>
1084 <div class="metasync-simple-dropdown">
1085 <button type="button" class="metasync-settings-btn" id="metasync-settings-btn" onclick="toggleSettingsMenuPortal(event)" aria-expanded="false">
1086 <span class="tab-icon"><span class="dashicons dashicons-admin-settings"></span></span>
1087 <span class="tab-text">Settings</span>
1088 <span class="dropdown-arrow"></span>
1089 </button>
1090 </div>
1091 </div>
1092 </div>
1093 </div>
1094
1095 <script>
1096 // Handle navigation dropdowns using portal pattern for reliable positioning
1097 (function() {
1098 var dropdowns = document.querySelectorAll('.metasync-nav-dropdown');
1099 var activePortal = null;
1100 var activeButton = null;
1101 var activeDropdown = null;
1102 var scrollHandler = null;
1103 var resizeHandler = null;
1104
1105 function positionPortalMenu(button, portalMenu) {
1106 var rect = button.getBoundingClientRect();
1107 var menuRect = portalMenu.getBoundingClientRect();
1108 var viewportWidth = window.innerWidth;
1109 var viewportHeight = window.innerHeight;
1110
1111 var left = rect.left;
1112 if (left + menuRect.width > viewportWidth - 10) {
1113 left = Math.max(10, viewportWidth - menuRect.width - 10);
1114 }
1115
1116 var top = rect.bottom + 8;
1117 if (top + menuRect.height > viewportHeight - 10 && rect.top > menuRect.height + 10) {
1118 top = rect.top - menuRect.height - 8;
1119 }
1120
1121 portalMenu.style.position = 'fixed';
1122 portalMenu.style.top = top + 'px';
1123 portalMenu.style.left = left + 'px';
1124 portalMenu.style.zIndex = '999999999';
1125 }
1126
1127 function closeActivePortal() {
1128 if (activePortal && activePortal.parentNode) {
1129 activePortal.parentNode.removeChild(activePortal);
1130 }
1131 if (activeButton) {
1132 activeButton.setAttribute('aria-expanded', 'false');
1133 }
1134 if (activeDropdown) {
1135 activeDropdown.classList.remove('active');
1136 }
1137 if (scrollHandler) {
1138 window.removeEventListener('scroll', scrollHandler, true);
1139 scrollHandler = null;
1140 }
1141 if (resizeHandler) {
1142 window.removeEventListener('resize', resizeHandler);
1143 resizeHandler = null;
1144 }
1145 activePortal = null;
1146 activeButton = null;
1147 activeDropdown = null;
1148 }
1149
1150 function openAsPortal(dropdown, button, menu) {
1151 closeActivePortal();
1152
1153 var portalMenu = menu.cloneNode(true);
1154 portalMenu.id = 'metasync-nav-portal-menu';
1155 portalMenu.style.opacity = '1';
1156 portalMenu.style.visibility = 'visible';
1157 portalMenu.style.transform = 'none';
1158
1159 document.body.appendChild(portalMenu);
1160
1161 positionPortalMenu(button, portalMenu);
1162
1163 activePortal = portalMenu;
1164 activeButton = button;
1165 activeDropdown = dropdown;
1166 dropdown.classList.add('active');
1167 button.setAttribute('aria-expanded', 'true');
1168
1169 scrollHandler = function() {
1170 if (activePortal && activeButton) {
1171 positionPortalMenu(activeButton, activePortal);
1172 }
1173 };
1174 resizeHandler = scrollHandler;
1175
1176 window.addEventListener('scroll', scrollHandler, true);
1177 window.addEventListener('resize', resizeHandler);
1178
1179 portalMenu.addEventListener('click', function(e) {
1180 var link = e.target.closest('a');
1181 if (link) {
1182 setTimeout(closeActivePortal, 0);
1183 }
1184 });
1185 }
1186
1187 dropdowns.forEach(function(dropdown) {
1188 var button = dropdown.querySelector('.metasync-nav-dropdown-btn');
1189 var menu = dropdown.querySelector('.metasync-nav-dropdown-menu');
1190
1191 if (!button || !menu) return;
1192
1193 button.addEventListener('click', function(e) {
1194 e.preventDefault();
1195 e.stopPropagation();
1196
1197 var isExpanded = button.getAttribute('aria-expanded') === 'true';
1198
1199 if (isExpanded) {
1200 closeActivePortal();
1201 } else {
1202 openAsPortal(dropdown, button, menu);
1203 }
1204 });
1205 });
1206
1207 document.addEventListener('click', function(e) {
1208 if (activePortal && activeButton) {
1209 if (!activePortal.contains(e.target) && !activeButton.contains(e.target)) {
1210 closeActivePortal();
1211 }
1212 }
1213 });
1214
1215 document.addEventListener('keydown', function(e) {
1216 if (e.key === 'Escape' && activePortal) {
1217 closeActivePortal();
1218 if (activeButton) {
1219 activeButton.focus();
1220 }
1221 }
1222 });
1223 })();
1224
1225 // Portal-style dropdown that bypasses stacking contexts
1226 function toggleSettingsMenuPortal(event) {
1227 event.preventDefault();
1228 event.stopPropagation();
1229
1230 var button = event.currentTarget;
1231 var existingMenu = document.getElementById('metasync-portal-menu');
1232
1233 if (existingMenu) {
1234 existingMenu.remove();
1235 button.classList.remove('active');
1236 button.setAttribute('aria-expanded', 'false');
1237 return;
1238 }
1239
1240 var menu = document.createElement('div');
1241 menu.id = 'metasync-portal-menu';
1242 menu.className = 'metasync-portal-menu';
1243
1244 var currentUrl = window.location.href;
1245 var isGeneralActive = currentUrl.indexOf('tab=general') > -1 || currentUrl.indexOf('tab=') === -1;
1246 var isAdvancedActive = currentUrl.indexOf('tab=advanced') > -1;
1247 var isWhitelabelActive = currentUrl.indexOf('tab=whitelabel') > -1;
1248
1249 menu.textContent = '';
1250
1251 var hideAdvanced = <?php
1252 echo !Metasync_Access_Control::user_can_access('hide_advanced') ? 'true' : 'false';
1253 ?>;
1254 var showGeneral = <?php
1255 echo Metasync_Access_Control::user_can_access('hide_settings') ? 'true' : 'false';
1256 ?>;
1257
1258 if (showGeneral) {
1259 const generalLink = document.createElement('a');
1260 generalLink.href = '?page=<?php echo esc_js( $page_slug ); ?>&tab=general';
1261 generalLink.className = 'metasync-portal-item' + (isGeneralActive ? ' active' : '');
1262 generalLink.textContent = 'General';
1263 menu.appendChild(generalLink);
1264 }
1265
1266 const whitelabelLink = document.createElement('a');
1267 whitelabelLink.href = '?page=<?php echo esc_js( $page_slug ); ?>&tab=whitelabel';
1268 whitelabelLink.className = 'metasync-portal-item' + (isWhitelabelActive ? ' active' : '');
1269 whitelabelLink.textContent = 'White label';
1270 menu.appendChild(whitelabelLink);
1271
1272 if (!hideAdvanced) {
1273 const advancedLink = document.createElement('a');
1274 advancedLink.href = '?page=<?php echo esc_js( $page_slug ); ?>&tab=advanced';
1275 advancedLink.className = 'metasync-portal-item' + (isAdvancedActive ? ' active' : '');
1276 advancedLink.textContent = 'Advanced';
1277 menu.appendChild(advancedLink);
1278 }
1279
1280
1281 var rect = button.getBoundingClientRect();
1282 menu.style.position = 'fixed';
1283 menu.style.top = (rect.bottom + 8) + 'px';
1284 menu.style.right = (window.innerWidth - rect.right) + 'px';
1285 menu.style.zIndex = '999999999';
1286
1287 document.body.appendChild(menu);
1288
1289 button.classList.add('active');
1290 button.setAttribute('aria-expanded', 'true');
1291 }
1292
1293 document.addEventListener('click', function(event) {
1294 var button = document.getElementById('metasync-settings-btn');
1295 var menu = document.getElementById('metasync-portal-menu');
1296
1297 if (menu && button && !button.contains(event.target) && !menu.contains(event.target)) {
1298 menu.remove();
1299 button.classList.remove('active');
1300 button.setAttribute('aria-expanded', 'false');
1301 }
1302 });
1303 </script>
1304 <?php
1305 }
1306
1307 /**
1308 * Render the plugin header with logo, theme toggle, and connection badge.
1309 */
1310 public function render_plugin_header($page_title = null)
1311 {
1312 $general_settings = Metasync::get_option('general');
1313
1314 $effective_plugin_name = Metasync::get_effective_plugin_name();
1315 $display_title = $page_title ?: $effective_plugin_name;
1316 $logo = $this->resolve_logo_data();
1317
1318 $searchatlas_api_key = isset($general_settings['searchatlas_api_key']) ? $general_settings['searchatlas_api_key'] : '';
1319 $otto_pixel_uuid = isset($general_settings['otto_pixel_uuid']) ? $general_settings['otto_pixel_uuid'] : '';
1320
1321 $is_integrated = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general_settings);
1322
1323 $current_theme = get_option('metasync_theme', 'dark');
1324 ?>
1325
1326 <!-- Plugin Header with Logo -->
1327 <div class="metasync-header" data-current-theme="<?php echo esc_attr($current_theme); ?>">
1328 <div class="metasync-header-left">
1329 <?php if ($logo['show_logo'] && $logo['use_dual']): ?>
1330 <div class="metasync-logo-container">
1331 <img src="<?php echo esc_url($logo['light_url']); ?>" alt="Logo" class="metasync-logo metasync-logo-light" />
1332 <img src="<?php echo esc_url($logo['dark_url']); ?>" alt="Logo" class="metasync-logo metasync-logo-dark" />
1333 </div>
1334 <?php elseif ($logo['show_logo'] && !empty($logo['url'])): ?>
1335 <div class="metasync-logo-container">
1336 <img src="<?php echo esc_url($logo['url']); ?>" alt="Logo" class="metasync-logo<?php echo !empty($logo['is_default']) ? ' metasync-logo-default' : ''; ?>" />
1337 </div>
1338 <?php endif; ?>
1339 </div>
1340
1341 <div class="metasync-header-right">
1342 <!-- Theme Toggle -->
1343 <div class="metasync-theme-toggle" role="group" aria-label="Theme Selector">
1344 <button class="metasync-theme-option <?php echo ($current_theme === 'light') ? 'active' : ''; ?>" data-theme="light" aria-label="Light Theme" type="button">
1345 <span class="metasync-theme-icon">&#9728;</span>
1346 <span class="theme-label">Light</span>
1347 </button>
1348 <button class="metasync-theme-option <?php echo ($current_theme === 'dark') ? 'active' : ''; ?>" data-theme="dark" aria-label="Dark Theme" type="button">
1349 <span class="metasync-theme-icon">&#9790;</span>
1350 <span class="theme-label">Dark</span>
1351 </button>
1352 </div>
1353
1354 <!-- Integration Status -->
1355 <?php
1356 if ($is_integrated && !empty($otto_pixel_uuid)) {
1357 $status_class_header = 'integrated';
1358 $status_title_header = 'Synced - Heartbeat API connectivity verified';
1359 $status_text_header = 'Synced';
1360 } elseif ($is_integrated && empty($otto_pixel_uuid)) {
1361 $status_class_header = 'warning';
1362 $status_title_header = 'Connected but OTTO UUID is missing — deploys will not work. Please reconnect.';
1363 $status_text_header = 'Warning';
1364 } else {
1365 $status_class_header = 'not-integrated';
1366 $status_title_header = 'Not Synced - Heartbeat API not responding or unreachable';
1367 $status_text_header = 'Not Synced';
1368 }
1369 ?>
1370 <div class="metasync-integration-status <?php echo $status_class_header; ?>"
1371 title="<?php echo esc_attr($status_title_header); ?>">
1372 <span class="status-indicator"></span>
1373 <span class="status-text"><?php echo esc_html($status_text_header); ?></span>
1374 </div>
1375 </div>
1376 </div>
1377
1378 <!-- Page Title Below Header -->
1379 <div class="metasync-page-title">
1380 <h1><?php echo esc_html($display_title); ?></h1>
1381 </div>
1382
1383 <?php
1384 }
1385
1386 // ------------------------------------------------------------------
1387 // Admin bar status indicator
1388 // ------------------------------------------------------------------
1389
1390 /**
1391 * Output inline CSS (and optional JS) for the admin-bar status node.
1392 */
1393 public function metasync_admin_bar_style()
1394 {
1395 if (!is_admin_bar_showing()) {
1396 return;
1397 }
1398
1399 # For backward compatibility, constant takes precedence.
1400 if (defined('METASYNC_SHOW_ADMIN_BAR_STATUS') && !METASYNC_SHOW_ADMIN_BAR_STATUS) {
1401 return;
1402 }
1403
1404
1405 # Check if admin bar status is enabled via setting
1406 $general_settings = Metasync::get_option('general');
1407 $show_admin_bar = $general_settings['show_admin_bar_status'] ?? true;
1408 if (!$show_admin_bar) {
1409 return;
1410 }
1411 ?>
1412 <style type="text/css">
1413 #wp-admin-bar-searchatlas-status .ab-item {
1414 font-weight: 500 !important;
1415 transition: all 0.2s ease !important;
1416 }
1417
1418 #wp-admin-bar-searchatlas-status:hover .ab-item {
1419 background-color: rgba(255, 255, 255, 0.1) !important;
1420 }
1421
1422 #wp-admin-bar-searchatlas-status.searchatlas-synced .ab-item {
1423 color: #46b450 !important; /* WordPress green for text */
1424 }
1425
1426 #wp-admin-bar-searchatlas-status.searchatlas-not-synced .ab-item {
1427 color: #dc3232 !important; /* WordPress red for text */
1428 }
1429
1430 #wp-admin-bar-searchatlas-status.searchatlas-warning .ab-item {
1431 color: #ffb900 !important; /* WordPress yellow for warning */
1432 }
1433
1434 /* Ensure emojis maintain their natural colors */
1435 #wp-admin-bar-searchatlas-status .ab-item {
1436 filter: none !important;
1437 }
1438
1439 /* Make status visible but subtle */
1440 #wp-admin-bar-searchatlas-status .ab-item {
1441 opacity: 0.9;
1442 }
1443
1444 #wp-admin-bar-searchatlas-status:hover .ab-item {
1445 opacity: 1;
1446 }
1447 </style>
1448
1449 <?php
1450 $page_slug = Metasync_Admin::$page_slug;
1451 $is_plugin_page = (
1452 (isset($_GET['page']) && strpos($_GET['page'], $page_slug) !== false) ||
1453 (isset($_GET['page']) && strpos($_GET['page'], 'searchatlas') !== false)
1454 );
1455 if ($is_plugin_page):
1456 ?>
1457 <script type="text/javascript">
1458 // Pass PHP variables to JavaScript
1459 window.MetasyncConfig = {
1460 pluginName: '<?php echo esc_js(Metasync::get_effective_plugin_name()); ?>',
1461 ottoName: '<?php echo esc_js(Metasync::get_whitelabel_otto_name()); ?>'
1462 };
1463
1464 jQuery(document).ready(function($) {
1465
1466 // Function to sync admin bar status
1467 function syncAdminBarStatus() {
1468 var pluginPageStatus = $('.metasync-integration-status .status-text').text();
1469 var adminBarItem = $('#wp-admin-bar-searchatlas-status .ab-item');
1470 var adminBarContainer = $('#wp-admin-bar-searchatlas-status');
1471 var pluginName = window.MetasyncConfig.pluginName;
1472
1473 if (pluginPageStatus && adminBarItem.length) {
1474 var allClasses = 'searchatlas-synced searchatlas-not-synced searchatlas-warning';
1475
1476 // Helper to update emoji in admin bar
1477 function updateAdminBarEmoji(targetEmoji, targetSvgCode) {
1478 var emojiImg = adminBarItem.find('img.emoji');
1479 if (emojiImg.length > 0) {
1480 emojiImg.attr('alt', targetEmoji);
1481 var currentSrc = emojiImg.attr('src');
1482 var updatedSrc = currentSrc.replace(/1f7e2\.svg|1f534\.svg|1f7e1\.svg/, targetSvgCode + '.svg');
1483 emojiImg.attr('src', updatedSrc);
1484 } else {
1485 var newHtml = adminBarItem.html().replace(/🟢|🔴|🟡/, targetEmoji);
1486 if (!newHtml.includes(targetEmoji) && newHtml.includes(pluginName)) {
1487 newHtml = newHtml.replace(pluginName, pluginName + ' ' + targetEmoji);
1488 }
1489 adminBarItem.html(newHtml);
1490 }
1491 }
1492
1493 if (pluginPageStatus.includes('Synced') && !pluginPageStatus.includes('Not Synced')) {
1494 // Update admin bar to synced (GREEN)
1495 updateAdminBarEmoji('🟢', '1f7e2');
1496 adminBarContainer.removeClass(allClasses).addClass('searchatlas-synced');
1497 var syncTitle = pluginName + ' - Synced (Heartbeat API connectivity verified)';
1498 adminBarContainer.attr('title', syncTitle);
1499 adminBarItem.attr('title', syncTitle);
1500
1501 } else if (pluginPageStatus.includes('Warning')) {
1502 // Update admin bar to warning (YELLOW)
1503 updateAdminBarEmoji('🟡', '1f7e1');
1504 adminBarContainer.removeClass(allClasses).addClass('searchatlas-warning');
1505 var warnTitle = pluginName + ' - Connected but OTTO UUID is missing — deploys will not work. Please reconnect.';
1506 adminBarContainer.attr('title', warnTitle);
1507 adminBarItem.attr('title', warnTitle);
1508
1509 } else if (pluginPageStatus.includes('Not Synced')) {
1510 // Update admin bar to not synced (RED)
1511 updateAdminBarEmoji('🔴', '1f534');
1512 adminBarContainer.removeClass(allClasses).addClass('searchatlas-not-synced');
1513 var notSyncTitle = pluginName + ' - Not Synced (Heartbeat API not responding or unreachable)';
1514 adminBarContainer.attr('title', notSyncTitle);
1515 adminBarItem.attr('title', notSyncTitle);
1516 }
1517 }
1518 }
1519
1520 // Sync when tabs are switched (for General/Advanced tabs)
1521 $(document).on('click', 'a[href*="tab="]', function() {
1522 setTimeout(syncAdminBarStatus, 200);
1523 });
1524
1525 // Also check every 5 seconds to keep it in sync
1526 setInterval(syncAdminBarStatus, 5000);
1527 });
1528 </script>
1529 <?php endif; ?>
1530 <?php
1531 }
1532
1533 /**
1534 * Add Search Atlas status indicator to WordPress admin bar.
1535 */
1536 public function add_searchatlas_admin_bar_status($wp_admin_bar)
1537 {
1538 if (!Metasync::current_user_has_plugin_access()) {
1539 return;
1540 }
1541
1542 # For backward compatibility, constant takes precedence.
1543 if (defined('METASYNC_SHOW_ADMIN_BAR_STATUS') && !METASYNC_SHOW_ADMIN_BAR_STATUS) {
1544 return;
1545 }
1546
1547 # Check if admin bar status is disabled via setting
1548 $general_settings = Metasync::get_option('general');
1549 if (!is_array($general_settings)) {
1550 $general_settings = [];
1551 }
1552 $show_admin_bar = $general_settings['show_admin_bar_status'] ?? true;
1553 if (!$show_admin_bar) {
1554 return;
1555 }
1556
1557 if (!is_admin() && !apply_filters('metasync_show_admin_bar_status_frontend', false)) {
1558 return;
1559 }
1560
1561 $node = get_transient(self::CACHE_KEY);
1562
1563 if ($node === false) {
1564 $is_synced = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general_settings);
1565
1566 $plugin_name = Metasync::get_effective_plugin_name();
1567
1568 if ($is_synced) {
1569 $otto_uuid = $general_settings['otto_pixel_uuid'] ?? '';
1570 if (!empty($otto_uuid)) {
1571 $status_emoji = '🟢'; // Green circle for synced
1572 $title = $plugin_name . ' - Synced (Heartbeat API connectivity verified)';
1573 $status_class = 'searchatlas-synced';
1574 } else {
1575 $status_emoji = '🟡'; // Yellow circle for warning
1576 $title = $plugin_name . ' - Connected but OTTO UUID is missing — deploys will not work. Please reconnect.';
1577 $status_class = 'searchatlas-warning';
1578 }
1579 } else {
1580 $status_emoji = '🔴'; // Red circle for not synced
1581 $title = $plugin_name . ' - Not Synced (Heartbeat API not responding or unreachable)';
1582 $status_class = 'searchatlas-not-synced';
1583 }
1584
1585 $admin_bar_title = $plugin_name . ' ' . $status_emoji;
1586
1587 $node = array(
1588 'title' => $admin_bar_title,
1589 'href' => admin_url('admin.php?page=' . Metasync_Admin::$page_slug),
1590 'meta_title' => $title,
1591 'meta_class' => $status_class,
1592 );
1593
1594 set_transient(self::CACHE_KEY, $node, apply_filters('metasync_admin_bar_status_cache_ttl', 5 * MINUTE_IN_SECONDS));
1595 }
1596
1597 $wp_admin_bar->add_node(array(
1598 'id' => 'searchatlas-status',
1599 'title' => $node['title'],
1600 'href' => $node['href'],
1601 'meta' => array(
1602 'title' => $node['meta_title'],
1603 'class' => $node['meta_class'],
1604 ),
1605 ));
1606 }
1607
1608 /**
1609 * Invalidate the admin bar status cache so the next page load recomputes status.
1610 */
1611 public static function invalidate_admin_bar_status_cache()
1612 {
1613 delete_transient(self::CACHE_KEY);
1614 }
1615
1616 // ------------------------------------------------------------------
1617 // Yoast-style 3-column layout helpers
1618 // ------------------------------------------------------------------
1619
1620 /**
1621 * Open the 3-column page layout.
1622 * Call this at the start of every admin page callback instead of
1623 * render_plugin_header() + render_navigation_menu().
1624 * Close with render_layout_close().
1625 *
1626 * @param string $page_title Human-readable page title.
1627 * @param string $current_page Key matching get_available_menu_items() (e.g. 'general').
1628 * @param string $description Optional subtitle shown below the title.
1629 */
1630 public function render_layout_open($page_title = '', $current_page = '', $description = '')
1631 {
1632 $theme = esc_attr(get_option('metasync_theme', 'dark'));
1633 $plugin_name = Metasync::get_effective_plugin_name();
1634 $general = Metasync::get_option('general') ?? [];
1635 $is_connected = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general);
1636 // badge reflects confirmed heartbeat health (connected / stale /
1637 // disconnected), not raw API-key presence.
1638 $badge = Metasync_Heartbeat_Manager::instance()->get_connection_badge($general);
1639 $logo = $this->resolve_logo_data();
1640
1641 $current_theme = get_option('metasync_theme', 'dark');
1642 ?>
1643 <div class="wrap metasync-dashboard-wrap" data-theme="<?php echo $theme; ?>">
1644
1645 <?php
1646 // Inject whitelabel color palette overrides
1647 $wl_settings = Metasync::get_whitelabel_settings();
1648 $wl_palette = isset($wl_settings['color_palette']) && is_array($wl_settings['color_palette']) ? $wl_settings['color_palette'] : array();
1649 if (!empty($wl_palette)) {
1650 echo '<style id="metasync-whitelabel-palette">';
1651 foreach (array('dark', 'light') as $t) {
1652 if (!empty($wl_palette[$t]) && is_array($wl_palette[$t])) {
1653 $selector = ($t === 'dark') ? ':root, [data-theme="dark"]' : '[data-theme="light"]';
1654 $vars = '';
1655 foreach ($wl_palette[$t] as $var_name => $color) {
1656 // Skip gradient partial keys — handled below
1657 if (strpos($var_name, 'gradient-') !== false) continue;
1658 if (preg_match('/^dashboard-[a-z-]+$/', $var_name) && preg_match('/^#[0-9a-fA-F]{3,8}$/', $color)) {
1659 $vars .= '--' . esc_attr($var_name) . ':' . esc_attr($color) . ';';
1660 }
1661 }
1662 // Build gradient overrides from paired from/to colors
1663 $gradient_pairs = array(
1664 'dashboard-gradient-primary' => array('dashboard-gradient-primary-from', 'dashboard-gradient-primary-to'),
1665 'dashboard-gradient-accent' => array('dashboard-gradient-accent-from', 'dashboard-gradient-accent-to'),
1666 );
1667 foreach ($gradient_pairs as $grad_var => $pair) {
1668 $from = isset($wl_palette[$t][$pair[0]]) ? $wl_palette[$t][$pair[0]] : '';
1669 $to = isset($wl_palette[$t][$pair[1]]) ? $wl_palette[$t][$pair[1]] : '';
1670 if ($from && $to && preg_match('/^#[0-9a-fA-F]{3,8}$/', $from) && preg_match('/^#[0-9a-fA-F]{3,8}$/', $to)) {
1671 $vars .= '--' . esc_attr($grad_var) . ':linear-gradient(135deg,' . esc_attr($from) . ' 0%,' . esc_attr($to) . ' 100%);';
1672 }
1673 }
1674 if ($vars) {
1675 echo $selector . '{' . $vars . '}';
1676 }
1677 }
1678 }
1679 echo '</style>';
1680 }
1681 ?>
1682
1683 <!-- Compact top header -->
1684 <div class="metasync-header-compact">
1685 <div style="display:flex;align-items:center;gap:10px;">
1686 <?php if ($logo['show_logo'] && $logo['use_dual']): ?>
1687 <img src="<?php echo esc_url($logo['light_url']); ?>" alt="<?php echo esc_attr($plugin_name); ?>" class="metasync-logo metasync-logo-light" style="height:28px;width:auto;">
1688 <img src="<?php echo esc_url($logo['dark_url']); ?>" alt="<?php echo esc_attr($plugin_name); ?>" class="metasync-logo metasync-logo-dark" style="height:28px;width:auto;">
1689 <?php elseif ($logo['show_logo'] && $logo['url']): ?>
1690 <img src="<?php echo esc_url($logo['url']); ?>" alt="<?php echo esc_attr($plugin_name); ?>" class="metasync-logo<?php echo !empty($logo['is_default']) ? ' metasync-logo-default' : ''; ?>" style="height:28px;width:auto;">
1691 <?php else: ?>
1692 <strong style="font-size:15px;color:var(--dashboard-text-primary);"><?php echo esc_html($plugin_name); ?></strong>
1693 <?php endif; ?>
1694 </div>
1695 <div class="metasync-header-compact-right">
1696 <div class="metasync-status <?php echo esc_attr($badge['class']); ?>">
1697 <span class="status-dot"></span>
1698 <span class="status-text"><?php echo esc_html($badge['text']); ?></span>
1699 </div>
1700 <button type="button" class="metasync-theme-toggle" onclick="toggleMetasyncTheme()" title="Toggle theme">
1701 <span class="theme-icon-light">&#9728;</span>
1702 <span class="theme-icon-dark">&#9790;</span>
1703 </button>
1704 </div>
1705 </div>
1706
1707 <!-- 3-column layout -->
1708 <div class="metasync-layout">
1709
1710 <!-- Left sidenav -->
1711 <aside class="metasync-layout-nav">
1712 <?php $this->render_sidenav($current_page); ?>
1713 </aside>
1714
1715 <!-- Main content -->
1716 <main class="metasync-layout-main">
1717 <?php
1718 // Anchor for WP core's notice relocation (common.js). Without it, core
1719 // moves admin notices after the first .wrap h1 only on DOM-ready, after
1720 // the server has already painted them at the top of #wpbody-content —
1721 // causing a flash above the header and layout shift. Providing
1722 // an explicit .wp-header-end inside this (FOUC-hidden) wrap makes core
1723 // relocate notices here deterministically, so they fade in with the page.
1724 ?>
1725 <hr class="wp-header-end">
1726 <?php if ($page_title || $description): ?>
1727 <div class="metasync-page-header">
1728 <?php if ($page_title): ?>
1729 <h1><?php echo esc_html($page_title); ?></h1>
1730 <?php endif; ?>
1731 <?php if ($description): ?>
1732 <p><?php echo esc_html($description); ?></p>
1733 <?php endif; ?>
1734 </div>
1735 <?php endif; ?>
1736 <?php
1737 // Note: render_layout_close() closes </main>, renders promo sidebar, closes </div>.metasync-layout and </div>.wrap
1738 }
1739
1740 /**
1741 * Close the 3-column layout opened by render_layout_open().
1742 *
1743 * @param bool $show_promo Whether to render the right promo sidebar. Default true.
1744 * Pass false for full-width pages (e.g. dashboard iframe).
1745 */
1746 public function render_layout_close($show_promo = true)
1747 {
1748 ?>
1749 </main><!-- /.metasync-layout-main -->
1750
1751 <?php if ($show_promo):
1752 $general = Metasync::get_option('general') ?? [];
1753 $is_connected = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general);
1754 ?>
1755 <!-- Right promo sidebar -->
1756 <aside class="metasync-layout-promo">
1757 <?php $this->render_promo_sidebar($is_connected); ?>
1758 </aside>
1759 <?php endif; ?>
1760
1761 </div><!-- /.metasync-layout -->
1762 </div><!-- /.metasync-dashboard-wrap -->
1763 <?php
1764 }
1765
1766 /**
1767 * Render the left sticky sidenav with grouped items.
1768 *
1769 * @param string $current_page Active page key.
1770 */
1771 public function render_sidenav($current_page = '')
1772 {
1773 $page_slug = Metasync_Admin::$page_slug;
1774 $menu_items = $this->get_available_menu_items();
1775
1776 // Dashicons names (without 'dashicons-' prefix) — monochrome, color set by CSS
1777 $icons = [
1778 'dashboard' => 'dashboard',
1779 'seo_controls' => 'search',
1780 'monitor_404' => 'warning',
1781 'redirections' => 'undo',
1782 'xml_sitemap' => 'networking',
1783 'robots_txt' => 'shield',
1784 'site_verification'=> 'yes-alt',
1785 'instant_index' => 'performance',
1786 'google_console' => 'chart-area',
1787 'bing_console' => 'chart-bar',
1788 'import_seo' => 'download',
1789 'general' => 'admin-settings',
1790 'schema_markup' => 'tag',
1791 'local_business' => 'building',
1792 'breadcrumbs' => 'menu',
1793 'code_snippets' => 'editor-code',
1794 'code_minification'=> 'media-code',
1795 'custom_pages' => 'admin-page',
1796 'compatibility' => 'admin-tools',
1797 'sync_log' => 'list-view',
1798 'bot_statistics' => 'visibility',
1799 'report_issue' => 'sos',
1800 'media_optimization'=> 'images-alt2',
1801 'seo_health' => 'heart'
1802 ];
1803
1804 $seo_items = [];
1805 $plugin_items = [];
1806 foreach ($menu_items as $key => $item) {
1807 if (($item['group'] ?? 'plugin') === 'seo') {
1808 $seo_items[$key] = $item;
1809 } else {
1810 $plugin_items[$key] = $item;
1811 }
1812 }
1813
1814 ?>
1815 <nav class="metasync-sidenav">
1816
1817 <!-- SEO Features group -->
1818 <div class="metasync-sidenav-group">
1819 <div class="metasync-sidenav-group-title">SEO Features</div>
1820 <ul>
1821 <?php foreach ($seo_items as $key => $item):
1822 $is_active = ($current_page === $key);
1823 $icon = $icons[$key] ?? 'admin-generic';
1824 $url = esc_url(admin_url('admin.php?page=' . $page_slug . $item['slug_suffix']));
1825 ?>
1826 <li class="<?php echo $is_active ? 'metasync-sidenav-active' : ''; ?>">
1827 <a href="<?php echo $url; ?>">
1828 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
1829 <?php echo esc_html($item['title']); ?>
1830 </a>
1831 </li>
1832 <?php endforeach; ?>
1833 </ul>
1834 </div>
1835
1836 <!-- Plugin group -->
1837 <div class="metasync-sidenav-group">
1838 <div class="metasync-sidenav-group-title">Plugin</div>
1839 <ul>
1840 <?php foreach ($plugin_items as $key => $item):
1841 if ($key === 'report_issue') continue;
1842 $is_active = ($current_page === $key);
1843 $icon = $icons[$key] ?? 'admin-generic';
1844 $url = esc_url(admin_url('admin.php?page=' . $page_slug . $item['slug_suffix']));
1845 ?>
1846 <li class="<?php echo $is_active ? 'metasync-sidenav-active' : ''; ?>">
1847 <a href="<?php echo $url; ?>">
1848 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
1849 <?php echo esc_html($item['title']); ?>
1850 </a>
1851 </li>
1852 <?php if ($key === 'general'): ?>
1853 <li class="<?php echo $current_page === 'advanced_settings' ? 'metasync-sidenav-active' : ''; ?>" style="padding-left: 8px;">
1854 <a href="<?php echo esc_url(admin_url('admin.php?page=' . $page_slug . '&tab=advanced')); ?>">
1855 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-admin-generic"></span></span>
1856 Advanced Settings
1857 </a>
1858 </li>
1859 <li class="<?php echo $current_page === 'whitelabel' ? 'metasync-sidenav-active' : ''; ?>" style="padding-left: 8px;">
1860 <a href="<?php echo esc_url(admin_url('admin.php?page=' . $page_slug . '&tab=whitelabel')); ?>">
1861 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-tag"></span></span>
1862 Whitelabel
1863 </a>
1864 </li>
1865 <?php endif; ?>
1866 <?php endforeach; ?>
1867 </ul>
1868 </div>
1869
1870 <!-- Report Issue at the bottom -->
1871 <?php if (isset($menu_items['report_issue'])): ?>
1872 <div class="metasync-sidenav-group">
1873 <ul>
1874 <li class="<?php echo $current_page === 'report_issue' ? 'metasync-sidenav-active' : ''; ?>">
1875 <a href="<?php echo esc_url(admin_url('admin.php?page=' . $page_slug . $menu_items['report_issue']['slug_suffix'])); ?>">
1876 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-sos"></span></span>
1877 Report Issue
1878 </a>
1879 </li>
1880 </ul>
1881 </div>
1882 <?php endif; ?>
1883
1884 </nav>
1885 <?php
1886 }
1887
1888 /**
1889 * Render the right promotional sidebar.
1890 *
1891 * @param bool $is_connected Whether the plugin is authenticated.
1892 */
1893 public function render_promo_sidebar($is_connected = false)
1894 {
1895 $plugin_name = Metasync::get_effective_plugin_name();
1896 $otto_name = Metasync::get_whitelabel_otto_name();
1897 $settings_url = esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug));
1898 $homepage = Metasync::HOMEPAGE_DOMAIN;
1899 $is_default_brand = ( $plugin_name === 'Search Atlas' );
1900 $whitelabel = Metasync::get_whitelabel_settings();
1901 $custom_links = isset($whitelabel['quick_links']) && is_array($whitelabel['quick_links'])
1902 ? array_filter($whitelabel['quick_links'], function($l) { return !empty($l['url']); })
1903 : [];
1904 ?>
1905
1906 <?php if (!$is_connected): ?>
1907 <!-- Connect CTA card -->
1908 <div class="metasync-promo-card metasync-promo-card--connect">
1909 <div class="metasync-promo-card-header">
1910 <div class="metasync-promo-card-icon">
1911 <span class="dashicons dashicons-admin-links"></span>
1912 </div>
1913 <div>
1914 <h3>Connect <?php echo esc_html($plugin_name); ?></h3>
1915 </div>
1916 </div>
1917 <p class="metasync-promo-tagline">Link your site to <?php echo esc_html($plugin_name); ?> to unlock <?php echo esc_html($otto_name); ?>, keyword data, and automated SEO.</p>
1918 <ul class="metasync-promo-benefits">
1919 <li><span class="promo-check">&#10003;</span> <?php echo esc_html($otto_name); ?> hands-free on-page SEO</li>
1920 <li><span class="promo-check">&#10003;</span> Real-time keyword tracking</li>
1921 <li><span class="promo-check">&#10003;</span> Automated schema markup</li>
1922 <li><span class="promo-check">&#10003;</span> Instant Google indexing</li>
1923 </ul>
1924 <a href="<?php echo $settings_url; ?>" class="metasync-promo-btn metasync-promo-btn--primary">
1925 Connect Now
1926 </a>
1927 </div>
1928 <?php else: ?>
1929 <!-- Connected feature highlights -->
1930 <div class="metasync-promo-card metasync-promo-card--accent">
1931 <div class="metasync-promo-card-header">
1932 <div class="metasync-promo-card-icon">
1933 <span class="dashicons dashicons-performance"></span>
1934 </div>
1935 <div>
1936 <h3><?php echo esc_html($otto_name); ?> Active</h3>
1937 </div>
1938 </div>
1939 <p class="metasync-promo-tagline">Your site is connected and <?php echo esc_html($otto_name); ?> is optimizing pages automatically.</p>
1940 <ul class="metasync-promo-benefits">
1941 <li><span class="promo-check">&#10003;</span> Schema markup auto-applied</li>
1942 <li><span class="promo-check">&#10003;</span> Meta titles &amp; descriptions optimized</li>
1943 <li><span class="promo-check">&#10003;</span> Internal linking suggestions active</li>
1944 </ul>
1945 <?php if ($is_default_brand): ?>
1946 <a href="<?php echo esc_url($homepage); ?>" target="_blank" rel="noopener" class="metasync-promo-btn metasync-promo-btn--outline">
1947 View <?php echo esc_html($plugin_name); ?> Dashboard
1948 </a>
1949 <?php elseif (!empty($whitelabel['domain'])): ?>
1950 <a href="<?php echo esc_url($whitelabel['domain']); ?>" target="_blank" rel="noopener" class="metasync-promo-btn metasync-promo-btn--outline">
1951 View <?php echo esc_html($plugin_name); ?> Dashboard
1952 </a>
1953 <?php endif; ?>
1954 </div>
1955 <?php endif; ?>
1956
1957 <?php
1958 // Quick Links: show default links for default brand, custom links if whitelabeled + provided, hide if whitelabeled + none
1959 $show_quick_links = $is_default_brand || !empty($custom_links);
1960 if ($show_quick_links):
1961 ?>
1962 <!-- Quick links card -->
1963 <div class="metasync-promo-card">
1964 <h3 style="margin:0 0 12px;font-size:13px;font-weight:700;color:var(--dashboard-text-primary);">Quick Links</h3>
1965 <ul class="metasync-promo-links">
1966 <?php if ($is_default_brand): ?>
1967 <li><a href="<?php echo esc_url($homepage . '/blog/'); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-rss"></span> SEO Blog</a></li>
1968 <li><a href="<?php echo esc_url($homepage . '/academy/'); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-welcome-learn-more"></span> SEO Academy</a></li>
1969 <li><a href="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-setup-wizard')); ?>"><span class="dashicons dashicons-admin-customizer"></span> Setup Wizard</a></li>
1970 <li><a href="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-report-issue')); ?>"><span class="dashicons dashicons-sos"></span> Report Issue</a></li>
1971 <?php else: ?>
1972 <?php foreach ($custom_links as $link): ?>
1973 <li><a href="<?php echo esc_url($link['url']); ?>" <?php echo !empty($link['external']) ? 'target="_blank" rel="noopener"' : ''; ?>><span class="dashicons dashicons-admin-links"></span> <?php echo esc_html($link['label'] ?: $link['url']); ?></a></li>
1974 <?php endforeach; ?>
1975 <?php endif; ?>
1976 </ul>
1977 </div>
1978 <?php endif; ?>
1979
1980 <?php
1981 }
1982 }
1983