PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
← All changes | includes/class-metasync-admin-navigation.php +351 -181 2.6.5 → 2.7.2 View file →
@@ -17,9 +17,110 @@
17 17 {
18 18 /** @var self|null */
19 19 private static $instance = null;
20 20
21 + const CACHE_KEY = 'metasync_admin_bar_status';
22 +
21 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 + /**
22 123 * Get singleton instance.
23 124 *
24 125 * @return self
25 126 */
@@ -32,8 +133,24 @@
32 133 }
33 134
34 135 private function __construct() {}
35 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 +
36 153 // ------------------------------------------------------------------
37 154 // Logo resolution helper
38 155 // ------------------------------------------------------------------
39 156
@@ -39,9 +156,9 @@
39 156
40 157 /**
41 158 * Resolve which logo(s) to display based on whitelabel settings.
42 159 *
43 - * @return array{show_logo: bool, use_dual: bool, url: string, light_url: string, dark_url: string}
160 + * @return array{show_logo: bool, use_dual: bool, url: string, light_url: string, dark_url: string, is_default: bool}
44 161 */
45 162 private function resolve_logo_data()
46 163 {
47 164 $wl_settings = Metasync::get_whitelabel_settings();
@@ -53,13 +170,14 @@
53 170 $has_dark = !empty($dark_raw) && filter_var($dark_raw, FILTER_VALIDATE_URL);
54 171 $use_dual = ($has_light && $has_dark && $light_raw !== $dark_raw);
55 172
56 173 $data = [
57 - 'show_logo' => false,
58 - 'use_dual' => false,
59 - 'url' => '',
60 - 'light_url' => '',
61 - 'dark_url' => '',
174 + 'show_logo' => false,
175 + 'use_dual' => false,
176 + 'url' => '',
177 + 'light_url' => '',
178 + 'dark_url' => '',
179 + 'is_default' => false,
62 180 ];
63 181
64 182 if ($use_dual) {
65 183 $data['show_logo'] = true;
@@ -71,10 +189,11 @@
71 189 if ($single) {
72 190 $data['show_logo'] = true;
73 191 $data['url'] = $single;
74 192 } elseif (!$is_whitelabel) {
75 - $data['show_logo'] = true;
76 - $data['url'] = Metasync::HOMEPAGE_DOMAIN . '/wp-content/uploads/2023/12/white.svg';
193 + $data['show_logo'] = true;
194 + $data['url'] = plugin_dir_url(dirname(__FILE__)) . 'assets/images/searchatlas-logo.svg';
195 + $data['is_default'] = true;
77 196 }
78 197 }
79 198
80 199 return $data;
@@ -103,10 +222,11 @@
103 222 $logo = $this->resolve_logo_data();
104 223
105 224 $current_theme = get_option('metasync_theme', 'dark');
106 225 $general_settings = Metasync::get_option('general');
107 - $searchatlas_api_key = isset($general_settings['searchatlas_api_key']) ? $general_settings['searchatlas_api_key'] : '';
108 - $is_integrated = !empty($searchatlas_api_key);
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);
109 229 ?>
110 230 <div class="metasync-header" data-current-theme="<?php echo esc_attr($current_theme); ?>">
111 231 <div class="metasync-header-left">
112 232 <?php if ($logo['show_logo'] && $logo['use_dual']): ?>
@@ -115,16 +235,16 @@
115 235 <img src="<?php echo esc_url($logo['dark_url']); ?>" alt="Logo" class="metasync-logo metasync-logo-dark" />
116 236 </div>
117 237 <?php elseif ($logo['show_logo'] && !empty($logo['url'])): ?>
118 238 <div class="metasync-logo-container">
119 - <img src="<?php echo esc_url($logo['url']); ?>" alt="Logo" class="metasync-logo" />
239 + <img src="<?php echo esc_url($logo['url']); ?>" alt="Logo" class="metasync-logo<?php echo !empty($logo['is_default']) ? ' metasync-logo-default' : ''; ?>" />
120 240 </div>
121 241 <?php endif; ?>
122 242 </div>
123 243 <div class="metasync-header-right">
124 - <div class="metasync-status <?php echo $is_integrated ? 'connected' : 'disconnected'; ?>">
244 + <div class="metasync-status <?php echo esc_attr($badge['class']); ?>">
125 245 <span class="status-dot"></span>
126 - <span class="status-text"><?php echo $is_integrated ? 'Connected' : 'Not Connected'; ?></span>
246 + <span class="status-text"><?php echo esc_html($badge['text']); ?></span>
127 247 </div>
128 248 <button type="button" class="metasync-theme-toggle" onclick="toggleMetasyncTheme()" title="Toggle theme">
129 249 <span class="theme-icon-light">&#9728;</span>
130 250 <span class="theme-icon-dark">&#9790;</span>
@@ -146,9 +266,8 @@
146 266 $menu_items = [];
147 267 $menu_icons = [
148 268 'dashboard' => 'dashboard',
149 269 'seo_controls' => 'search',
150 - 'optimal_settings' => 'superhero-alt',
151 270 'instant_index' => 'performance',
152 271 'google_console' => 'chart-area',
153 272 'compatibility' => 'admin-tools',
154 273 'sync_log' => 'list-view',
@@ -159,9 +278,9 @@
159 278 'report_issue' => 'sos',
160 279 'general' => 'admin-settings',
161 280 ];
162 281
163 - if (empty($whitelabel_settings['hide_dashboard'])) {
282 + if (empty($whitelabel_settings['hide_dashboard']) && !self::is_dashboard_hidden_by_framework()) {
164 283 $menu_items['dashboard'] = ['title' => 'Dashboard', 'slug_suffix' => '-dashboard'];
165 284 }
166 285
167 286 if (empty($whitelabel_settings['hide_indexation_control'])) {
@@ -166,17 +285,9 @@
166 285
167 286 if (empty($whitelabel_settings['hide_indexation_control'])) {
168 287 $menu_items['seo_controls'] = ['title' => 'Indexation Control', 'slug_suffix' => '-seo-controls'];
169 288 }
170 -
171 - if ($general_options['enable_optimal_settings'] ?? false) {
172 - $menu_items['optimal_settings'] = ['title' => 'Optimal Settings', 'slug_suffix' => '-optimal-settings'];
173 - }
174 -
175 - if ($general_options['enable_googleinstantindex'] ?? false) {
176 - $menu_items['instant_index'] = ['title' => 'Instant Indexing', 'slug_suffix' => '-instant-index'];
177 - }
178 -
289 +
179 290 if ($general_options['enable_google_console'] ?? false) {
180 291 $menu_items['google_console'] = ['title' => 'Google Console', 'slug_suffix' => '-google-console'];
181 292 }
182 293
@@ -202,9 +313,9 @@
202 313 <div class="metasync-nav-tabs">
203 314 <div class="metasync-nav-left">
204 315 <?php foreach ($menu_items as $key => $menu_item):
205 316 $is_active = ($current_page === $key);
206 - $icon = $menu_icons[$key] ?? 'admin-generic';
317 + $icon = $menu_icons[$key];
207 318 $page_url = '?page=' . $page_slug . $menu_item['slug_suffix'];
208 319 ?>
209 320 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-tab <?php echo $is_active ? 'active' : ''; ?>">
210 321 <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
@@ -369,10 +480,10 @@
369 480 $menu_items = [];
370 481
371 482 // === SEO FEATURES GROUP ===
372 483
373 - // Dashboard (check access control)
374 - if (Metasync_Access_Control::user_can_access('hide_dashboard')) {
484 + // Dashboard (check access control + "Hide Dashboard" general setting)
485 + if (Metasync_Access_Control::user_can_access('hide_dashboard') && !self::is_dashboard_hidden_by_framework()) {
375 486 $menu_items['dashboard'] = [
376 487 'title' => 'Dashboard',
377 488 'slug_suffix' => '-dashboard',
378 489 'callback' => 'create_admin_dashboard_iframe',
@@ -391,20 +502,8 @@
391 502 'group' => 'seo'
392 503 ];
393 504 }
394 505
395 - // Instant Indexing - setting now stored in seo_controls (moved from Settings to Indexation Control)
396 - $seo_controls = Metasync::get_option('seo_controls');
397 - if ($seo_controls['enable_googleinstantindex'] ?? false) {
398 - $menu_items['instant_index'] = [
399 - 'title' => 'Instant Indexing',
400 - 'slug_suffix' => '-instant-index',
401 - 'callback' => 'create_admin_google_instant_index_page',
402 - 'internal_nav' => 'Instant Indexing',
403 - 'group' => 'seo'
404 - ];
405 - }
406 -
407 506 if ($general_options['enable_google_console'] ?? false) {
408 507 $menu_items['google_console'] = [
409 508 'title' => 'Google Console',
410 509 'slug_suffix' => '-google-console',
@@ -552,8 +651,17 @@
552 651 'internal_nav' => 'Code Minification',
553 652 'group' => 'plugin'
554 653 ];
555 654
655 + // Media Optimization
656 + $menu_items['media_optimization'] = [
657 + 'title' => 'Media Optimization',
658 + 'slug_suffix' => '-media-optimization',
659 + 'callback' => 'create_admin_media_optimization_page',
660 + 'internal_nav' => 'Media Optimization',
661 + 'group' => 'plugin'
662 + ];
663 +
556 664 // Custom HTML Pages (check access control)
557 665 if (Metasync_Access_Control::user_can_access('hide_custom_pages')) {
558 666 $menu_items['custom_pages'] = [
559 667 'title' => 'Custom Pages',
@@ -563,18 +671,8 @@
563 671 'group' => 'plugin'
564 672 ];
565 673 }
566 674
567 - if ($general_options['enable_optimal_settings'] ?? false) {
568 - $menu_items['optimal_settings'] = [
569 - 'title' => 'Optimal Settings',
570 - 'slug_suffix' => '-optimal-settings',
571 - 'callback' => 'create_admin_optimal_settings_page',
572 - 'internal_nav' => 'Optimal Settings',
573 - 'group' => 'plugin'
574 - ];
575 - }
576 -
577 675 // Compatibility page (check access control)
578 676 if (Metasync_Access_Control::user_can_access('hide_compatibility')) {
579 677 $menu_items['compatibility'] = [
580 678 'title' => 'Compatibility',
@@ -633,9 +731,11 @@
633 731 $data = Metasync::get_option('general');
634 732 $plugin_name = Metasync::get_effective_plugin_name();
635 733 $menu_name = $plugin_name;
636 734 $menu_title = $plugin_name;
637 - $menu_slug = !isset($data['white_label_plugin_menu_slug']) || $data['white_label_plugin_menu_slug'] == "" ? Metasync_Admin::$page_slug : $data['white_label_plugin_menu_slug'];
735 + // Sanitize so a URL-shaped legacy value still yields a valid WP menu slug
736 + $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']);
737 + $menu_slug = $menu_slug === '' ? Metasync_Admin::$page_slug : $menu_slug;
638 738 $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'];
639 739
640 740 // Use 'read' capability since actual access is controlled by current_user_has_plugin_access() check above
641 741 $menu_capability = 'read';
@@ -645,15 +745,19 @@
645 745 global $menu;
646 746 $menu['100'] = array('', 'read', 'separator-metasync-before', '', 'wp-menu-separator');
647 747 }, 5);
648 748
649 - // Main menu page at position 100.1 — bottom of the menu after all standard items
749 + // Main menu page at position 100.1 — bottom of the menu after all standard items.
750 + // The top-level callback is the floor: when Settings is reachable it renders
751 + // Settings; when Settings is hidden it falls back to the first reachable
752 + // keep-list page so the menu never leads to a blank page.
753 + $top_level_callback = self::resolve_top_level_callback($admin);
650 754 add_menu_page(
651 755 $menu_name,
652 756 $menu_title,
653 757 $menu_capability,
654 758 $menu_slug,
655 - array($admin, 'create_admin_settings_page'),
759 + $top_level_callback,
656 760 $menu_icon,
657 761 '100.1'
658 762 );
659 763
@@ -667,114 +771,157 @@
667 771
668 772 // ── Connect slug ──────────────────────────────────────────────────
669 773 $connect_slug = $menu_slug . '-connect';
670 774
671 - // Dashboard
672 - if (Metasync_Access_Control::user_can_access('hide_dashboard')) {
673 - add_submenu_page($menu_slug, 'Dashboard', 'Dashboard', $menu_capability, $menu_slug . '-dashboard', array($admin, 'create_admin_dashboard_iframe'));
775 + // Keep a slug-to-title map so hidden pages can still provide WordPress
776 + // with a browser-tab title when they are registered without a parent.
777 + $admin_page_titles = array();
778 +
779 + // Local helper: register a submenu page under the plugin parent slug
780 + // when the item keeps its WordPress admin sidebar row, or under an
781 + // empty parent slug (hidden from the sidebar but still reachable)
782 + // otherwise. The Settings page (general) is registered against the
783 + // bare $menu_slug so it doubles as the parent menu callback.
784 + $register_submenu = function($item_key, $page_title, $menu_title, $capability, $menu_page_slug, $callback) use ($menu_slug, &$admin_page_titles) {
785 + $admin_page_titles[$menu_page_slug] = $page_title;
786 + $parent = self::item_keeps_wp_sidebar_row($item_key) ? $menu_slug : '';
787 + add_submenu_page($parent, $page_title, $menu_title, $capability, $menu_page_slug, $callback);
788 + };
789 +
790 + // Dashboard (check access control + "Hide Dashboard" general setting)
791 + if (Metasync_Access_Control::user_can_access('hide_dashboard') && !self::is_dashboard_hidden_by_framework()) {
792 + $register_submenu('dashboard', 'Dashboard', 'Dashboard', $menu_capability, $menu_slug . '-dashboard', array($admin, 'create_admin_dashboard_iframe'));
674 793 }
675 794
676 795 // Indexation Control
677 796 if (Metasync_Access_Control::user_can_access('hide_indexation_control')) {
678 - add_submenu_page($menu_slug, 'Indexation Control', 'Indexation Control', $menu_capability, $menu_slug . '-seo-controls', array($admin, 'create_admin_seo_controls_page'));
797 + $register_submenu('seo_controls', 'Indexation Control', 'Indexation Control', $menu_capability, $menu_slug . '-seo-controls', array($admin, 'create_admin_seo_controls_page'));
679 798 }
680 799
681 800 // 404 Monitor
682 - add_submenu_page($menu_slug, '404 Monitor', '404 Monitor', $menu_capability, $menu_slug . '-404-monitor', array($admin, 'create_admin_404_monitor_page'));
801 + $register_submenu('monitor_404', '404 Monitor', '404 Monitor', $menu_capability, $menu_slug . '-404-monitor', array($admin, 'create_admin_404_monitor_page'));
683 802
684 803 // Redirections
685 804 if (Metasync_Access_Control::user_can_access('hide_redirections')) {
686 - add_submenu_page($menu_slug, 'Redirections', 'Redirections', $menu_capability, $menu_slug . '-redirections', array($admin, 'create_admin_redirections_page'));
805 + $register_submenu('redirections', 'Redirections', 'Redirections', $menu_capability, $menu_slug . '-redirections', array($admin, 'create_admin_redirections_page'));
687 806 }
688 807
689 808 // XML Sitemap
690 809 if (Metasync_Access_Control::user_can_access('hide_xml_sitemap')) {
691 - add_submenu_page($menu_slug, 'XML Sitemap', 'XML Sitemap', $menu_capability, $menu_slug . '-xml-sitemap', array($admin, 'create_admin_xml_sitemap_page'));
810 + $register_submenu('xml_sitemap', 'XML Sitemap', 'XML Sitemap', $menu_capability, $menu_slug . '-xml-sitemap', array($admin, 'create_admin_xml_sitemap_page'));
692 811 }
693 812
694 813 // Robots.txt
695 814 if (Metasync_Access_Control::user_can_access('hide_robots')) {
696 - add_submenu_page($menu_slug, 'Robots.txt', 'Robots.txt', $menu_capability, $menu_slug . '-robots-txt', array($admin, 'create_admin_robots_txt_page'));
815 + $register_submenu('robots_txt', 'Robots.txt', 'Robots.txt', $menu_capability, $menu_slug . '-robots-txt', array($admin, 'create_admin_robots_txt_page'));
697 816 }
698 817
699 - // Site Verification
700 - add_submenu_page($menu_slug, 'Site Verification', 'Site Verification', $menu_capability, $menu_slug . '-search-engine-verify', array($admin, 'create_admin_search_engine_verification_page'));
818 + // Site Verification (in-page sidebar only in short mode)
819 + $register_submenu('site_verification', 'Site Verification', 'Site Verification', $menu_capability, $menu_slug . '-search-engine-verify', array($admin, 'create_admin_search_engine_verification_page'));
701 820
702 - // Instant Indexing (conditional)
703 - if ($seo_controls['enable_googleinstantindex'] ?? false) {
704 - add_submenu_page($menu_slug, 'Instant Indexing', 'Instant Indexing', $menu_capability, $menu_slug . '-instant-index', array($admin, 'create_admin_google_instant_index_page'));
705 - }
706 -
707 821 // Google Console (conditional)
708 822 if ($general_options['enable_google_console'] ?? false) {
709 - add_submenu_page($menu_slug, 'Google Console', 'Google Console', $menu_capability, $menu_slug . '-google-console', array($admin, 'create_admin_google_console_page'));
823 + $register_submenu('google_console', 'Google Console', 'Google Console', $menu_capability, $menu_slug . '-google-console', array($admin, 'create_admin_google_console_page'));
710 824 }
711 825
712 826 // Bing Console (conditional)
713 827 if ($seo_controls['enable_binginstantindex'] ?? false) {
714 - add_submenu_page($menu_slug, 'Bing Console', 'Bing Console', $menu_capability, $menu_slug . '-bing-console', array($admin, 'create_admin_bing_console_page'));
828 + $register_submenu('bing_console', 'Bing Console', 'Bing Console', $menu_capability, $menu_slug . '-bing-console', array($admin, 'create_admin_bing_console_page'));
715 829 }
716 830
717 831 // Schema Markup
718 - add_submenu_page($menu_slug, 'Schema Markup', 'Schema Markup', $menu_capability, $menu_slug . '-schema-markup', array($admin, 'create_admin_schema_markup_page'));
832 + $register_submenu('schema_markup', 'Schema Markup', 'Schema Markup', $menu_capability, $menu_slug . '-schema-markup', array($admin, 'create_admin_schema_markup_page'));
719 833
720 834 // Import SEO Data
721 835 if (Metasync_Access_Control::user_can_access('hide_import_seo')) {
722 - add_submenu_page($menu_slug, 'Import SEO Data', 'Import SEO Data', $menu_capability, $menu_slug . '-import-external', array($admin, 'render_import_external_data_page'));
836 + $register_submenu('import_seo', 'Import SEO Data', 'Import SEO Data', $menu_capability, $menu_slug . '-import-external', array($admin, 'render_import_external_data_page'));
723 837 }
724 838
725 - // Settings
839 + // Settings — registered against the bare $menu_slug (parent menu floor).
840 + // It always keeps its sidebar row so the plugin always has at least
841 + // one working entry point (see is_settings_reachable()).
726 842 if (Metasync_Access_Control::user_can_access('hide_settings')) {
727 - add_submenu_page($menu_slug, 'Settings', 'Settings', $menu_capability, $menu_slug, array($admin, 'create_admin_settings_page'));
843 + $register_submenu('general', 'Settings', 'Settings', $menu_capability, $menu_slug, array($admin, 'create_admin_settings_page'));
728 844 }
729 845
730 - // Local Business
731 - add_submenu_page($menu_slug, 'Local Business', 'Local Business', $menu_capability, $menu_slug . '-local-business', array($admin, 'create_admin_local_business_page'));
846 + // Local Business (in-page sidebar only in short mode)
847 + $register_submenu('local_business', 'Local Business', 'Local Business', $menu_capability, $menu_slug . '-local-business', array($admin, 'create_admin_local_business_page'));
732 848
733 - // Breadcrumbs
734 - add_submenu_page($menu_slug, 'Breadcrumbs', 'Breadcrumbs', $menu_capability, $menu_slug . '-breadcrumbs', array($admin, 'create_admin_breadcrumbs_page'));
849 + // Breadcrumbs (in-page sidebar only in short mode)
850 + $register_submenu('breadcrumbs', 'Breadcrumbs', 'Breadcrumbs', $menu_capability, $menu_slug . '-breadcrumbs', array($admin, 'create_admin_breadcrumbs_page'));
735 851
736 - // Code Snippets
737 - add_submenu_page($menu_slug, 'Code Snippets', 'Code Snippets', $menu_capability, $menu_slug . '-code-snippets', array($admin, 'create_admin_code_snippets_page'));
852 + // Code Snippets (in-page sidebar only in short mode)
853 + $register_submenu('code_snippets', 'Code Snippets', 'Code Snippets', $menu_capability, $menu_slug . '-code-snippets', array($admin, 'create_admin_code_snippets_page'));
738 854
739 - // Code Minification
740 - add_submenu_page($menu_slug, 'Code Minification', 'Code Minification', $menu_capability, $menu_slug . '-code-minification', array($admin, 'create_admin_code_minification_page'));
855 + // Code Minification (in-page sidebar only in short mode)
856 + $register_submenu('code_minification', 'Code Minification', 'Code Minification', $menu_capability, $menu_slug . '-code-minification', array($admin, 'create_admin_code_minification_page'));
741 857
858 + // Media Optimization (in-page sidebar only in short mode)
859 + $register_submenu('media_optimization', 'Media Optimization', 'Media Optimization', $menu_capability, $menu_slug . '-media-optimization', array($admin, 'create_admin_media_optimization_page'));
860 +
742 861 // Custom Pages
743 862 if (Metasync_Access_Control::user_can_access('hide_custom_pages')) {
744 - add_submenu_page($menu_slug, 'Custom Pages', 'Custom Pages', $menu_capability, $menu_slug . '-custom-pages', array($admin, 'create_admin_custom_pages_page'));
863 + $register_submenu('custom_pages', 'Custom Pages', 'Custom Pages', $menu_capability, $menu_slug . '-custom-pages', array($admin, 'create_admin_custom_pages_page'));
745 864 }
746 865
747 - // Bot Statistics
748 - add_submenu_page($menu_slug, 'Bot Statistics', 'Bot Statistics', $menu_capability, $menu_slug . '-bot-statistics', array($admin, 'create_admin_bot_statistics_page'));
866 + // Bot Statistics (in-page sidebar only in short mode)
867 + $register_submenu('bot_statistics', 'Bot Statistics', 'Bot Statistics', $menu_capability, $menu_slug . '-bot-statistics', array($admin, 'create_admin_bot_statistics_page'));
749 868
750 869 // SEO Health dashboard
751 - add_submenu_page($menu_slug, 'SEO Health', 'SEO Health', $menu_capability, $menu_slug . '-seo-health', array($admin, 'create_admin_seo_health_page'));
870 + $register_submenu('seo_health', 'SEO Health', 'SEO Health', $menu_capability, $menu_slug . '-seo-health', array($admin, 'create_admin_seo_health_page'));
752 871
753 872 // Compatibility
754 873 if (Metasync_Access_Control::user_can_access('hide_compatibility')) {
755 - add_submenu_page($menu_slug, 'Compatibility', 'Compatibility', $menu_capability, $menu_slug . '-compatibility', array($admin, 'create_admin_compatibility_page'));
874 + $register_submenu('compatibility', 'Compatibility', 'Compatibility', $menu_capability, $menu_slug . '-compatibility', array($admin, 'create_admin_compatibility_page'));
756 875 }
757 876
758 877 // Changes Log
759 878 if (Metasync_Access_Control::user_can_access('hide_sync_log')) {
760 - add_submenu_page($menu_slug, 'Changes Log', 'Changes Log', $menu_capability, $menu_slug . '-sync-log', array($admin, 'create_admin_sync_log_page'));
879 + $register_submenu('sync_log', 'Changes Log', 'Changes Log', $menu_capability, $menu_slug . '-sync-log', array($admin, 'create_admin_sync_log_page'));
761 880 }
762 881
763 882 // Report Issue
764 883 if (Metasync_Access_Control::user_can_access('hide_report_issue')) {
765 - add_submenu_page($menu_slug, 'Report Issue', 'Report Issue', $menu_capability, $menu_slug . '-report-issue', array($admin, 'create_admin_report_issue_page'));
884 + $register_submenu('report_issue', 'Report Issue', 'Report Issue', $menu_capability, $menu_slug . '-report-issue', array($admin, 'create_admin_report_issue_page'));
766 885 }
767 886
768 887 // ── Connect CTA (shown when not authenticated) ────────────────────
888 + // Kept as a sidebar row so an unconnected site always has a visible
889 + // entry point alongside the top-level menu item.
769 890 if (!$is_fully_connected) {
770 - add_submenu_page($menu_slug, 'Connect to SearchAtlas', 'Connect to SearchAtlas', $menu_capability, $connect_slug, array($admin, 'create_admin_settings_page'));
891 + $connect_label = sprintf('Connect to %s', Metasync::get_effective_plugin_name());
892 + add_submenu_page($menu_slug, $connect_label, $connect_label, $menu_capability, $connect_slug, array($admin, 'create_admin_settings_page'));
771 893 }
772 894
773 895 // ── Hidden pages (no sidebar entry needed) ────────────────────────
896 + $admin_page_titles[$menu_slug . '-setup-wizard'] = 'Setup Wizard';
774 897 add_submenu_page('', 'Setup Wizard', 'Setup Wizard', $menu_capability, $menu_slug . '-setup-wizard', array($admin->setup_wizard, 'render_wizard_page'));
775 898
899 + // WordPress cannot resolve titles for pages registered under an empty
900 + // parent, even though those pages remain reachable by URL. Restore the
901 + // standard admin title format for our registered pages only.
902 + add_filter('admin_title', function($admin_title, $title) use (&$admin_page_titles) {
903 + $current_page = isset($_GET['page']) && is_string($_GET['page']) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page slug for the admin title
904 + ? sanitize_text_field(wp_unslash($_GET['page'])) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page slug for the admin title
905 + : '';
776 906
907 + if (empty($title) && isset($admin_page_titles[$current_page])) {
908 + $site_title = is_network_admin() || is_user_admin()
909 + ? get_network()->site_name
910 + : get_bloginfo('name');
911 +
912 + $admin_title = sprintf(
913 + /* translators: 1: Admin page title, 2: Site or network title. */
914 + __('%1$s &lsaquo; %2$s &#8212; WordPress', 'metasync'),
915 + $admin_page_titles[$current_page],
916 + $site_title
917 + );
918 + }
919 +
920 + return $admin_title;
921 + }, 10, 2);
922 +
923 +
777 924 // Rename auto-generated first submenu from plugin name to "Settings" and reorder
778 925 add_action('admin_menu', function() use ($menu_slug) {
779 926 global $submenu;
780 927 if (!isset($submenu[$menu_slug])) {
@@ -804,9 +951,8 @@
804 951 'dashboard' => 'dashboard',
805 952 'compatibility' => 'admin-tools',
806 953 'sync_log' => 'list-view',
807 954 'seo_controls' => 'search',
808 - 'optimal_settings' => 'superhero-alt',
809 955 'instant_index' => 'performance',
810 956 'google_console' => 'chart-area',
811 957 'bing_console' => 'chart-bar',
812 958 'redirections' => 'undo',
@@ -846,9 +992,9 @@
846 992 <?php
847 993 // Dashboard tab (standalone)
848 994 if (isset($seo_items['dashboard'])) {
849 995 $is_active = ($current_page === 'dashboard');
850 - $icon = $menu_icons['dashboard'] ?? 'admin-generic';
996 + $icon = $menu_icons['dashboard'];
851 997 $page_url = '?page=' . $page_slug . $seo_items['dashboard']['slug_suffix'];
852 998 ?>
853 999 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-tab <?php echo $is_active ? 'active' : ''; ?>">
854 1000 <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
@@ -1195,13 +1341,13 @@
1195 1341 <img src="<?php echo esc_url($logo['dark_url']); ?>" alt="Logo" class="metasync-logo metasync-logo-dark" />
1196 1342 </div>
1197 1343 <?php elseif ($logo['show_logo'] && !empty($logo['url'])): ?>
1198 1344 <div class="metasync-logo-container">
1199 - <img src="<?php echo esc_url($logo['url']); ?>" alt="Logo" class="metasync-logo" />
1345 + <img src="<?php echo esc_url($logo['url']); ?>" alt="Logo" class="metasync-logo<?php echo !empty($logo['is_default']) ? ' metasync-logo-default' : ''; ?>" />
1200 1346 </div>
1201 1347 <?php endif; ?>
1202 1348 </div>
1203 -
1349 +
1204 1350 <div class="metasync-header-right">
1205 1351 <!-- Theme Toggle -->
1206 1352 <div class="metasync-theme-toggle" role="group" aria-label="Theme Selector">
1207 1353 <button class="metasync-theme-option <?php echo ($current_theme === 'light') ? 'active' : ''; ?>" data-theme="light" aria-label="Light Theme" type="button">
@@ -1229,9 +1375,9 @@
1229 1375 $status_title_header = 'Not Synced - Heartbeat API not responding or unreachable';
1230 1376 $status_text_header = 'Not Synced';
1231 1377 }
1232 1378 ?>
1233 - <div class="metasync-integration-status <?php echo $status_class_header; ?>"
1379 + <div class="metasync-integration-status <?php echo esc_attr($status_class_header); ?>"
1234 1380 title="<?php echo esc_attr($status_title_header); ?>">
1235 1381 <span class="status-indicator"></span>
1236 1382 <span class="status-text"><?php echo esc_html($status_text_header); ?></span>
1237 1383 </div>
@@ -1311,10 +1457,10 @@
1311 1457
1312 1458 <?php
1313 1459 $page_slug = Metasync_Admin::$page_slug;
1314 1460 $is_plugin_page = (
1315 - (isset($_GET['page']) && strpos($_GET['page'], $page_slug) !== false) ||
1316 - (isset($_GET['page']) && strpos($_GET['page'], 'searchatlas') !== false)
1461 + (isset($_GET['page']) && strpos($_GET['page'], $page_slug) !== false) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page check for an inline config script
1462 + (isset($_GET['page']) && strpos($_GET['page'], 'searchatlas') !== false) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page check for an inline config script
1317 1463 );
1318 1464 if ($is_plugin_page):
1319 1465 ?>
1320 1466 <script type="text/javascript">
@@ -1405,64 +1551,78 @@
1405 1551 # For backward compatibility, constant takes precedence.
1406 1552 if (defined('METASYNC_SHOW_ADMIN_BAR_STATUS') && !METASYNC_SHOW_ADMIN_BAR_STATUS) {
1407 1553 return;
1408 1554 }
1409 -
1555 +
1410 1556 # Check if admin bar status is disabled via setting
1411 1557 $general_settings = Metasync::get_option('general');
1558 + if (!is_array($general_settings)) {
1559 + $general_settings = [];
1560 + }
1412 1561 $show_admin_bar = $general_settings['show_admin_bar_status'] ?? true;
1413 1562 if (!$show_admin_bar) {
1414 1563 return;
1415 1564 }
1416 -
1417 - if (!Metasync::current_user_has_plugin_access()) {
1418 - return;
1419 - }
1420 1565
1421 1566 if (!is_admin() && !apply_filters('metasync_show_admin_bar_status_frontend', false)) {
1422 1567 return;
1423 1568 }
1424 1569
1425 - $general_settings = Metasync::get_option('general');
1426 - if (!is_array($general_settings)) {
1427 - $general_settings = [];
1428 - }
1429 -
1430 - $is_synced = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general_settings);
1431 -
1432 - $plugin_name = Metasync::get_effective_plugin_name();
1433 -
1434 - if ($is_synced) {
1435 - $otto_uuid = $general_settings['otto_pixel_uuid'] ?? '';
1436 - if (!empty($otto_uuid)) {
1437 - $status_emoji = '🟢'; // Green circle for synced
1438 - $title = $plugin_name . ' - Synced (Heartbeat API connectivity verified)';
1439 - $status_class = 'searchatlas-synced';
1570 + $node = get_transient(self::CACHE_KEY);
1571 +
1572 + if ($node === false) {
1573 + $is_synced = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general_settings);
1574 +
1575 + $plugin_name = Metasync::get_effective_plugin_name();
1576 +
1577 + if ($is_synced) {
1578 + $otto_uuid = $general_settings['otto_pixel_uuid'] ?? '';
1579 + if (!empty($otto_uuid)) {
1580 + $status_emoji = '🟢'; // Green circle for synced
1581 + $title = $plugin_name . ' - Synced (Heartbeat API connectivity verified)';
1582 + $status_class = 'searchatlas-synced';
1583 + } else {
1584 + $status_emoji = '🟡'; // Yellow circle for warning
1585 + $title = $plugin_name . ' - Connected but OTTO UUID is missing — deploys will not work. Please reconnect.';
1586 + $status_class = 'searchatlas-warning';
1587 + }
1440 1588 } else {
1441 - $status_emoji = '🟡'; // Yellow circle for warning
1442 - $title = $plugin_name . ' - Connected but OTTO UUID is missing — deploys will not work. Please reconnect.';
1443 - $status_class = 'searchatlas-warning';
1589 + $status_emoji = '🔴'; // Red circle for not synced
1590 + $title = $plugin_name . ' - Not Synced (Heartbeat API not responding or unreachable)';
1591 + $status_class = 'searchatlas-not-synced';
1444 1592 }
1445 - } else {
1446 - $status_emoji = '🔴'; // Red circle for not synced
1447 - $title = $plugin_name . ' - Not Synced (Heartbeat API not responding or unreachable)';
1448 - $status_class = 'searchatlas-not-synced';
1593 +
1594 + $admin_bar_title = $plugin_name . ' ' . $status_emoji;
1595 +
1596 + $node = array(
1597 + 'title' => $admin_bar_title,
1598 + 'href' => admin_url('admin.php?page=' . Metasync_Admin::$page_slug),
1599 + 'meta_title' => $title,
1600 + 'meta_class' => $status_class,
1601 + );
1602 +
1603 + set_transient(self::CACHE_KEY, $node, apply_filters('metasync_admin_bar_status_cache_ttl', 5 * MINUTE_IN_SECONDS));
1449 1604 }
1450 1605
1451 - $display_name = $plugin_name;
1452 - $admin_bar_title = $display_name . ' ' . $status_emoji;
1453 -
1454 1606 $wp_admin_bar->add_node(array(
1455 1607 'id' => 'searchatlas-status',
1456 - 'title' => $admin_bar_title,
1457 - 'href' => admin_url('admin.php?page=' . Metasync_Admin::$page_slug),
1608 + 'title' => $node['title'],
1609 + 'href' => $node['href'],
1458 1610 'meta' => array(
1459 - 'title' => $title,
1460 - 'class' => $status_class
1461 - )
1611 + 'title' => $node['meta_title'],
1612 + 'class' => $node['meta_class'],
1613 + ),
1462 1614 ));
1463 1615 }
1464 1616
1617 + /**
1618 + * Invalidate the admin bar status cache so the next page load recomputes status.
1619 + */
1620 + public static function invalidate_admin_bar_status_cache()
1621 + {
1622 + delete_transient(self::CACHE_KEY);
1623 + }
1624 +
1465 1625 // ------------------------------------------------------------------
1466 1626 // Yoast-style 3-column layout helpers
1467 1627 // ------------------------------------------------------------------
1468 1628
@@ -1477,18 +1637,20 @@
1477 1637 * @param string $description Optional subtitle shown below the title.
1478 1638 */
1479 1639 public function render_layout_open($page_title = '', $current_page = '', $description = '')
1480 1640 {
1481 - $theme = esc_attr(get_option('metasync_theme', 'dark'));
1641 + $theme = get_option('metasync_theme', 'dark');
1482 1642 $plugin_name = Metasync::get_effective_plugin_name();
1483 1643 $general = Metasync::get_option('general') ?? [];
1484 1644 $is_connected = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general);
1645 + // badge reflects confirmed heartbeat health (connected / stale /
1646 + // disconnected), not raw API-key presence.
1647 + $badge = Metasync_Heartbeat_Manager::instance()->get_connection_badge($general);
1485 1648 $logo = $this->resolve_logo_data();
1486 1649
1487 1650 $current_theme = get_option('metasync_theme', 'dark');
1488 - $api_key = $general['searchatlas_api_key'] ?? '';
1489 1651 ?>
1490 - <div class="wrap metasync-dashboard-wrap" data-theme="<?php echo $theme; ?>">
1652 + <div class="wrap metasync-dashboard-wrap" data-theme="<?php echo esc_attr($theme); ?>">
1491 1653
1492 1654 <?php
1493 1655 // Inject whitelabel color palette overrides
1494 1656 $wl_settings = Metasync::get_whitelabel_settings();
@@ -1518,8 +1680,9 @@
1518 1680 $vars .= '--' . esc_attr($grad_var) . ':linear-gradient(135deg,' . esc_attr($from) . ' 0%,' . esc_attr($to) . ' 100%);';
1519 1681 }
1520 1682 }
1521 1683 if ($vars) {
1684 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSS custom properties inside a raw <style> element; every value is a regex-validated hex color and HTML entities are not decoded in raw text elements
1522 1685 echo $selector . '{' . $vars . '}';
1523 1686 }
1524 1687 }
1525 1688 }
@@ -1533,17 +1696,17 @@
1533 1696 <?php if ($logo['show_logo'] && $logo['use_dual']): ?>
1534 1697 <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;">
1535 1698 <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;">
1536 1699 <?php elseif ($logo['show_logo'] && $logo['url']): ?>
1537 - <img src="<?php echo esc_url($logo['url']); ?>" alt="<?php echo esc_attr($plugin_name); ?>" class="metasync-logo" style="height:28px;width:auto;">
1700 + <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;">
1538 1701 <?php else: ?>
1539 1702 <strong style="font-size:15px;color:var(--dashboard-text-primary);"><?php echo esc_html($plugin_name); ?></strong>
1540 1703 <?php endif; ?>
1541 1704 </div>
1542 1705 <div class="metasync-header-compact-right">
1543 - <div class="metasync-status <?php echo !empty($api_key) ? 'connected' : 'disconnected'; ?>">
1706 + <div class="metasync-status <?php echo esc_attr($badge['class']); ?>">
1544 1707 <span class="status-dot"></span>
1545 - <span class="status-text"><?php echo !empty($api_key) ? 'Connected' : 'Not Connected'; ?></span>
1708 + <span class="status-text"><?php echo esc_html($badge['text']); ?></span>
1546 1709 </div>
1547 1710 <button type="button" class="metasync-theme-toggle" onclick="toggleMetasyncTheme()" title="Toggle theme">
1548 1711 <span class="theme-icon-light">&#9728;</span>
1549 1712 <span class="theme-icon-dark">&#9790;</span>
@@ -1560,8 +1723,17 @@
1560 1723 </aside>
1561 1724
1562 1725 <!-- Main content -->
1563 1726 <main class="metasync-layout-main">
1727 + <?php
1728 + // Anchor for WP core's notice relocation (common.js). Without it, core
1729 + // moves admin notices after the first .wrap h1 only on DOM-ready, after
1730 + // the server has already painted them at the top of #wpbody-content —
1731 + // causing a flash above the header and layout shift. Providing
1732 + // an explicit .wp-header-end inside this (FOUC-hidden) wrap makes core
1733 + // relocate notices here deterministically, so they fade in with the page.
1734 + ?>
1735 + <hr class="wp-header-end">
1564 1736 <?php if ($page_title || $description): ?>
1565 1737 <div class="metasync-page-header">
1566 1738 <?php if ($page_title): ?>
1567 1739 <h1><?php echo esc_html($page_title); ?></h1>
@@ -1585,15 +1757,19 @@
1585 1757 {
1586 1758 ?>
1587 1759 </main><!-- /.metasync-layout-main -->
1588 1760
1589 - <?php if ($show_promo):
1590 - $general = Metasync::get_option('general') ?? [];
1591 - $is_connected = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general);
1761 + <?php
1762 + // The sidebar exists to get an unconnected site connected. Once the
1763 + // site is linked it has nothing further to offer, so the whole
1764 + // column is dropped and the grid reflows to two columns.
1765 + $show_promo = $show_promo && ! Metasync_Heartbeat_Manager::instance()
1766 + ->is_heartbeat_connected(Metasync::get_option('general') ?? []);
1767 + if ($show_promo):
1592 1768 ?>
1593 1769 <!-- Right promo sidebar -->
1594 1770 <aside class="metasync-layout-promo">
1595 - <?php $this->render_promo_sidebar($is_connected); ?>
1771 + <?php $this->render_promo_sidebar(); ?>
1596 1772 </aside>
1597 1773 <?php endif; ?>
1598 1774
1599 1775 </div><!-- /.metasync-layout -->
@@ -1630,9 +1806,8 @@
1630 1806 'breadcrumbs' => 'menu',
1631 1807 'code_snippets' => 'editor-code',
1632 1808 'code_minification'=> 'media-code',
1633 1809 'custom_pages' => 'admin-page',
1634 - 'optimal_settings' => 'superhero-alt',
1635 1810 'compatibility' => 'admin-tools',
1636 1811 'sync_log' => 'list-view',
1637 1812 'bot_statistics' => 'visibility',
1638 1813 'report_issue' => 'sos',
@@ -1648,11 +1823,20 @@
1648 1823 } else {
1649 1824 $plugin_items[$key] = $item;
1650 1825 }
1651 1826 }
1827 +
1828 + $total_items = count($seo_items) + count($plugin_items);
1829 +
1652 1830 ?>
1653 1831 <nav class="metasync-sidenav">
1654 1832
1833 + <?php if ($total_items === 0): ?>
1834 + <div class="metasync-sidenav-empty">
1835 + <?php esc_html_e('No pages are available.', 'metasync'); ?>
1836 + </div>
1837 + <?php else: ?>
1838 +
1655 1839 <!-- SEO Features group -->
1656 1840 <div class="metasync-sidenav-group">
1657 1841 <div class="metasync-sidenav-group-title">SEO Features</div>
1658 1842 <ul>
@@ -1658,12 +1842,12 @@
1658 1842 <ul>
1659 1843 <?php foreach ($seo_items as $key => $item):
1660 1844 $is_active = ($current_page === $key);
1661 1845 $icon = $icons[$key] ?? 'admin-generic';
1662 - $url = esc_url(admin_url('admin.php?page=' . $page_slug . $item['slug_suffix']));
1846 + $url = admin_url('admin.php?page=' . $page_slug . $item['slug_suffix']);
1663 1847 ?>
1664 1848 <li class="<?php echo $is_active ? 'metasync-sidenav-active' : ''; ?>">
1665 - <a href="<?php echo $url; ?>">
1849 + <a href="<?php echo esc_url($url); ?>">
1666 1850 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
1667 1851 <?php echo esc_html($item['title']); ?>
1668 1852 </a>
1669 1853 </li>
@@ -1678,12 +1862,12 @@
1678 1862 <?php foreach ($plugin_items as $key => $item):
1679 1863 if ($key === 'report_issue') continue;
1680 1864 $is_active = ($current_page === $key);
1681 1865 $icon = $icons[$key] ?? 'admin-generic';
1682 - $url = esc_url(admin_url('admin.php?page=' . $page_slug . $item['slug_suffix']));
1866 + $url = admin_url('admin.php?page=' . $page_slug . $item['slug_suffix']);
1683 1867 ?>
1684 1868 <li class="<?php echo $is_active ? 'metasync-sidenav-active' : ''; ?>">
1685 - <a href="<?php echo $url; ?>">
1869 + <a href="<?php echo esc_url($url); ?>">
1686 1870 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
1687 1871 <?php echo esc_html($item['title']); ?>
1688 1872 </a>
1689 1873 </li>
@@ -1718,8 +1902,10 @@
1718 1902 </ul>
1719 1903 </div>
1720 1904 <?php endif; ?>
1721 1905
1906 + <?php endif; ?>
1907 +
1722 1908 </nav>
1723 1909 <?php
1724 1910 }
1725 1911
@@ -1725,17 +1911,22 @@
1725 1911
1726 1912 /**
1727 1913 * Render the right promotional sidebar.
1728 1914 *
1729 - * @param bool $is_connected Whether the plugin is authenticated.
1915 + * Only reached while the site is unconnected; the caller drops the
1916 + * whole column once the plugin is linked.
1730 1917 */
1731 - public function render_promo_sidebar($is_connected = false)
1918 + public function render_promo_sidebar()
1732 1919 {
1733 1920 $plugin_name = Metasync::get_effective_plugin_name();
1734 1921 $otto_name = Metasync::get_whitelabel_otto_name();
1735 - $settings_url = esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug));
1922 + $settings_url = admin_url('admin.php?page=' . Metasync_Admin::$page_slug);
1736 1923 $homepage = Metasync::HOMEPAGE_DOMAIN;
1737 - $is_default_brand = ( $plugin_name === 'Search Atlas' );
1924 + $docs_url = Metasync::DOCUMENTATION_DOMAIN;
1925 + // These links point at Search Atlas properties, so they only belong
1926 + // here while the plugin runs under its own name AND branding — a
1927 + // reseller can leave the name untouched and still be whitelabeled.
1928 + $is_default_brand = ( $plugin_name === 'Search Atlas' ) && ! Metasync::is_whitelabel_enabled();
1738 1929 $whitelabel = Metasync::get_whitelabel_settings();
1739 1930 $custom_links = isset($whitelabel['quick_links']) && is_array($whitelabel['quick_links'])
1740 1931 ? array_filter($whitelabel['quick_links'], function($l) { return !empty($l['url']); })
1741 1932 : [];
@@ -1740,9 +1931,8 @@
1740 1931 ? array_filter($whitelabel['quick_links'], function($l) { return !empty($l['url']); })
1741 1932 : [];
1742 1933 ?>
1743 1934
1744 - <?php if (!$is_connected): ?>
1745 1935 <!-- Connect CTA card -->
1746 1936 <div class="metasync-promo-card metasync-promo-card--connect">
1747 1937 <div class="metasync-promo-card-header">
1748 1938 <div class="metasync-promo-card-icon">
@@ -1758,40 +1948,12 @@
1758 1948 <li><span class="promo-check">&#10003;</span> Real-time keyword tracking</li>
1759 1949 <li><span class="promo-check">&#10003;</span> Automated schema markup</li>
1760 1950 <li><span class="promo-check">&#10003;</span> Instant Google indexing</li>
1761 1951 </ul>
1762 - <a href="<?php echo $settings_url; ?>" class="metasync-promo-btn metasync-promo-btn--primary">
1952 + <a href="<?php echo esc_url($settings_url); ?>" class="metasync-promo-btn metasync-promo-btn--primary">
1763 1953 Connect Now
1764 1954 </a>
1765 1955 </div>
1766 - <?php else: ?>
1767 - <!-- Connected — feature highlights -->
1768 - <div class="metasync-promo-card metasync-promo-card--accent">
1769 - <div class="metasync-promo-card-header">
1770 - <div class="metasync-promo-card-icon">
1771 - <span class="dashicons dashicons-performance"></span>
1772 - </div>
1773 - <div>
1774 - <h3><?php echo esc_html($otto_name); ?> Active</h3>
1775 - </div>
1776 - </div>
1777 - <p class="metasync-promo-tagline">Your site is connected and <?php echo esc_html($otto_name); ?> is optimizing pages automatically.</p>
1778 - <ul class="metasync-promo-benefits">
1779 - <li><span class="promo-check">&#10003;</span> Schema markup auto-applied</li>
1780 - <li><span class="promo-check">&#10003;</span> Meta titles &amp; descriptions optimized</li>
1781 - <li><span class="promo-check">&#10003;</span> Internal linking suggestions active</li>
1782 - </ul>
1783 - <?php if ($is_default_brand): ?>
1784 - <a href="<?php echo esc_url($homepage); ?>" target="_blank" rel="noopener" class="metasync-promo-btn metasync-promo-btn--outline">
1785 - View <?php echo esc_html($plugin_name); ?> Dashboard
1786 - </a>
1787 - <?php elseif (!empty($whitelabel['domain'])): ?>
1788 - <a href="<?php echo esc_url($whitelabel['domain']); ?>" target="_blank" rel="noopener" class="metasync-promo-btn metasync-promo-btn--outline">
1789 - View <?php echo esc_html($plugin_name); ?> Dashboard
1790 - </a>
1791 - <?php endif; ?>
1792 - </div>
1793 - <?php endif; ?>
1794 1956
1795 1957 <?php
1796 1958 // Quick Links: show default links for default brand, custom links if whitelabeled + provided, hide if whitelabeled + none
1797 1959 $show_quick_links = $is_default_brand || !empty($custom_links);
@@ -1801,12 +1963,20 @@
1801 1963 <div class="metasync-promo-card">
1802 1964 <h3 style="margin:0 0 12px;font-size:13px;font-weight:700;color:var(--dashboard-text-primary);">Quick Links</h3>
1803 1965 <ul class="metasync-promo-links">
1804 1966 <?php if ($is_default_brand): ?>
1967 + <li><a href="<?php echo esc_url($docs_url); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-book-alt"></span> Documentation</a></li>
1805 1968 <li><a href="<?php echo esc_url($homepage . '/blog/'); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-rss"></span> SEO Blog</a></li>
1806 - <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($homepage . '/contact/'); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-email-alt"></span> Contact Support</a></li>
1807 1970 <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>
1971 + <?php
1972 + // Mirrors the gate that decides whether the page is registered
1973 + // at all. Without it the link outlives the page for any user
1974 + // the access rules hide it from, and leads nowhere.
1975 + if (Metasync_Access_Control::user_can_access('hide_report_issue')):
1976 + ?>
1808 1977 <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>
1978 + <?php endif; ?>
1809 1979 <?php else: ?>
1810 1980 <?php foreach ($custom_links as $link): ?>
1811 1981 <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>
1812 1982 <?php endforeach; ?>