On the Inner Circle, I was answering a question about getting the number of pages when a sitemap wasn’t available, and I quickly had ChatGPT throw this together. The person was looking for a tool to scope a project, so they would not have access to the dashboard, but it turned out to be a nice snippet.

<?php
/**
* Dashboard widget: Post type counts by status (WCAG-friendly + centered numbers)
* Drop into WPCodeBox as a PHP snippet.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_action( 'wp_dashboard_setup', function () {
if ( ! current_user_can( 'edit_posts' ) ) {
return;
}
wp_add_dashboard_widget(
'fw_post_type_status_counts',
__( 'Content Totals by Status', 'fw' ),
'fw_render_post_type_status_counts_widget'
);
} );
function fw_render_post_type_status_counts_widget() {
$post_types = get_post_types(
[
'public' => true,
'show_ui' => true,
],
'objects'
);
if ( empty( $post_types ) ) {
echo '<p>' . esc_html__( 'No post types found.', 'fw' ) . '</p>';
return;
}
// Statuses to display (adjust as needed).
$statuses = [
'publish' => __( 'Published', 'fw' ),
'draft' => __( 'Drafts', 'fw' ),
'pending' => __( 'Pending', 'fw' ),
'future' => __( 'Scheduled', 'fw' ),
'private' => __( 'Private', 'fw' ),
'trash' => __( 'Trash', 'fw' ),
];
$widget_id = 'fw-pt-status-widget';
$caption_id = 'fw-pt-status-caption';
$summary_id = 'fw-pt-status-summary';
$statuses_id = 'fw-pt-status-statuses';
// Inline styles kept minimal; uses WP admin colors by default.
echo '<style>
#' . esc_attr( $widget_id ) . ' table{width:100%;border-collapse:collapse}
#' . esc_attr( $widget_id ) . ' caption{padding:8px 0;text-align:left}
#' . esc_attr( $widget_id ) . ' th,#' . esc_attr( $widget_id ) . ' td{padding:8px;border-bottom:1px solid #dcdcde;vertical-align:middle}
#' . esc_attr( $widget_id ) . ' th{font-weight:600;text-align:left}
#' . esc_attr( $widget_id ) . ' td.num,#' . esc_attr( $widget_id ) . ' th.num{text-align:center;white-space:nowrap}
#' . esc_attr( $widget_id ) . ' .pt-slug{opacity:.8;font-size:12px;margin-top:2px}
#' . esc_attr( $widget_id ) . ' a{color:inherit}
#' . esc_attr( $widget_id ) . ' a:focus{outline:2px solid currentColor;outline-offset:2px}
</style>';
echo '<div id="' . esc_attr( $widget_id ) . '">';
// Extra context for screen readers (and visible users) without being noisy.
echo '<p id="' . esc_attr( $summary_id ) . '" class="description">';
echo esc_html__( 'Counts are grouped by post type and status. Select a number to open the filtered list in the admin.', 'fw' );
echo '</p>';
// Legend list for abbreviations (if any) — here it helps SR users understand column meaning,
// and gives a clear mapping of which statuses are present.
echo '<div id="' . esc_attr( $statuses_id ) . '" class="description" style="margin:0 0 10px 0;">';
echo '<strong>' . esc_html__( 'Statuses:', 'fw' ) . '</strong> ';
echo esc_html( implode( ', ', array_values( $statuses ) ) );
echo '</div>';
echo '<table aria-describedby="' . esc_attr( $summary_id ) . ' ' . esc_attr( $statuses_id ) . '">';
// Caption provides table purpose (WCAG-friendly).
echo '<caption id="' . esc_attr( $caption_id ) . '">';
echo esc_html__( 'Post type totals by status', 'fw' );
echo '</caption>';
echo '<thead><tr>';
echo '<th scope="col">' . esc_html__( 'Post type', 'fw' ) . '</th>';
foreach ( $statuses as $status_key => $label ) {
echo '<th scope="col" class="num">' . esc_html( $label ) . '</th>';
}
echo '<th scope="col" class="num">' . esc_html__( 'Total', 'fw' ) . '</th>';
echo '</tr></thead><tbody>';
foreach ( $post_types as $pt ) {
// Optional: skip Media attachments. Remove if you want to include.
if ( 'attachment' === $pt->name ) {
continue;
}
$pt_obj = get_post_type_object( $pt->name );
if ( ! $pt_obj ) {
continue;
}
$counts_obj = wp_count_posts( $pt->name );
$total = 0;
echo '<tr>';
echo '<th scope="row">';
echo esc_html( $pt->labels->singular_name );
echo '<div class="pt-slug">' . esc_html( $pt->name ) . '</div>';
echo '</th>';
foreach ( $statuses as $status_key => $label ) {
$count = 0;
if ( is_object( $counts_obj ) && isset( $counts_obj->{$status_key} ) ) {
$count = (int) $counts_obj->{$status_key};
}
$total += $count;
// If user can see/edit this post type, link counts to the filtered list.
$can_access = current_user_can( $pt_obj->cap->edit_posts );
$link = admin_url(
'edit.php?post_type=' . rawurlencode( $pt->name ) .
'&post_status=' . rawurlencode( $status_key )
);
// Make link text accessible: include post type + status + count (visually still just the number).
$aria = sprintf(
/* translators: 1: count, 2: post type singular label, 3: status label */
__( '%1$s %2$s items with status %3$s', 'fw' ),
number_format_i18n( $count ),
$pt->labels->singular_name,
$label
);
echo '<td class="num">';
if ( $can_access ) {
echo '<a href="' . esc_url( $link ) . '" aria-label="' . esc_attr( $aria ) . '">';
echo esc_html( number_format_i18n( $count ) );
echo '</a>';
} else {
echo esc_html( number_format_i18n( $count ) );
}
echo '</td>';
}
// Total column (links to "All" list table).
$all_link = admin_url( 'edit.php?post_type=' . rawurlencode( $pt->name ) . '&all_posts=1' );
$aria_total = sprintf(
/* translators: 1: total count, 2: post type singular label */
__( '%1$s total %2$s items (all statuses)', 'fw' ),
number_format_i18n( $total ),
$pt->labels->singular_name
);
echo '<td class="num"><strong>';
if ( current_user_can( $pt_obj->cap->edit_posts ) ) {
echo '<a href="' . esc_url( $all_link ) . '" aria-label="' . esc_attr( $aria_total ) . '">';
echo esc_html( number_format_i18n( $total ) );
echo '</a>';
} else {
echo esc_html( number_format_i18n( $total ) );
}
echo '</strong></td>';
echo '</tr>';
}
echo '</tbody></table>';
echo '</div>';
}
Leave a Reply