PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.1
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.1
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 +218 -93 2.6.21 → 2.7.1 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 */
@@ -187,12 +286,8 @@
187 286 if (empty($whitelabel_settings['hide_indexation_control'])) {
188 287 $menu_items['seo_controls'] = ['title' => 'Indexation Control', 'slug_suffix' => '-seo-controls'];
189 288 }
190 289
191 - if ($general_options['enable_googleinstantindex'] ?? false) {
192 - $menu_items['instant_index'] = ['title' => 'Instant Indexing', 'slug_suffix' => '-instant-index'];
193 - }
194 -
195 290 if ($general_options['enable_google_console'] ?? false) {
196 291 $menu_items['google_console'] = ['title' => 'Google Console', 'slug_suffix' => '-google-console'];
197 292 }
198 293
@@ -407,20 +502,8 @@
407 502 'group' => 'seo'
408 503 ];
409 504 }
410 505
411 - // Instant Indexing - setting now stored in seo_controls (moved from Settings to Indexation Control)
412 - $seo_controls = Metasync::get_option('seo_controls');
413 - if ($seo_controls['enable_googleinstantindex'] ?? false) {
414 - $menu_items['instant_index'] = [
415 - 'title' => 'Instant Indexing',
416 - 'slug_suffix' => '-instant-index',
417 - 'callback' => 'create_admin_google_instant_index_page',
418 - 'internal_nav' => 'Instant Indexing',
419 - 'group' => 'seo'
420 - ];
421 - }
422 -
423 506 if ($general_options['enable_google_console'] ?? false) {
424 507 $menu_items['google_console'] = [
425 508 'title' => 'Google Console',
426 509 'slug_suffix' => '-google-console',
@@ -662,15 +745,19 @@
662 745 global $menu;
663 746 $menu['100'] = array('', 'read', 'separator-metasync-before', '', 'wp-menu-separator');
664 747 }, 5);
665 748
666 - // 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);
667 754 add_menu_page(
668 755 $menu_name,
669 756 $menu_title,
670 757 $menu_capability,
671 758 $menu_slug,
672 - array($admin, 'create_admin_settings_page'),
759 + $top_level_callback,
673 760 $menu_icon,
674 761 '100.1'
675 762 );
676 763
@@ -684,109 +771,123 @@
684 771
685 772 // ── Connect slug ──────────────────────────────────────────────────
686 773 $connect_slug = $menu_slug . '-connect';
687 774
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 +
688 790 // Dashboard (check access control + "Hide Dashboard" general setting)
689 791 if (Metasync_Access_Control::user_can_access('hide_dashboard') && !self::is_dashboard_hidden_by_framework()) {
690 - add_submenu_page($menu_slug, 'Dashboard', 'Dashboard', $menu_capability, $menu_slug . '-dashboard', array($admin, 'create_admin_dashboard_iframe'));
792 + $register_submenu('dashboard', 'Dashboard', 'Dashboard', $menu_capability, $menu_slug . '-dashboard', array($admin, 'create_admin_dashboard_iframe'));
691 793 }
692 794
693 795 // Indexation Control
694 796 if (Metasync_Access_Control::user_can_access('hide_indexation_control')) {
695 - 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'));
696 798 }
697 799
698 800 // 404 Monitor
699 - 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'));
700 802
701 803 // Redirections
702 804 if (Metasync_Access_Control::user_can_access('hide_redirections')) {
703 - 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'));
704 806 }
705 807
706 808 // XML Sitemap
707 809 if (Metasync_Access_Control::user_can_access('hide_xml_sitemap')) {
708 - 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'));
709 811 }
710 812
711 813 // Robots.txt
712 814 if (Metasync_Access_Control::user_can_access('hide_robots')) {
713 - 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'));
714 816 }
715 817
716 - // Site Verification
717 - 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'));
718 820
719 - // Instant Indexing (conditional)
720 - if ($seo_controls['enable_googleinstantindex'] ?? false) {
721 - add_submenu_page($menu_slug, 'Instant Indexing', 'Instant Indexing', $menu_capability, $menu_slug . '-instant-index', array($admin, 'create_admin_google_instant_index_page'));
722 - }
723 -
724 821 // Google Console (conditional)
725 822 if ($general_options['enable_google_console'] ?? false) {
726 - 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'));
727 824 }
728 825
729 826 // Bing Console (conditional)
730 827 if ($seo_controls['enable_binginstantindex'] ?? false) {
731 - 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'));
732 829 }
733 830
734 831 // Schema Markup
735 - 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'));
736 833
737 834 // Import SEO Data
738 835 if (Metasync_Access_Control::user_can_access('hide_import_seo')) {
739 - 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'));
740 837 }
741 838
742 - // 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()).
743 842 if (Metasync_Access_Control::user_can_access('hide_settings')) {
744 - 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'));
745 844 }
746 845
747 - // Local Business
748 - 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'));
749 848
750 - // Breadcrumbs
751 - 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'));
752 851
753 - // Code Snippets
754 - 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'));
755 854
756 - // Code Minification
757 - 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'));
758 857
759 - // Media Optimization
760 - add_submenu_page($menu_slug, 'Media Optimization', 'Media Optimization', $menu_capability, $menu_slug . '-media-optimization', array($admin, 'create_admin_media_optimization_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'));
761 860
762 861 // Custom Pages
763 862 if (Metasync_Access_Control::user_can_access('hide_custom_pages')) {
764 - 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'));
765 864 }
766 865
767 - // Bot Statistics
768 - 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'));
769 868
770 869 // SEO Health dashboard
771 - 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'));
772 871
773 872 // Compatibility
774 873 if (Metasync_Access_Control::user_can_access('hide_compatibility')) {
775 - 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'));
776 875 }
777 876
778 877 // Changes Log
779 878 if (Metasync_Access_Control::user_can_access('hide_sync_log')) {
780 - 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'));
781 880 }
782 881
783 882 // Report Issue
784 883 if (Metasync_Access_Control::user_can_access('hide_report_issue')) {
785 - 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'));
786 885 }
787 886
788 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.
789 890 if (!$is_fully_connected) {
790 891 $connect_label = sprintf('Connect to %s', Metasync::get_effective_plugin_name());
791 892 add_submenu_page($menu_slug, $connect_label, $connect_label, $menu_capability, $connect_slug, array($admin, 'create_admin_settings_page'));
792 893 }
@@ -791,11 +892,36 @@
791 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']) // 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 + : '';
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', 'metasync'),
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])) {
@@ -1331,10 +1457,10 @@
1331 1457
1332 1458 <?php
1333 1459 $page_slug = Metasync_Admin::$page_slug;
1334 1460 $is_plugin_page = (
1335 - (isset($_GET['page']) && strpos($_GET['page'], $page_slug) !== false) ||
1336 - (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
1337 1463 );
1338 1464 if ($is_plugin_page):
1339 1465 ?>
1340 1466 <script type="text/javascript">
@@ -1630,15 +1756,19 @@
1630 1756 {
1631 1757 ?>
1632 1758 </main><!-- /.metasync-layout-main -->
1633 1759
1634 - <?php if ($show_promo):
1635 - $general = Metasync::get_option('general') ?? [];
1636 - $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):
1637 1767 ?>
1638 1768 <!-- Right promo sidebar -->
1639 1769 <aside class="metasync-layout-promo">
1640 - <?php $this->render_promo_sidebar($is_connected); ?>
1770 + <?php $this->render_promo_sidebar(); ?>
1641 1771 </aside>
1642 1772 <?php endif; ?>
1643 1773
1644 1774 </div><!-- /.metasync-layout -->
@@ -1692,11 +1822,20 @@
1692 1822 } else {
1693 1823 $plugin_items[$key] = $item;
1694 1824 }
1695 1825 }
1826 +
1827 + $total_items = count($seo_items) + count($plugin_items);
1828 +
1696 1829 ?>
1697 1830 <nav class="metasync-sidenav">
1698 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 +
1699 1838 <!-- SEO Features group -->
1700 1839 <div class="metasync-sidenav-group">
1701 1840 <div class="metasync-sidenav-group-title">SEO Features</div>
1702 1841 <ul>
@@ -1762,8 +1901,10 @@
1762 1901 </ul>
1763 1902 </div>
1764 1903 <?php endif; ?>
1765 1904
1905 + <?php endif; ?>
1906 +
1766 1907 </nav>
1767 1908 <?php
1768 1909 }
1769 1910
@@ -1769,17 +1910,22 @@
1769 1910
1770 1911 /**
1771 1912 * Render the right promotional sidebar.
1772 1913 *
1773 - * @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.
1774 1916 */
1775 - public function render_promo_sidebar($is_connected = false)
1917 + public function render_promo_sidebar()
1776 1918 {
1777 1919 $plugin_name = Metasync::get_effective_plugin_name();
1778 1920 $otto_name = Metasync::get_whitelabel_otto_name();
1779 1921 $settings_url = esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug));
1780 1922 $homepage = Metasync::HOMEPAGE_DOMAIN;
1781 - $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();
1782 1928 $whitelabel = Metasync::get_whitelabel_settings();
1783 1929 $custom_links = isset($whitelabel['quick_links']) && is_array($whitelabel['quick_links'])
1784 1930 ? array_filter($whitelabel['quick_links'], function($l) { return !empty($l['url']); })
1785 1931 : [];
@@ -1784,9 +1930,8 @@
1784 1930 ? array_filter($whitelabel['quick_links'], function($l) { return !empty($l['url']); })
1785 1931 : [];
1786 1932 ?>
1787 1933
1788 - <?php if (!$is_connected): ?>
1789 1934 <!-- Connect CTA card -->
1790 1935 <div class="metasync-promo-card metasync-promo-card--connect">
1791 1936 <div class="metasync-promo-card-header">
1792 1937 <div class="metasync-promo-card-icon">
@@ -1806,36 +1951,8 @@
1806 1951 <a href="<?php echo $settings_url; ?>" class="metasync-promo-btn metasync-promo-btn--primary">
1807 1952 Connect Now
1808 1953 </a>
1809 1954 </div>
1810 - <?php else: ?>
1811 - <!-- Connected — feature highlights -->
1812 - <div class="metasync-promo-card metasync-promo-card--accent">
1813 - <div class="metasync-promo-card-header">
1814 - <div class="metasync-promo-card-icon">
1815 - <span class="dashicons dashicons-performance"></span>
1816 - </div>
1817 - <div>
1818 - <h3><?php echo esc_html($otto_name); ?> Active</h3>
1819 - </div>
1820 - </div>
1821 - <p class="metasync-promo-tagline">Your site is connected and <?php echo esc_html($otto_name); ?> is optimizing pages automatically.</p>
1822 - <ul class="metasync-promo-benefits">
1823 - <li><span class="promo-check">&#10003;</span> Schema markup auto-applied</li>
1824 - <li><span class="promo-check">&#10003;</span> Meta titles &amp; descriptions optimized</li>
1825 - <li><span class="promo-check">&#10003;</span> Internal linking suggestions active</li>
1826 - </ul>
1827 - <?php if ($is_default_brand): ?>
1828 - <a href="<?php echo esc_url($homepage); ?>" target="_blank" rel="noopener" class="metasync-promo-btn metasync-promo-btn--outline">
1829 - View <?php echo esc_html($plugin_name); ?> Dashboard
1830 - </a>
1831 - <?php elseif (!empty($whitelabel['domain'])): ?>
1832 - <a href="<?php echo esc_url($whitelabel['domain']); ?>" target="_blank" rel="noopener" class="metasync-promo-btn metasync-promo-btn--outline">
1833 - View <?php echo esc_html($plugin_name); ?> Dashboard
1834 - </a>
1835 - <?php endif; ?>
1836 - </div>
1837 - <?php endif; ?>
1838 1955
1839 1956 <?php
1840 1957 // Quick Links: show default links for default brand, custom links if whitelabeled + provided, hide if whitelabeled + none
1841 1958 $show_quick_links = $is_default_brand || !empty($custom_links);
@@ -1845,12 +1962,20 @@
1845 1962 <div class="metasync-promo-card">
1846 1963 <h3 style="margin:0 0 12px;font-size:13px;font-weight:700;color:var(--dashboard-text-primary);">Quick Links</h3>
1847 1964 <ul class="metasync-promo-links">
1848 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>
1849 1967 <li><a href="<?php echo esc_url($homepage . '/blog/'); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-rss"></span> SEO Blog</a></li>
1850 - <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>
1851 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 + ?>
1852 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; ?>
1853 1978 <?php else: ?>
1854 1979 <?php foreach ($custom_links as $link): ?>
1855 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>
1856 1981 <?php endforeach; ?>