| @@ -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">☀</span> |
| 134 | 250 | <span class="theme-icon-dark">☾</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 ‹ %2$s — 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">☀</span> |
| 1584 | 1711 | <span class="theme-icon-dark">☾</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">✓</span> Schema markup auto-applied</li> | |
| 1815 | - <li><span class="promo-check">✓</span> Meta titles & descriptions optimized</li> | |
| 1816 | - <li><span class="promo-check">✓</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; ?> |