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 +300 -146 2.6.7 → 2.7.2 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
@@ -41,9 +156,9 @@
41 156
42 157 /**
43 158 * Resolve which logo(s) to display based on whitelabel settings.
44 159 *
45 - * @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}
46 161 */
47 162 private function resolve_logo_data()
48 163 {
49 164 $wl_settings = Metasync::get_whitelabel_settings();
@@ -55,13 +170,14 @@
55 170 $has_dark = !empty($dark_raw) && filter_var($dark_raw, FILTER_VALIDATE_URL);
56 171 $use_dual = ($has_light && $has_dark && $light_raw !== $dark_raw);
57 172
58 173 $data = [
59 - 'show_logo' => false,
60 - 'use_dual' => false,
61 - 'url' => '',
62 - 'light_url' => '',
63 - 'dark_url' => '',
174 + 'show_logo' => false,
175 + 'use_dual' => false,
176 + 'url' => '',
177 + 'light_url' => '',
178 + 'dark_url' => '',
179 + 'is_default' => false,
64 180 ];
65 181
66 182 if ($use_dual) {
67 183 $data['show_logo'] = true;
@@ -73,10 +189,11 @@
73 189 if ($single) {
74 190 $data['show_logo'] = true;
75 191 $data['url'] = $single;
76 192 } elseif (!$is_whitelabel) {
77 - $data['show_logo'] = true;
78 - $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;
79 196 }
80 197 }
81 198
82 199 return $data;
@@ -105,10 +222,11 @@
105 222 $logo = $this->resolve_logo_data();
106 223
107 224 $current_theme = get_option('metasync_theme', 'dark');
108 225 $general_settings = Metasync::get_option('general');
109 - $searchatlas_api_key = isset($general_settings['searchatlas_api_key']) ? $general_settings['searchatlas_api_key'] : '';
110 - $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);
111 229 ?>
112 230 <div class="metasync-header" data-current-theme="<?php echo esc_attr($current_theme); ?>">
113 231 <div class="metasync-header-left">
114 232 <?php if ($logo['show_logo'] && $logo['use_dual']): ?>
@@ -117,16 +235,16 @@
117 235 <img src="<?php echo esc_url($logo['dark_url']); ?>" alt="Logo" class="metasync-logo metasync-logo-dark" />
118 236 </div>
119 237 <?php elseif ($logo['show_logo'] && !empty($logo['url'])): ?>
120 238 <div class="metasync-logo-container">
121 - <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' : ''; ?>" />
122 240 </div>
123 241 <?php endif; ?>
124 242 </div>
125 243 <div class="metasync-header-right">
126 - <div class="metasync-status <?php echo $is_integrated ? 'connected' : 'disconnected'; ?>">
244 + <div class="metasync-status <?php echo esc_attr($badge['class']); ?>">
127 245 <span class="status-dot"></span>
128 - <span class="status-text"><?php echo $is_integrated ? 'Connected' : 'Not Connected'; ?></span>
246 + <span class="status-text"><?php echo esc_html($badge['text']); ?></span>
129 247 </div>
130 248 <button type="button" class="metasync-theme-toggle" onclick="toggleMetasyncTheme()" title="Toggle theme">
131 249 <span class="theme-icon-light">&#9728;</span>
132 250 <span class="theme-icon-dark">&#9790;</span>
@@ -148,9 +266,8 @@
148 266 $menu_items = [];
149 267 $menu_icons = [
150 268 'dashboard' => 'dashboard',
151 269 'seo_controls' => 'search',
152 - 'optimal_settings' => 'superhero-alt',
153 270 'instant_index' => 'performance',
154 271 'google_console' => 'chart-area',
155 272 'compatibility' => 'admin-tools',
156 273 'sync_log' => 'list-view',
@@ -161,9 +278,9 @@
161 278 'report_issue' => 'sos',
162 279 'general' => 'admin-settings',
163 280 ];
164 281
165 - if (empty($whitelabel_settings['hide_dashboard'])) {
282 + if (empty($whitelabel_settings['hide_dashboard']) && !self::is_dashboard_hidden_by_framework()) {
166 283 $menu_items['dashboard'] = ['title' => 'Dashboard', 'slug_suffix' => '-dashboard'];
167 284 }
168 285
169 286 if (empty($whitelabel_settings['hide_indexation_control'])) {
@@ -168,17 +285,9 @@
168 285
169 286 if (empty($whitelabel_settings['hide_indexation_control'])) {
170 287 $menu_items['seo_controls'] = ['title' => 'Indexation Control', 'slug_suffix' => '-seo-controls'];
171 288 }
172 -
173 - if ($general_options['enable_optimal_settings'] ?? false) {
174 - $menu_items['optimal_settings'] = ['title' => 'Optimal Settings', 'slug_suffix' => '-optimal-settings'];
175 - }
176 -
177 - if ($general_options['enable_googleinstantindex'] ?? false) {
178 - $menu_items['instant_index'] = ['title' => 'Instant Indexing', 'slug_suffix' => '-instant-index'];
179 - }
180 -
289 +
181 290 if ($general_options['enable_google_console'] ?? false) {
182 291 $menu_items['google_console'] = ['title' => 'Google Console', 'slug_suffix' => '-google-console'];
183 292 }
184 293
@@ -204,9 +313,9 @@
204 313 <div class="metasync-nav-tabs">
205 314 <div class="metasync-nav-left">
206 315 <?php foreach ($menu_items as $key => $menu_item):
207 316 $is_active = ($current_page === $key);
208 - $icon = $menu_icons[$key] ?? 'admin-generic';
317 + $icon = $menu_icons[$key];
209 318 $page_url = '?page=' . $page_slug . $menu_item['slug_suffix'];
210 319 ?>
211 320 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-tab <?php echo $is_active ? 'active' : ''; ?>">
212 321 <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
@@ -371,10 +480,10 @@
371 480 $menu_items = [];
372 481
373 482 // === SEO FEATURES GROUP ===
374 483
375 - // Dashboard (check access control)
376 - 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()) {
377 486 $menu_items['dashboard'] = [
378 487 'title' => 'Dashboard',
379 488 'slug_suffix' => '-dashboard',
380 489 'callback' => 'create_admin_dashboard_iframe',
@@ -393,20 +502,8 @@
393 502 'group' => 'seo'
394 503 ];
395 504 }
396 505
397 - // Instant Indexing - setting now stored in seo_controls (moved from Settings to Indexation Control)
398 - $seo_controls = Metasync::get_option('seo_controls');
399 - if ($seo_controls['enable_googleinstantindex'] ?? false) {
400 - $menu_items['instant_index'] = [
401 - 'title' => 'Instant Indexing',
402 - 'slug_suffix' => '-instant-index',
403 - 'callback' => 'create_admin_google_instant_index_page',
404 - 'internal_nav' => 'Instant Indexing',
405 - 'group' => 'seo'
406 - ];
407 - }
408 -
409 506 if ($general_options['enable_google_console'] ?? false) {
410 507 $menu_items['google_console'] = [
411 508 'title' => 'Google Console',
412 509 'slug_suffix' => '-google-console',
@@ -554,8 +651,17 @@
554 651 'internal_nav' => 'Code Minification',
555 652 'group' => 'plugin'
556 653 ];
557 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 +
558 664 // Custom HTML Pages (check access control)
559 665 if (Metasync_Access_Control::user_can_access('hide_custom_pages')) {
560 666 $menu_items['custom_pages'] = [
561 667 'title' => 'Custom Pages',
@@ -565,18 +671,8 @@
565 671 'group' => 'plugin'
566 672 ];
567 673 }
568 674
569 - if ($general_options['enable_optimal_settings'] ?? false) {
570 - $menu_items['optimal_settings'] = [
571 - 'title' => 'Optimal Settings',
572 - 'slug_suffix' => '-optimal-settings',
573 - 'callback' => 'create_admin_optimal_settings_page',
574 - 'internal_nav' => 'Optimal Settings',
575 - 'group' => 'plugin'
576 - ];
577 - }
578 -
579 675 // Compatibility page (check access control)
580 676 if (Metasync_Access_Control::user_can_access('hide_compatibility')) {
581 677 $menu_items['compatibility'] = [
582 678 'title' => 'Compatibility',
@@ -635,9 +731,11 @@
635 731 $data = Metasync::get_option('general');
636 732 $plugin_name = Metasync::get_effective_plugin_name();
637 733 $menu_name = $plugin_name;
638 734 $menu_title = $plugin_name;
639 - $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;
640 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'];
641 739
642 740 // Use 'read' capability since actual access is controlled by current_user_has_plugin_access() check above
643 741 $menu_capability = 'read';
@@ -647,15 +745,19 @@
647 745 global $menu;
648 746 $menu['100'] = array('', 'read', 'separator-metasync-before', '', 'wp-menu-separator');
649 747 }, 5);
650 748
651 - // 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);
652 754 add_menu_page(
653 755 $menu_name,
654 756 $menu_title,
655 757 $menu_capability,
656 758 $menu_slug,
657 - array($admin, 'create_admin_settings_page'),
759 + $top_level_callback,
658 760 $menu_icon,
659 761 '100.1'
660 762 );
661 763
@@ -669,114 +771,157 @@
669 771
670 772 // ── Connect slug ──────────────────────────────────────────────────
671 773 $connect_slug = $menu_slug . '-connect';
672 774
673 - // Dashboard
674 - if (Metasync_Access_Control::user_can_access('hide_dashboard')) {
675 - 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'));
676 793 }
677 794
678 795 // Indexation Control
679 796 if (Metasync_Access_Control::user_can_access('hide_indexation_control')) {
680 - 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'));
681 798 }
682 799
683 800 // 404 Monitor
684 - 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'));
685 802
686 803 // Redirections
687 804 if (Metasync_Access_Control::user_can_access('hide_redirections')) {
688 - 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'));
689 806 }
690 807
691 808 // XML Sitemap
692 809 if (Metasync_Access_Control::user_can_access('hide_xml_sitemap')) {
693 - 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'));
694 811 }
695 812
696 813 // Robots.txt
697 814 if (Metasync_Access_Control::user_can_access('hide_robots')) {
698 - 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'));
699 816 }
700 817
701 - // Site Verification
702 - 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'));
703 820
704 - // Instant Indexing (conditional)
705 - if ($seo_controls['enable_googleinstantindex'] ?? false) {
706 - add_submenu_page($menu_slug, 'Instant Indexing', 'Instant Indexing', $menu_capability, $menu_slug . '-instant-index', array($admin, 'create_admin_google_instant_index_page'));
707 - }
708 -
709 821 // Google Console (conditional)
710 822 if ($general_options['enable_google_console'] ?? false) {
711 - 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'));
712 824 }
713 825
714 826 // Bing Console (conditional)
715 827 if ($seo_controls['enable_binginstantindex'] ?? false) {
716 - 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'));
717 829 }
718 830
719 831 // Schema Markup
720 - 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'));
721 833
722 834 // Import SEO Data
723 835 if (Metasync_Access_Control::user_can_access('hide_import_seo')) {
724 - 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'));
725 837 }
726 838
727 - // 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()).
728 842 if (Metasync_Access_Control::user_can_access('hide_settings')) {
729 - 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'));
730 844 }
731 845
732 - // Local Business
733 - 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'));
734 848
735 - // Breadcrumbs
736 - 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'));
737 851
738 - // Code Snippets
739 - 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'));
740 854
741 - // Code Minification
742 - 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'));
743 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 +
744 861 // Custom Pages
745 862 if (Metasync_Access_Control::user_can_access('hide_custom_pages')) {
746 - 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'));
747 864 }
748 865
749 - // Bot Statistics
750 - 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'));
751 868
752 869 // SEO Health dashboard
753 - 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'));
754 871
755 872 // Compatibility
756 873 if (Metasync_Access_Control::user_can_access('hide_compatibility')) {
757 - 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'));
758 875 }
759 876
760 877 // Changes Log
761 878 if (Metasync_Access_Control::user_can_access('hide_sync_log')) {
762 - 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'));
763 880 }
764 881
765 882 // Report Issue
766 883 if (Metasync_Access_Control::user_can_access('hide_report_issue')) {
767 - 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'));
768 885 }
769 886
770 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.
771 890 if (!$is_fully_connected) {
772 - 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'));
773 893 }
774 894
775 895 // ── Hidden pages (no sidebar entry needed) ────────────────────────
896 + $admin_page_titles[$menu_slug . '-setup-wizard'] = 'Setup Wizard';
776 897 add_submenu_page('', 'Setup Wizard', 'Setup Wizard', $menu_capability, $menu_slug . '-setup-wizard', array($admin->setup_wizard, 'render_wizard_page'));
777 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 + : '';
778 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 +
779 924 // Rename auto-generated first submenu from plugin name to "Settings" and reorder
780 925 add_action('admin_menu', function() use ($menu_slug) {
781 926 global $submenu;
782 927 if (!isset($submenu[$menu_slug])) {
@@ -806,9 +951,8 @@
806 951 'dashboard' => 'dashboard',
807 952 'compatibility' => 'admin-tools',
808 953 'sync_log' => 'list-view',
809 954 'seo_controls' => 'search',
810 - 'optimal_settings' => 'superhero-alt',
811 955 'instant_index' => 'performance',
812 956 'google_console' => 'chart-area',
813 957 'bing_console' => 'chart-bar',
814 958 'redirections' => 'undo',
@@ -848,9 +992,9 @@
848 992 <?php
849 993 // Dashboard tab (standalone)
850 994 if (isset($seo_items['dashboard'])) {
851 995 $is_active = ($current_page === 'dashboard');
852 - $icon = $menu_icons['dashboard'] ?? 'admin-generic';
996 + $icon = $menu_icons['dashboard'];
853 997 $page_url = '?page=' . $page_slug . $seo_items['dashboard']['slug_suffix'];
854 998 ?>
855 999 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-tab <?php echo $is_active ? 'active' : ''; ?>">
856 1000 <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
@@ -1197,13 +1341,13 @@
1197 1341 <img src="<?php echo esc_url($logo['dark_url']); ?>" alt="Logo" class="metasync-logo metasync-logo-dark" />
1198 1342 </div>
1199 1343 <?php elseif ($logo['show_logo'] && !empty($logo['url'])): ?>
1200 1344 <div class="metasync-logo-container">
1201 - <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' : ''; ?>" />
1202 1346 </div>
1203 1347 <?php endif; ?>
1204 1348 </div>
1205 -
1349 +
1206 1350 <div class="metasync-header-right">
1207 1351 <!-- Theme Toggle -->
1208 1352 <div class="metasync-theme-toggle" role="group" aria-label="Theme Selector">
1209 1353 <button class="metasync-theme-option <?php echo ($current_theme === 'light') ? 'active' : ''; ?>" data-theme="light" aria-label="Light Theme" type="button">
@@ -1231,9 +1375,9 @@
1231 1375 $status_title_header = 'Not Synced - Heartbeat API not responding or unreachable';
1232 1376 $status_text_header = 'Not Synced';
1233 1377 }
1234 1378 ?>
1235 - <div class="metasync-integration-status <?php echo $status_class_header; ?>"
1379 + <div class="metasync-integration-status <?php echo esc_attr($status_class_header); ?>"
1236 1380 title="<?php echo esc_attr($status_title_header); ?>">
1237 1381 <span class="status-indicator"></span>
1238 1382 <span class="status-text"><?php echo esc_html($status_text_header); ?></span>
1239 1383 </div>
@@ -1313,10 +1457,10 @@
1313 1457
1314 1458 <?php
1315 1459 $page_slug = Metasync_Admin::$page_slug;
1316 1460 $is_plugin_page = (
1317 - (isset($_GET['page']) && strpos($_GET['page'], $page_slug) !== false) ||
1318 - (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
1319 1463 );
1320 1464 if ($is_plugin_page):
1321 1465 ?>
1322 1466 <script type="text/javascript">
@@ -1493,18 +1637,20 @@
1493 1637 * @param string $description Optional subtitle shown below the title.
1494 1638 */
1495 1639 public function render_layout_open($page_title = '', $current_page = '', $description = '')
1496 1640 {
1497 - $theme = esc_attr(get_option('metasync_theme', 'dark'));
1641 + $theme = get_option('metasync_theme', 'dark');
1498 1642 $plugin_name = Metasync::get_effective_plugin_name();
1499 1643 $general = Metasync::get_option('general') ?? [];
1500 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);
1501 1648 $logo = $this->resolve_logo_data();
1502 1649
1503 1650 $current_theme = get_option('metasync_theme', 'dark');
1504 - $api_key = $general['searchatlas_api_key'] ?? '';
1505 1651 ?>
1506 - <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); ?>">
1507 1653
1508 1654 <?php
1509 1655 // Inject whitelabel color palette overrides
1510 1656 $wl_settings = Metasync::get_whitelabel_settings();
@@ -1534,8 +1680,9 @@
1534 1680 $vars .= '--' . esc_attr($grad_var) . ':linear-gradient(135deg,' . esc_attr($from) . ' 0%,' . esc_attr($to) . ' 100%);';
1535 1681 }
1536 1682 }
1537 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
1538 1685 echo $selector . '{' . $vars . '}';
1539 1686 }
1540 1687 }
1541 1688 }
@@ -1549,17 +1696,17 @@
1549 1696 <?php if ($logo['show_logo'] && $logo['use_dual']): ?>
1550 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;">
1551 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;">
1552 1699 <?php elseif ($logo['show_logo'] && $logo['url']): ?>
1553 - <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;">
1554 1701 <?php else: ?>
1555 1702 <strong style="font-size:15px;color:var(--dashboard-text-primary);"><?php echo esc_html($plugin_name); ?></strong>
1556 1703 <?php endif; ?>
1557 1704 </div>
1558 1705 <div class="metasync-header-compact-right">
1559 - <div class="metasync-status <?php echo !empty($api_key) ? 'connected' : 'disconnected'; ?>">
1706 + <div class="metasync-status <?php echo esc_attr($badge['class']); ?>">
1560 1707 <span class="status-dot"></span>
1561 - <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>
1562 1709 </div>
1563 1710 <button type="button" class="metasync-theme-toggle" onclick="toggleMetasyncTheme()" title="Toggle theme">
1564 1711 <span class="theme-icon-light">&#9728;</span>
1565 1712 <span class="theme-icon-dark">&#9790;</span>
@@ -1576,8 +1723,17 @@
1576 1723 </aside>
1577 1724
1578 1725 <!-- Main content -->
1579 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">
1580 1736 <?php if ($page_title || $description): ?>
1581 1737 <div class="metasync-page-header">
1582 1738 <?php if ($page_title): ?>
1583 1739 <h1><?php echo esc_html($page_title); ?></h1>
@@ -1601,15 +1757,19 @@
1601 1757 {
1602 1758 ?>
1603 1759 </main><!-- /.metasync-layout-main -->
1604 1760
1605 - <?php if ($show_promo):
1606 - $general = Metasync::get_option('general') ?? [];
1607 - $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):
1608 1768 ?>
1609 1769 <!-- Right promo sidebar -->
1610 1770 <aside class="metasync-layout-promo">
1611 - <?php $this->render_promo_sidebar($is_connected); ?>
1771 + <?php $this->render_promo_sidebar(); ?>
1612 1772 </aside>
1613 1773 <?php endif; ?>
1614 1774
1615 1775 </div><!-- /.metasync-layout -->
@@ -1646,9 +1806,8 @@
1646 1806 'breadcrumbs' => 'menu',
1647 1807 'code_snippets' => 'editor-code',
1648 1808 'code_minification'=> 'media-code',
1649 1809 'custom_pages' => 'admin-page',
1650 - 'optimal_settings' => 'superhero-alt',
1651 1810 'compatibility' => 'admin-tools',
1652 1811 'sync_log' => 'list-view',
1653 1812 'bot_statistics' => 'visibility',
1654 1813 'report_issue' => 'sos',
@@ -1664,11 +1823,20 @@
1664 1823 } else {
1665 1824 $plugin_items[$key] = $item;
1666 1825 }
1667 1826 }
1827 +
1828 + $total_items = count($seo_items) + count($plugin_items);
1829 +
1668 1830 ?>
1669 1831 <nav class="metasync-sidenav">
1670 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 +
1671 1839 <!-- SEO Features group -->
1672 1840 <div class="metasync-sidenav-group">
1673 1841 <div class="metasync-sidenav-group-title">SEO Features</div>
1674 1842 <ul>
@@ -1674,12 +1842,12 @@
1674 1842 <ul>
1675 1843 <?php foreach ($seo_items as $key => $item):
1676 1844 $is_active = ($current_page === $key);
1677 1845 $icon = $icons[$key] ?? 'admin-generic';
1678 - $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']);
1679 1847 ?>
1680 1848 <li class="<?php echo $is_active ? 'metasync-sidenav-active' : ''; ?>">
1681 - <a href="<?php echo $url; ?>">
1849 + <a href="<?php echo esc_url($url); ?>">
1682 1850 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
1683 1851 <?php echo esc_html($item['title']); ?>
1684 1852 </a>
1685 1853 </li>
@@ -1694,12 +1862,12 @@
1694 1862 <?php foreach ($plugin_items as $key => $item):
1695 1863 if ($key === 'report_issue') continue;
1696 1864 $is_active = ($current_page === $key);
1697 1865 $icon = $icons[$key] ?? 'admin-generic';
1698 - $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']);
1699 1867 ?>
1700 1868 <li class="<?php echo $is_active ? 'metasync-sidenav-active' : ''; ?>">
1701 - <a href="<?php echo $url; ?>">
1869 + <a href="<?php echo esc_url($url); ?>">
1702 1870 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
1703 1871 <?php echo esc_html($item['title']); ?>
1704 1872 </a>
1705 1873 </li>
@@ -1734,8 +1902,10 @@
1734 1902 </ul>
1735 1903 </div>
1736 1904 <?php endif; ?>
1737 1905
1906 + <?php endif; ?>
1907 +
1738 1908 </nav>
1739 1909 <?php
1740 1910 }
1741 1911
@@ -1741,17 +1911,22 @@
1741 1911
1742 1912 /**
1743 1913 * Render the right promotional sidebar.
1744 1914 *
1745 - * @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.
1746 1917 */
1747 - public function render_promo_sidebar($is_connected = false)
1918 + public function render_promo_sidebar()
1748 1919 {
1749 1920 $plugin_name = Metasync::get_effective_plugin_name();
1750 1921 $otto_name = Metasync::get_whitelabel_otto_name();
1751 - $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);
1752 1923 $homepage = Metasync::HOMEPAGE_DOMAIN;
1753 - $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();
1754 1929 $whitelabel = Metasync::get_whitelabel_settings();
1755 1930 $custom_links = isset($whitelabel['quick_links']) && is_array($whitelabel['quick_links'])
1756 1931 ? array_filter($whitelabel['quick_links'], function($l) { return !empty($l['url']); })
1757 1932 : [];
@@ -1756,9 +1931,8 @@
1756 1931 ? array_filter($whitelabel['quick_links'], function($l) { return !empty($l['url']); })
1757 1932 : [];
1758 1933 ?>
1759 1934
1760 - <?php if (!$is_connected): ?>
1761 1935 <!-- Connect CTA card -->
1762 1936 <div class="metasync-promo-card metasync-promo-card--connect">
1763 1937 <div class="metasync-promo-card-header">
1764 1938 <div class="metasync-promo-card-icon">
@@ -1774,40 +1948,12 @@
1774 1948 <li><span class="promo-check">&#10003;</span> Real-time keyword tracking</li>
1775 1949 <li><span class="promo-check">&#10003;</span> Automated schema markup</li>
1776 1950 <li><span class="promo-check">&#10003;</span> Instant Google indexing</li>
1777 1951 </ul>
1778 - <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">
1779 1953 Connect Now
1780 1954 </a>
1781 1955 </div>
1782 - <?php else: ?>
1783 - <!-- Connected — feature highlights -->
1784 - <div class="metasync-promo-card metasync-promo-card--accent">
1785 - <div class="metasync-promo-card-header">
1786 - <div class="metasync-promo-card-icon">
1787 - <span class="dashicons dashicons-performance"></span>
1788 - </div>
1789 - <div>
1790 - <h3><?php echo esc_html($otto_name); ?> Active</h3>
1791 - </div>
1792 - </div>
1793 - <p class="metasync-promo-tagline">Your site is connected and <?php echo esc_html($otto_name); ?> is optimizing pages automatically.</p>
1794 - <ul class="metasync-promo-benefits">
1795 - <li><span class="promo-check">&#10003;</span> Schema markup auto-applied</li>
1796 - <li><span class="promo-check">&#10003;</span> Meta titles &amp; descriptions optimized</li>
1797 - <li><span class="promo-check">&#10003;</span> Internal linking suggestions active</li>
1798 - </ul>
1799 - <?php if ($is_default_brand): ?>
1800 - <a href="<?php echo esc_url($homepage); ?>" target="_blank" rel="noopener" class="metasync-promo-btn metasync-promo-btn--outline">
1801 - View <?php echo esc_html($plugin_name); ?> Dashboard
1802 - </a>
1803 - <?php elseif (!empty($whitelabel['domain'])): ?>
1804 - <a href="<?php echo esc_url($whitelabel['domain']); ?>" target="_blank" rel="noopener" class="metasync-promo-btn metasync-promo-btn--outline">
1805 - View <?php echo esc_html($plugin_name); ?> Dashboard
1806 - </a>
1807 - <?php endif; ?>
1808 - </div>
1809 - <?php endif; ?>
1810 1956
1811 1957 <?php
1812 1958 // Quick Links: show default links for default brand, custom links if whitelabeled + provided, hide if whitelabeled + none
1813 1959 $show_quick_links = $is_default_brand || !empty($custom_links);
@@ -1817,12 +1963,20 @@
1817 1963 <div class="metasync-promo-card">
1818 1964 <h3 style="margin:0 0 12px;font-size:13px;font-weight:700;color:var(--dashboard-text-primary);">Quick Links</h3>
1819 1965 <ul class="metasync-promo-links">
1820 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>
1821 1968 <li><a href="<?php echo esc_url($homepage . '/blog/'); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-rss"></span> SEO Blog</a></li>
1822 - <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>
1823 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 + ?>
1824 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; ?>
1825 1979 <?php else: ?>
1826 1980 <?php foreach ($custom_links as $link): ?>
1827 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>
1828 1982 <?php endforeach; ?>