plugin = $plugin;
}
/**
* Registers hooks.
*/
public function register(): void {
add_action( 'wp_dashboard_setup', array( $this, 'add' ) );
}
/**
* Adds the widget for users who can see the SEO dashboard.
*/
public function add(): void {
if ( ! current_user_can( Capabilities::MANAGE_SETTINGS ) ) {
return;
}
wp_add_dashboard_widget( self::ID, __( 'SEO overview', 'wpinterface-seo' ), array( $this, 'render' ) );
wp_add_inline_style( 'dashboard', self::css() );
}
/**
* Prints the widget.
*/
public function render(): void {
$report = new Report( $this->plugin );
$stats = ( new Stats( $this->plugin ) )->get();
$insights = $report->insights( $stats );
$tiles = $this->tiles( $stats );
$more = count( $insights ) - self::STEPS;
echo '
';
printf(
'',
esc_url( Report::page_url( 'dashboard' ) ),
esc_html__( 'Open SEO dashboard', 'wpinterface-seo' ),
$more > 0 ? '' . esc_html(
sprintf(
/* translators: %s: number of further next steps. */
_n( '%s more step', '%s more steps', $more, 'wpinterface-seo' ),
number_format_i18n( $more )
)
) . '' : ''
);
}
/**
* Figures shown above the next steps.
*
* @param array $stats Dashboard figures.
* @return list
*/
private function tiles( array $stats ): array {
$tiles = array();
$search = $stats['search'] ?? null;
$content = $stats['content'];
if ( is_array( $search ) ) {
$change = self::change( (float) $search['totals']['clicks'], (float) $search['previous']['clicks'] );
$tiles[] = array(
'label' => __( 'Clicks, 28 days', 'wpinterface-seo' ),
'value' => number_format_i18n( $search['totals']['clicks'] ),
'unit' => '',
'note' => null === $change ? __( 'No earlier data', 'wpinterface-seo' ) : sprintf( '%+d%%', $change ),
'tone' => null === $change || 0 === $change ? '' : ( $change > 0 ? 'up' : 'down' ),
);
} else {
$tiles[] = array(
'label' => __( 'Indexable', 'wpinterface-seo' ),
'value' => number_format_i18n( $content['indexable'] ),
'unit' => '',
'note' => sprintf(
/* translators: %s: number of published posts. */
__( 'of %s published', 'wpinterface-seo' ),
number_format_i18n( $content['published'] )
),
'tone' => '',
);
}
$scored = $content['published'] - $content['none'];
$tiles[] = array(
'label' => __( 'Average SEO score', 'wpinterface-seo' ),
'value' => $scored > 0 ? number_format_i18n( (int) round( $content['scoreSum'] / $scored ) ) : '–',
'unit' => $scored > 0 ? '/100' : '',
'note' => $content['none'] > 0
? sprintf(
/* translators: %s: number of posts. */
__( '%s not analyzed', 'wpinterface-seo' ),
number_format_i18n( $content['none'] )
)
: __( 'All analyzed', 'wpinterface-seo' ),
'tone' => '',
);
/**
* Filters the figures in the "SEO overview" Dashboard widget.
*
* @param array $tiles Tiles: label, value, unit, note, tone (''|up|down).
* @param array $stats Dashboard figures.
*/
$tiles = (array) apply_filters( 'wpiseo_dashboard_widget_tiles', $tiles, $stats );
return array_values(
array_map(
static fn( array $tile ): array => array(
'label' => (string) ( $tile['label'] ?? '' ),
'value' => (string) ( $tile['value'] ?? '' ),
'unit' => (string) ( $tile['unit'] ?? '' ),
'note' => (string) ( $tile['note'] ?? '' ),
'tone' => in_array( $tile['tone'] ?? '', array( 'up', 'down' ), true ) ? (string) $tile['tone'] : '',
),
array_filter( $tiles, 'is_array' )
)
);
}
/**
* Percentage change, or null without a previous value.
*
* @param float $current Current value.
* @param float $previous Previous value.
*/
private static function change( float $current, float $previous ): ?int {
return $previous > 0 ? (int) round( ( $current - $previous ) / $previous * 100 ) : null;
}
/**
* Severity icon with an accessible name.
*
* @param string $severity critical|warning|notice.
*/
private static function icon( string $severity ): string {
$icons = array(
'critical' => array( __( 'Critical', 'wpinterface-seo' ), '' ),
'warning' => array( __( 'Should fix', 'wpinterface-seo' ), '' ),
'notice' => array( __( 'Suggestion', 'wpinterface-seo' ), '' ),
);
$icon = $icons[ $severity ] ?? $icons['notice'];
return sprintf( '', esc_attr( $icon[0] ), $icon[1] );
}
/**
* Widget styles (added to the Dashboard's own stylesheet).
*/
private static function css(): string {
return '#wpiseo_overview .inside{margin:0;padding:0}'
. '.wpiseo-widget{padding:12px}'
. '.wpiseo-widget__tiles{display:grid;gap:8px}'
. '.wpiseo-widget__tile{display:flex;flex-direction:column;min-width:0;background:#f6f7f7;border:1px solid #f0f0f1;border-radius:2px;padding:8px 10px}'
. '.wpiseo-widget__label,.wpiseo-widget__note{font-size:12px;color:#646970}'
. '.wpiseo-widget__value{font-size:18px;font-weight:600;line-height:1.4;color:#1d2327}'
. '.wpiseo-widget__unit{font-size:12px;font-weight:400;color:#646970}'
. '.wpiseo-widget__note.is-up{color:#007017}.wpiseo-widget__note.is-down{color:#b32d2e}'
. '.wpiseo-widget__heading{margin:16px 0 4px;font-size:13px;font-weight:600}'
. '.wpiseo-widget__steps{margin:0}'
. '.wpiseo-widget__steps li{display:flex;gap:8px;align-items:flex-start;margin:0;padding:8px 0;border-bottom:1px solid #f0f0f1}'
. '.wpiseo-widget__steps li:last-child{border-bottom:0}'
. '.wpiseo-widget__icon{flex:0 0 18px;margin-top:1px}'
. '.wpiseo-widget__step{flex:1 1 auto}'
. '.wpiseo-widget__empty{margin:4px 0 0;color:#646970}'
. '.wpiseo-widget__footer{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:10px 12px;border-top:1px solid #f0f0f1;background:#f6f7f7;font-size:12px;color:#646970}'
. '.wpiseo-widget__footer a{font-size:13px}';
}
}