PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / trunk
Search Atlas SEO – OTTO AI SEO Automation for WordPress vtrunk
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 2.6.4 2.6.3 All 139 releases
← All changes | includes/class-metasync-admin-navigation.php +265 -131 2.6.10trunk View file →
@@ -20,8 +20,107 @@
20 20
21 21 const CACHE_KEY = 'metasync_admin_bar_status';
22 22
23 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 + /**
24 123 * Get singleton instance.
25 124 *
26 125 * @return self
27 126 */
@@ -34,8 +133,24 @@
34 133 }
35 134
36 135 private function __construct() {}
37 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 +
38 153 // ------------------------------------------------------------------
39 154 // Logo resolution helper
40 155 // ------------------------------------------------------------------
41 156
@@ -75,9 +190,9 @@
75 190 $data['show_logo'] = true;
76 191 $data['url'] = $single;
77 192 } elseif (!$is_whitelabel) {
78 193 $data['show_logo'] = true;
79 - $data['url'] = Metasync::HOMEPAGE_DOMAIN . '/wp-content/uploads/2023/12/white.svg';
194 + $data['url'] = plugin_dir_url(dirname(__FILE__)) . 'assets/images/searchatlas-logo.svg';
80 195 $data['is_default'] = true;
81 196 }
82 197 }
83 198
@@ -107,10 +222,11 @@
107 222 $logo = $this->resolve_logo_data();
108 223
109 224 $current_theme = get_option('metasync_theme', 'dark');
110 225 $general_settings = Metasync::get_option('general');
111 - $searchatlas_api_key = isset($general_settings['searchatlas_api_key']) ? $general_settings['searchatlas_api_key'] : '';
112 - $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);
113 229 ?>
114 230 <div class="metasync-header" data-current-theme="<?php echo esc_attr($current_theme); ?>">
115 231 <div class="metasync-header-left">
116 232 <?php if ($logo['show_logo'] && $logo['use_dual']): ?>
@@ -124,11 +240,11 @@
124 240 </div>
125 241 <?php endif; ?>
126 242 </div>
127 243 <div class="metasync-header-right">
128 - <div class="metasync-status <?php echo $is_integrated ? 'connected' : 'disconnected'; ?>">
244 + <div class="metasync-status <?php echo esc_attr($badge['class']); ?>">
129 245 <span class="status-dot"></span>
130 - <span class="status-text"><?php echo $is_integrated ? 'Connected' : 'Not Connected'; ?></span>
246 + <span class="status-text"><?php echo esc_html($badge['text']); ?></span>
131 247 </div>
132 248 <button type="button" class="metasync-theme-toggle" onclick="toggleMetasyncTheme()" title="Toggle theme">
133 249 <span class="theme-icon-light">&#9728;</span>
134 250 <span class="theme-icon-dark">&#9790;</span>
@@ -150,9 +266,8 @@
150 266 $menu_items = [];
151 267 $menu_icons = [
152 268 'dashboard' => 'dashboard',
153 269 'seo_controls' => 'search',
154 - 'optimal_settings' => 'superhero-alt',
155 270 'instant_index' => 'performance',
156 271 'google_console' => 'chart-area',
157 272 'compatibility' => 'admin-tools',
158 273 'sync_log' => 'list-view',
@@ -163,9 +278,9 @@
163 278 'report_issue' => 'sos',
164 279 'general' => 'admin-settings',
165 280 ];
166 281
167 - if (empty($whitelabel_settings['hide_dashboard'])) {
282 + if (empty($whitelabel_settings['hide_dashboard']) && !self::is_dashboard_hidden_by_framework()) {
168 283 $menu_items['dashboard'] = ['title' => 'Dashboard', 'slug_suffix' => '-dashboard'];
169 284 }
170 285
171 286 if (empty($whitelabel_settings['hide_indexation_control'])) {
@@ -170,17 +285,9 @@
170 285
171 286 if (empty($whitelabel_settings['hide_indexation_control'])) {
172 287 $menu_items['seo_controls'] = ['title' => 'Indexation Control', 'slug_suffix' => '-seo-controls'];
173 288 }
174 -
175 - if ($general_options['enable_optimal_settings'] ?? false) {
176 - $menu_items['optimal_settings'] = ['title' => 'Optimal Settings', 'slug_suffix' => '-optimal-settings'];
177 - }
178 -
179 - if ($general_options['enable_googleinstantindex'] ?? false) {
180 - $menu_items['instant_index'] = ['title' => 'Instant Indexing', 'slug_suffix' => '-instant-index'];
181 - }
182 -
289 +
183 290 if ($general_options['enable_google_console'] ?? false) {
184 291 $menu_items['google_console'] = ['title' => 'Google Console', 'slug_suffix' => '-google-console'];
185 292 }
186 293
@@ -206,9 +313,9 @@
206 313 <div class="metasync-nav-tabs">
207 314 <div class="metasync-nav-left">
208 315 <?php foreach ($menu_items as $key => $menu_item):
209 316 $is_active = ($current_page === $key);
210 - $icon = $menu_icons[$key] ?? 'admin-generic';
317 + $icon = $menu_icons[$key];
211 318 $page_url = '?page=' . $page_slug . $menu_item['slug_suffix'];
212 319 ?>
213 320 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-tab <?php echo $is_active ? 'active' : ''; ?>">
214 321 <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
@@ -373,10 +480,10 @@
373 480 $menu_items = [];
374 481
375 482 // === SEO FEATURES GROUP ===
376 483
377 - // Dashboard (check access control)
378 - 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()) {
379 486 $menu_items['dashboard'] = [
380 487 'title' => 'Dashboard',
381 488 'slug_suffix' => '-dashboard',
382 489 'callback' => 'create_admin_dashboard_iframe',
@@ -395,20 +502,8 @@
395 502 'group' => 'seo'
396 503 ];
397 504 }
398 505
399 - // Instant Indexing - setting now stored in seo_controls (moved from Settings to Indexation Control)
400 - $seo_controls = Metasync::get_option('seo_controls');
401 - if ($seo_controls['enable_googleinstantindex'] ?? false) {
402 - $menu_items['instant_index'] = [
403 - 'title' => 'Instant Indexing',
404 - 'slug_suffix' => '-instant-index',
405 - 'callback' => 'create_admin_google_instant_index_page',
406 - 'internal_nav' => 'Instant Indexing',
407 - 'group' => 'seo'
408 - ];
409 - }
410 -
411 506 if ($general_options['enable_google_console'] ?? false) {
412 507 $menu_items['google_console'] = [
413 508 'title' => 'Google Console',
414 509 'slug_suffix' => '-google-console',
@@ -576,18 +671,8 @@
576 671 'group' => 'plugin'
577 672 ];
578 673 }
579 674
580 - if ($general_options['enable_optimal_settings'] ?? false) {
581 - $menu_items['optimal_settings'] = [
582 - 'title' => 'Optimal Settings',
583 - 'slug_suffix' => '-optimal-settings',
584 - 'callback' => 'create_admin_optimal_settings_page',
585 - 'internal_nav' => 'Optimal Settings',
586 - 'group' => 'plugin'
587 - ];
588 - }
589 -
590 675 // Compatibility page (check access control)
591 676 if (Metasync_Access_Control::user_can_access('hide_compatibility')) {
592 677 $menu_items['compatibility'] = [
593 678 'title' => 'Compatibility',
@@ -646,9 +731,11 @@
646 731 $data = Metasync::get_option('general');
647 732 $plugin_name = Metasync::get_effective_plugin_name();
648 733 $menu_name = $plugin_name;
649 734 $menu_title = $plugin_name;
650 - $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;
651 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'];
652 739
653 740 // Use 'read' capability since actual access is controlled by current_user_has_plugin_access() check above
654 741 $menu_capability = 'read';
@@ -658,15 +745,19 @@
658 745 global $menu;
659 746 $menu['100'] = array('', 'read', 'separator-metasync-before', '', 'wp-menu-separator');
660 747 }, 5);
661 748
662 - // 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);
663 754 add_menu_page(
664 755 $menu_name,
665 756 $menu_title,
666 757 $menu_capability,
667 758 $menu_slug,
668 - array($admin, 'create_admin_settings_page'),
759 + $top_level_callback,
669 760 $menu_icon,
670 761 '100.1'
671 762 );
672 763
@@ -680,122 +771,157 @@
680 771
681 772 // ── Connect slug ──────────────────────────────────────────────────
682 773 $connect_slug = $menu_slug . '-connect';
683 774
684 - // Dashboard
685 - if (Metasync_Access_Control::user_can_access('hide_dashboard')) {
686 - 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'));
687 793 }
688 794
689 795 // Indexation Control
690 796 if (Metasync_Access_Control::user_can_access('hide_indexation_control')) {
691 - 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'));
692 798 }
693 799
694 800 // 404 Monitor
695 - 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'));
696 802
697 803 // Redirections
698 804 if (Metasync_Access_Control::user_can_access('hide_redirections')) {
699 - 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'));
700 806 }
701 807
702 808 // XML Sitemap
703 809 if (Metasync_Access_Control::user_can_access('hide_xml_sitemap')) {
704 - 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'));
705 811 }
706 812
707 813 // Robots.txt
708 814 if (Metasync_Access_Control::user_can_access('hide_robots')) {
709 - 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'));
710 816 }
711 817
712 - // Site Verification
713 - 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'));
714 820
715 - // Instant Indexing (conditional)
716 - if ($seo_controls['enable_googleinstantindex'] ?? false) {
717 - add_submenu_page($menu_slug, 'Instant Indexing', 'Instant Indexing', $menu_capability, $menu_slug . '-instant-index', array($admin, 'create_admin_google_instant_index_page'));
718 - }
719 -
720 821 // Google Console (conditional)
721 822 if ($general_options['enable_google_console'] ?? false) {
722 - 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'));
723 824 }
724 825
725 826 // Bing Console (conditional)
726 827 if ($seo_controls['enable_binginstantindex'] ?? false) {
727 - 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'));
728 829 }
729 830
730 831 // Schema Markup
731 - 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'));
732 833
733 834 // Import SEO Data
734 835 if (Metasync_Access_Control::user_can_access('hide_import_seo')) {
735 - 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'));
736 837 }
737 838
738 - // 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()).
739 842 if (Metasync_Access_Control::user_can_access('hide_settings')) {
740 - 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'));
741 844 }
742 845
743 - // Optimal Settings (conditional)
744 - if ($general_options['enable_optimal_settings'] ?? false) {
745 - add_submenu_page($menu_slug, 'Optimal Settings', 'Optimal Settings', $menu_capability, $menu_slug . '-optimal-settings', array($admin, 'create_admin_optimal_settings_page'));
746 - }
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'));
747 848
748 - // Local Business
749 - add_submenu_page($menu_slug, 'Local Business', 'Local Business', $menu_capability, $menu_slug . '-local-business', array($admin, 'create_admin_local_business_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'));
750 851
751 - // Breadcrumbs
752 - add_submenu_page($menu_slug, 'Breadcrumbs', 'Breadcrumbs', $menu_capability, $menu_slug . '-breadcrumbs', array($admin, 'create_admin_breadcrumbs_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'));
753 854
754 - // Code Snippets
755 - add_submenu_page($menu_slug, 'Code Snippets', 'Code Snippets', $menu_capability, $menu_slug . '-code-snippets', array($admin, 'create_admin_code_snippets_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'));
756 857
757 - // Code Minification
758 - add_submenu_page($menu_slug, 'Code Minification', 'Code Minification', $menu_capability, $menu_slug . '-code-minification', array($admin, 'create_admin_code_minification_page'));
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'));
759 860
760 - // Media Optimization
761 - add_submenu_page($menu_slug, 'Media Optimization', 'Media Optimization', $menu_capability, $menu_slug . '-media-optimization', array($admin, 'create_admin_media_optimization_page'));
762 -
763 861 // Custom Pages
764 862 if (Metasync_Access_Control::user_can_access('hide_custom_pages')) {
765 - 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'));
766 864 }
767 865
768 - // Bot Statistics
769 - 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'));
770 868
771 869 // SEO Health dashboard
772 - 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'));
773 871
774 872 // Compatibility
775 873 if (Metasync_Access_Control::user_can_access('hide_compatibility')) {
776 - 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'));
777 875 }
778 876
779 877 // Changes Log
780 878 if (Metasync_Access_Control::user_can_access('hide_sync_log')) {
781 - 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'));
782 880 }
783 881
784 882 // Report Issue
785 883 if (Metasync_Access_Control::user_can_access('hide_report_issue')) {
786 - 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'));
787 885 }
788 886
789 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.
790 890 if (!$is_fully_connected) {
791 - 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'));
792 893 }
793 894
794 895 // ── Hidden pages (no sidebar entry needed) ────────────────────────
896 + $admin_page_titles[$menu_slug . '-setup-wizard'] = 'Setup Wizard';
795 897 add_submenu_page('', 'Setup Wizard', 'Setup Wizard', $menu_capability, $menu_slug . '-setup-wizard', array($admin->setup_wizard, 'render_wizard_page'));
796 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'])
904 + ? sanitize_text_field(wp_unslash($_GET['page']))
905 + : '';
797 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'),
915 + $admin_page_titles[$current_page],
916 + $site_title
917 + );
918 + }
919 +
920 + return $admin_title;
921 + }, 10, 2);
922 +
923 +
798 924 // Rename auto-generated first submenu from plugin name to "Settings" and reorder
799 925 add_action('admin_menu', function() use ($menu_slug) {
800 926 global $submenu;
801 927 if (!isset($submenu[$menu_slug])) {
@@ -825,9 +951,8 @@
825 951 'dashboard' => 'dashboard',
826 952 'compatibility' => 'admin-tools',
827 953 'sync_log' => 'list-view',
828 954 'seo_controls' => 'search',
829 - 'optimal_settings' => 'superhero-alt',
830 955 'instant_index' => 'performance',
831 956 'google_console' => 'chart-area',
832 957 'bing_console' => 'chart-bar',
833 958 'redirections' => 'undo',
@@ -867,9 +992,9 @@
867 992 <?php
868 993 // Dashboard tab (standalone)
869 994 if (isset($seo_items['dashboard'])) {
870 995 $is_active = ($current_page === 'dashboard');
871 - $icon = $menu_icons['dashboard'] ?? 'admin-generic';
996 + $icon = $menu_icons['dashboard'];
872 997 $page_url = '?page=' . $page_slug . $seo_items['dashboard']['slug_suffix'];
873 998 ?>
874 999 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-tab <?php echo $is_active ? 'active' : ''; ?>">
875 1000 <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
@@ -1516,12 +1641,14 @@
1516 1641 $theme = esc_attr(get_option('metasync_theme', 'dark'));
1517 1642 $plugin_name = Metasync::get_effective_plugin_name();
1518 1643 $general = Metasync::get_option('general') ?? [];
1519 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);
1520 1648 $logo = $this->resolve_logo_data();
1521 1649
1522 1650 $current_theme = get_option('metasync_theme', 'dark');
1523 - $api_key = $general['searchatlas_api_key'] ?? '';
1524 1651 ?>
1525 1652 <div class="wrap metasync-dashboard-wrap" data-theme="<?php echo $theme; ?>">
1526 1653
1527 1654 <?php
@@ -1574,11 +1701,11 @@
1574 1701 <strong style="font-size:15px;color:var(--dashboard-text-primary);"><?php echo esc_html($plugin_name); ?></strong>
1575 1702 <?php endif; ?>
1576 1703 </div>
1577 1704 <div class="metasync-header-compact-right">
1578 - <div class="metasync-status <?php echo !empty($api_key) ? 'connected' : 'disconnected'; ?>">
1705 + <div class="metasync-status <?php echo esc_attr($badge['class']); ?>">
1579 1706 <span class="status-dot"></span>
1580 - <span class="status-text"><?php echo !empty($api_key) ? 'Connected' : 'Not Connected'; ?></span>
1707 + <span class="status-text"><?php echo esc_html($badge['text']); ?></span>
1581 1708 </div>
1582 1709 <button type="button" class="metasync-theme-toggle" onclick="toggleMetasyncTheme()" title="Toggle theme">
1583 1710 <span class="theme-icon-light">&#9728;</span>
1584 1711 <span class="theme-icon-dark">&#9790;</span>
@@ -1595,8 +1722,17 @@
1595 1722 </aside>
1596 1723
1597 1724 <!-- Main content -->
1598 1725 <main class="metasync-layout-main">
1726 + <?php
1727 + // Anchor for WP core's notice relocation (common.js). Without it, core
1728 + // moves admin notices after the first .wrap h1 only on DOM-ready, after
1729 + // the server has already painted them at the top of #wpbody-content —
1730 + // causing a flash above the header and layout shift. Providing
1731 + // an explicit .wp-header-end inside this (FOUC-hidden) wrap makes core
1732 + // relocate notices here deterministically, so they fade in with the page.
1733 + ?>
1734 + <hr class="wp-header-end">
1599 1735 <?php if ($page_title || $description): ?>
1600 1736 <div class="metasync-page-header">
1601 1737 <?php if ($page_title): ?>
1602 1738 <h1><?php echo esc_html($page_title); ?></h1>
@@ -1620,15 +1756,19 @@
1620 1756 {
1621 1757 ?>
1622 1758 </main><!-- /.metasync-layout-main -->
1623 1759
1624 - <?php if ($show_promo):
1625 - $general = Metasync::get_option('general') ?? [];
1626 - $is_connected = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general);
1760 + <?php
1761 + // The sidebar exists to get an unconnected site connected. Once the
1762 + // site is linked it has nothing further to offer, so the whole
1763 + // column is dropped and the grid reflows to two columns.
1764 + $show_promo = $show_promo && ! Metasync_Heartbeat_Manager::instance()
1765 + ->is_heartbeat_connected(Metasync::get_option('general') ?? []);
1766 + if ($show_promo):
1627 1767 ?>
1628 1768 <!-- Right promo sidebar -->
1629 1769 <aside class="metasync-layout-promo">
1630 - <?php $this->render_promo_sidebar($is_connected); ?>
1770 + <?php $this->render_promo_sidebar(); ?>
1631 1771 </aside>
1632 1772 <?php endif; ?>
1633 1773
1634 1774 </div><!-- /.metasync-layout -->
@@ -1665,9 +1805,8 @@
1665 1805 'breadcrumbs' => 'menu',
1666 1806 'code_snippets' => 'editor-code',
1667 1807 'code_minification'=> 'media-code',
1668 1808 'custom_pages' => 'admin-page',
1669 - 'optimal_settings' => 'superhero-alt',
1670 1809 'compatibility' => 'admin-tools',
1671 1810 'sync_log' => 'list-view',
1672 1811 'bot_statistics' => 'visibility',
1673 1812 'report_issue' => 'sos',
@@ -1683,11 +1822,20 @@
1683 1822 } else {
1684 1823 $plugin_items[$key] = $item;
1685 1824 }
1686 1825 }
1826 +
1827 + $total_items = count($seo_items) + count($plugin_items);
1828 +
1687 1829 ?>
1688 1830 <nav class="metasync-sidenav">
1689 1831
1832 + <?php if ($total_items === 0): ?>
1833 + <div class="metasync-sidenav-empty">
1834 + <?php esc_html_e('No pages are available.', 'metasync'); ?>
1835 + </div>
1836 + <?php else: ?>
1837 +
1690 1838 <!-- SEO Features group -->
1691 1839 <div class="metasync-sidenav-group">
1692 1840 <div class="metasync-sidenav-group-title">SEO Features</div>
1693 1841 <ul>
@@ -1753,8 +1901,10 @@
1753 1901 </ul>
1754 1902 </div>
1755 1903 <?php endif; ?>
1756 1904
1905 + <?php endif; ?>
1906 +
1757 1907 </nav>
1758 1908 <?php
1759 1909 }
1760 1910
@@ -1760,17 +1910,22 @@
1760 1910
1761 1911 /**
1762 1912 * Render the right promotional sidebar.
1763 1913 *
1764 - * @param bool $is_connected Whether the plugin is authenticated.
1914 + * Only reached while the site is unconnected; the caller drops the
1915 + * whole column once the plugin is linked.
1765 1916 */
1766 - public function render_promo_sidebar($is_connected = false)
1917 + public function render_promo_sidebar()
1767 1918 {
1768 1919 $plugin_name = Metasync::get_effective_plugin_name();
1769 1920 $otto_name = Metasync::get_whitelabel_otto_name();
1770 1921 $settings_url = esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug));
1771 1922 $homepage = Metasync::HOMEPAGE_DOMAIN;
1772 - $is_default_brand = ( $plugin_name === 'Search Atlas' );
1923 + $docs_url = Metasync::DOCUMENTATION_DOMAIN;
1924 + // These links point at Search Atlas properties, so they only belong
1925 + // here while the plugin runs under its own name AND branding — a
1926 + // reseller can leave the name untouched and still be whitelabeled.
1927 + $is_default_brand = ( $plugin_name === 'Search Atlas' ) && ! Metasync::is_whitelabel_enabled();
1773 1928 $whitelabel = Metasync::get_whitelabel_settings();
1774 1929 $custom_links = isset($whitelabel['quick_links']) && is_array($whitelabel['quick_links'])
1775 1930 ? array_filter($whitelabel['quick_links'], function($l) { return !empty($l['url']); })
1776 1931 : [];
@@ -1775,9 +1930,8 @@
1775 1930 ? array_filter($whitelabel['quick_links'], function($l) { return !empty($l['url']); })
1776 1931 : [];
1777 1932 ?>
1778 1933
1779 - <?php if (!$is_connected): ?>
1780 1934 <!-- Connect CTA card -->
1781 1935 <div class="metasync-promo-card metasync-promo-card--connect">
1782 1936 <div class="metasync-promo-card-header">
1783 1937 <div class="metasync-promo-card-icon">
@@ -1797,36 +1951,8 @@
1797 1951 <a href="<?php echo $settings_url; ?>" class="metasync-promo-btn metasync-promo-btn--primary">
1798 1952 Connect Now
1799 1953 </a>
1800 1954 </div>
1801 - <?php else: ?>
1802 - <!-- Connected — feature highlights -->
1803 - <div class="metasync-promo-card metasync-promo-card--accent">
1804 - <div class="metasync-promo-card-header">
1805 - <div class="metasync-promo-card-icon">
1806 - <span class="dashicons dashicons-performance"></span>
1807 - </div>
1808 - <div>
1809 - <h3><?php echo esc_html($otto_name); ?> Active</h3>
1810 - </div>
1811 - </div>
1812 - <p class="metasync-promo-tagline">Your site is connected and <?php echo esc_html($otto_name); ?> is optimizing pages automatically.</p>
1813 - <ul class="metasync-promo-benefits">
1814 - <li><span class="promo-check">&#10003;</span> Schema markup auto-applied</li>
1815 - <li><span class="promo-check">&#10003;</span> Meta titles &amp; descriptions optimized</li>
1816 - <li><span class="promo-check">&#10003;</span> Internal linking suggestions active</li>
1817 - </ul>
1818 - <?php if ($is_default_brand): ?>
1819 - <a href="<?php echo esc_url($homepage); ?>" target="_blank" rel="noopener" class="metasync-promo-btn metasync-promo-btn--outline">
1820 - View <?php echo esc_html($plugin_name); ?> Dashboard
1821 - </a>
1822 - <?php elseif (!empty($whitelabel['domain'])): ?>
1823 - <a href="<?php echo esc_url($whitelabel['domain']); ?>" target="_blank" rel="noopener" class="metasync-promo-btn metasync-promo-btn--outline">
1824 - View <?php echo esc_html($plugin_name); ?> Dashboard
1825 - </a>
1826 - <?php endif; ?>
1827 - </div>
1828 - <?php endif; ?>
1829 1955
1830 1956 <?php
1831 1957 // Quick Links: show default links for default brand, custom links if whitelabeled + provided, hide if whitelabeled + none
1832 1958 $show_quick_links = $is_default_brand || !empty($custom_links);
@@ -1836,12 +1962,20 @@
1836 1962 <div class="metasync-promo-card">
1837 1963 <h3 style="margin:0 0 12px;font-size:13px;font-weight:700;color:var(--dashboard-text-primary);">Quick Links</h3>
1838 1964 <ul class="metasync-promo-links">
1839 1965 <?php if ($is_default_brand): ?>
1966 + <li><a href="<?php echo esc_url($docs_url); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-book-alt"></span> Documentation</a></li>
1840 1967 <li><a href="<?php echo esc_url($homepage . '/blog/'); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-rss"></span> SEO Blog</a></li>
1841 - <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>
1968 + <li><a href="<?php echo esc_url($homepage . '/contact/'); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-email-alt"></span> Contact Support</a></li>
1842 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 + <?php
1971 + // Mirrors the gate that decides whether the page is registered
1972 + // at all. Without it the link outlives the page for any user
1973 + // the access rules hide it from, and leads nowhere.
1974 + if (Metasync_Access_Control::user_can_access('hide_report_issue')):
1975 + ?>
1843 1976 <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>
1977 + <?php endif; ?>
1844 1978 <?php else: ?>
1845 1979 <?php foreach ($custom_links as $link): ?>
1846 1980 <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>
1847 1981 <?php endforeach; ?>