"Home" screen) widget - a * compact SEO snapshot so admins see traffic + health at a glance without * opening the AWBSEO menu. Reuses AWBSEO_Admin::get_dashboard_data() so the * numbers here always match the full AWBSEO Dashboard page exactly - one * source of truth, no separate query path to drift out of sync. * * Widget CSS is enqueued via admin-inline.css (loaded on all admin pages). */ class AWBSEO_Dashboard_Widget { public static function init() { add_action( 'wp_dashboard_setup', [ __CLASS__, 'add_widget' ] ); // Run at priority 999 so all other widgets are registered first, // then move ours to the top of the $wp_meta_boxes array. add_action( 'wp_dashboard_setup', [ __CLASS__, 'move_to_top' ], 999 ); // Also reorder any saved user positions so returning users see it first too. add_filter( 'get_user_option_meta-box-order_dashboard', [ __CLASS__, 'force_top_position' ] ); add_action( 'admin_enqueue_scripts', [ __CLASS__, 'enqueue_styles' ] ); } public static function enqueue_styles( $hook ) { if ( 'index.php' !== $hook ) { return; } wp_register_style( 'awbseo-dashboard-widget', false, [], AWBSEO_VERSION ); wp_enqueue_style( 'awbseo-dashboard-widget' ); wp_add_inline_style( 'awbseo-dashboard-widget', '.awb-dw-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:14px}' . '.awb-dw-stat{background:#f6f7f7;border-radius:6px;padding:11px 14px}' . '.awb-dw-stat__label{font-size:11px;color:#646970;text-transform:uppercase;letter-spacing:.04em;margin-bottom:3px}' . '.awb-dw-stat__value{font-size:22px;font-weight:700;line-height:1.2;color:#1d2327;margin-bottom:2px}' . '.awb-dw-change{font-size:11px;font-weight:600;display:block}' . '.awb-dw-source-badge{font-size:11px;color:#646970;margin-bottom:10px;padding:5px 8px;background:#f6f7f7;border-radius:4px;display:inline-block}' . '.awb-dw-health{display:flex;align-items:center;justify-content:space-between;padding:11px 0 0;border-top:1px solid #f0f0f1;font-size:13px;cursor:pointer;user-select:none}' . '.awb-dw-health-toggle{font-size:11px;color:#646970;margin-left:6px}' . '.awb-dw-issues{margin:6px 0 4px;display:none}' . '.awb-dw-issues.open{display:block}' . '.awb-dw-issue-row{display:flex;align-items:center;justify-content:space-between;padding:6px 0;border-bottom:1px solid #f0f0f1;font-size:12px;color:#3c434a}' . '.awb-dw-issue-row:last-child{border-bottom:none}' . '.awb-dw-issue-count{font-weight:700;min-width:24px;text-align:right;margin-right:8px}' . '.awb-dw-issue-fix{font-size:11px;color:#135e96;white-space:nowrap;text-decoration:none;font-weight:500}' . '.awb-dw-issue-fix:hover{text-decoration:underline}' . '.awb-dw-footer{margin:10px 0 2px;text-align:right;font-size:12px}' . '.awb-dw-footer a{color:#135e96;text-decoration:none}' . '.awb-dw-footer a:hover{text-decoration:underline}' . '.awb-dw-connect{padding:6px 0 10px;font-size:13px;color:#50575e}' . '.awb-dw-connect p{margin:0 0 8px}' . '.awb-dw-notice{background:#fff8e5;border-left:3px solid #dba617;border-radius:3px;padding:8px 10px;font-size:12px;color:#4a4a4a;margin-bottom:12px;line-height:1.5}' . '.awb-dw-notice a{color:#135e96}' ); } public static function add_widget() { wp_add_dashboard_widget( 'awbseo_dashboard_widget', 'CitedSEO', [ __CLASS__, 'render_widget' ] ); } /** * Reorder the global $wp_meta_boxes array so our widget is first in the * 'normal' column at 'high' priority. This affects users who haven't saved * a custom widget layout yet. */ public static function move_to_top() { global $wp_meta_boxes; $widget_id = 'awbseo_dashboard_widget'; $widget = null; // Find and remove from wherever WordPress placed it. foreach ( [ 'normal', 'side', 'column3', 'column4' ] as $col ) { foreach ( [ 'high', 'core', 'default', 'low' ] as $prio ) { if ( isset( $wp_meta_boxes['dashboard'][ $col ][ $prio ][ $widget_id ] ) ) { $widget = $wp_meta_boxes['dashboard'][ $col ][ $prio ][ $widget_id ]; unset( $wp_meta_boxes['dashboard'][ $col ][ $prio ][ $widget_id ] ); } } } if ( ! $widget ) { return; } // Prepend to normal/high so it appears above everything else. $existing = $wp_meta_boxes['dashboard']['normal']['high'] ?? []; $wp_meta_boxes['dashboard']['normal']['high'] = array_merge( [ $widget_id => $widget ], $existing ); } /** * For users who already have a saved dashboard widget order, inject our * widget at the top of the normal column so it's always first. * * @param array|false $order Saved meta-box order from user_meta. * @return array|false */ public static function force_top_position( $order ) { if ( empty( $order ) || ! is_array( $order ) ) { return $order; } $widget_id = 'awbseo_dashboard_widget'; // Strip it from every column so we don't get duplicates. foreach ( $order as $col => $ids_str ) { $ids = array_filter( explode( ',', $ids_str ) ); $ids = array_values( array_diff( $ids, [ $widget_id ] ) ); $order[ $col ] = implode( ',', $ids ); } // Prepend to the normal column. $normal_ids = array_filter( explode( ',', $order['normal'] ?? '' ) ); array_unshift( $normal_ids, $widget_id ); $order['normal'] = implode( ',', $normal_ids ); return $order; } public static function render_widget() { global $wpdb; // ── Traffic data (reuse admin data layer) ────────────────────────── $data = AWBSEO_Admin::get_dashboard_data( 28 ); $gsc_on = $data['gsc_on']; $bing_on = $data['bing_on']; $gsc_summary = $data['gsc_summary']; $bing_summary = $data['bing_summary']; $gsc_last_error = $data['gsc_last_error'] ?? null; // Determine which source to display and whether we have a permission error. $gsc_permission_error = $gsc_on && ! $gsc_summary; if ( $gsc_summary ) { $summary = $gsc_summary; $source = 'GSC'; } elseif ( $bing_summary ) { $summary = $bing_summary; $source = 'Bing'; } else { $summary = null; $source = null; } // ── Site health counts ───────────────────────────────────────────── // Scoped to post + page (not just 'post') so this count matches what // clicking "Fix →" below actually shows - both are what a normal // site owner means by "my pages," and the Bulk Editor's own "All // Posts & Pages" filter draws from the same two post types. $no_kw_count = (int) $wpdb->get_var( "SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_awb_seo_focus_keyword' WHERE p.post_type IN ('post','page') AND p.post_status = 'publish' AND (pm.meta_value IS NULL OR pm.meta_value = '')" ); $no_desc_count = (int) $wpdb->get_var( "SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_awb_seo_description' WHERE p.post_type IN ('post','page') AND p.post_status = 'publish' AND (pm.meta_value IS NULL OR pm.meta_value = '')" ); $no_title_count = (int) $wpdb->get_var( "SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_awb_seo_title' WHERE p.post_type IN ('post','page') AND p.post_status = 'publish' AND (pm.meta_value IS NULL OR pm.meta_value = '')" ); // Only a real 404 or a failed connection counts as "broken" - see // AWBSEO_Broken_Links::is_broken_code() for why 401/403/429/etc are // excluded (bot/rate-limit blocks that aren't actually broken links). $broken_sql = AWBSEO_Broken_Links::broken_sql(); $broken_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}awb_broken_links WHERE {$broken_sql} AND fix_status != 'fixed'" ); $issues = $no_kw_count + $no_desc_count + $no_title_count + $broken_count; $issue_rows = []; if ( $broken_count ) { $issue_rows[] = [ 'label' => 'Broken links', 'count' => $broken_count, 'url' => admin_url( 'admin.php?page=awbseo-broken-links' ), ]; } if ( $no_title_count ) { $issue_rows[] = [ 'label' => 'Missing SEO title', 'count' => $no_title_count, 'url' => admin_url( 'admin.php?page=awbseo-bulk&awb_filter=missing_title' ), ]; } if ( $no_desc_count ) { $issue_rows[] = [ 'label' => 'Missing meta description', 'count' => $no_desc_count, 'url' => admin_url( 'admin.php?page=awbseo-bulk&awb_filter=missing_description' ), ]; } if ( $no_kw_count ) { $issue_rows[] = [ 'label' => 'Missing focus keyword', 'count' => $no_kw_count, 'url' => admin_url( 'admin.php?page=awbseo-bulk&awb_filter=missing_keyword' ), ]; } $settings_url = admin_url( 'admin.php?page=awbseo-settings' ); $dashboard_url = admin_url( 'admin.php?page=awbseo' ); ?>
Connect Google Search Console or Bing Webmaster Tools to see clicks, impressions, and rankings right here.