WordPress Tip: Add Custom Content Above & Below Admin Post Tables
Enhance your WordPress admin with custom content
Originally published: November 29th, 2017. Updated on: April 29th, 2025.
A recent project required extending the WordPress ‘All Posts’ listing page (wp-admin
) to display small amounts of custom content above and below the default posts table.
Here’s what we're aiming for – notice the new notification-style boxes:
(Image placeholder: Screenshot showing custom content boxes above and below the WP All Posts table)
As I outlined in an earlier post, WordPress doesn't offer many hooks for customizing the layout of admin index pages (unlike post edit pages, which are highly customizable). So, to get around this, I had to get a bit more technical and use a JavaScript-based solution. Definitely not ideal, but since we can't modify core admin files, that’s pretty much all we can do. You know, sometimes you just have to work with what you've got.
(Quick Link: If you just want the code, you can download the plugin zip file here. Some setup is needed, explained below.)
Note: This solution relies on JavaScript. Users with JavaScript disabled won't see the custom content. Therefore, don't depend on this for mission-critical information. Use it to present content that complements the standard admin view.
The JavaScript Solution
The JavaScript itself is relatively straightforward. I decided to hook onto two existing elements:
- The status bar at the top (e.g.,
All (2) | Published (2)
) - Content will be inserted before this. - The bottom navigation element (
.tablenav.bottom
) - Content will be inserted after this.
The code gets inserted directly into the <head>
of the admin pages, which is typically the simplest way to add extra scripts or styles to the admin area.
Here's the essence of the JavaScript:
// Wait for the DOM to be fully loaded
document.addEventListener('DOMContentLoaded', function() {
// Create container divs for our custom content
var aboveContainer = document.createElement("div");
var belowContainer = document.createElement("div");
// Add CSS classes for styling
aboveContainer.classList.add('custom-admin-table-content', 'above');
belowContainer.classList.add('custom-admin-table-content', 'below');
// Find the PHP-generated templates (we'll define these later)
// Using unique IDs is generally safer than querySelector('template[name=...]')
var aboveTemplate = document.getElementById('admin-template-above');
var belowTemplate = document.getElementById('admin-template-below');
// If the templates exist, clone their content into our containers
if (aboveTemplate) {
// Use template.content.cloneNode(true) for proper template handling
aboveContainer.appendChild(aboveTemplate.content.cloneNode(true));
}
if (belowTemplate) {
belowContainer.appendChild(belowTemplate.content.cloneNode(true));
}
// Target elements to insert content relative to
var targetAbove = document.querySelector(".subsubsub"); // The All | Published | Trash links container
var targetBelow = document.querySelector('.tablenav.bottom'); // The bottom table navigation
// Insert the containers into the DOM
if (aboveTemplate && targetAbove && targetAbove.parentNode) {
// Insert the "above" container just before the .subsubsub links
targetAbove.parentNode.insertBefore(aboveContainer, targetAbove);
}
if (belowTemplate && targetBelow && targetBelow.parentNode) {
// Insert the "below" container right after the bottom navigation element
// Use insertAdjacentElement for potentially cleaner insertion
targetBelow.insertAdjacentElement('afterend', belowContainer);
}
});
Freestylin' (CSS)
Because we’re adding some custom containers, some basic styling helps. Naturally, these styles are added to the admin <head>
via the admin_head
hook in PHP.
<style>
.custom-admin-table-content {
border: 1px solid #ccd0d4; /* Updated WP admin color */
color: #1d2327; /* Updated WP admin color */
background: #fff;
border-left: 4px solid #7e8993; /* Updated WP admin color */
padding: 1px 12px; /* Standard WP notice padding */
margin-bottom: 15px; /* Spacing below */
box-shadow: 0 1px 1px rgba(0,0,0,.04);
}
/* Add margin-top only if it's the first element after the header */
.wp-header-end + .custom-admin-table-content.above {
margin-top: 15px; /* Spacing above */
}
.custom-admin-table-content.below {
margin-top: 15px; /* Spacing above */
}
.custom-admin-table-content p {
margin: 0.5em 0;
padding: 2px;
font-size: 13px; /* Standard WP notice font size */
}
/* Clearfix if needed, depending on content */
.custom-admin-table-content::after {
content: "";
display: table;
clear: both;
}
</style>
I’ve included .above
and .below
classes so you can target styles specifically.
Defining the Content (PHP)
To make this robust, the actual content for these boxes is defined in PHP files within your active theme directory. This keeps content manageable and trackable in version control, which is pretty neat.
The plugin code looks for files inside an /admin-above-below
directory within your theme. Create this folder and place files named like {$post_type}-above-all.php
and {$post_type}-below-all.php
inside it (e.g., post-above-all.php
, page-below-all.php
).
Here are a couple of sample files you can use:
Content Above the Table
This example greets the current user and shows info about their last post.
File: {your_theme_folder}/admin-above-below/post-above-all.php
<?php
// Get the current logged-in user's details
$user = wp_get_current_user();
$name = $user->display_name; // Use display_name
// Determine the current post type (default to 'post')
$post_type = isset($_GET['post_type']) ? sanitize_key($_GET['post_type']) : 'post';
$post_type_object = get_post_type_object($post_type);
$post_type_label = $post_type_object ? $post_type_object->labels->singular_name : 'post';
// Check for the user's latest post of the current type
$args = array(
'author' => $user->ID,
'orderby' => 'post_date',
'post_type' => $post_type,
'post_status' => 'any', // Include drafts, etc.
'order' => 'DESC',
'posts_per_page' => 1 // More efficient than numberposts
);
$user_posts = get_posts($args);
// Show a different message depending on whether they've posted or not
if ($user_posts) :
$last_post = $user_posts[0];
$edit_link = get_edit_post_link($last_post->ID);
$post_title = get_the_title($last_post->ID);
// translators: 1: User name, 2: Edit link, 3: Post title, 4: Time difference
$message = sprintf(
esc_html__('Welcome back, %1$s! Your last %4$s was %2$s, created %3$s ago.', 'your-text-domain'),
esc_html($name),
sprintf('<a href="%s">%s</a>', esc_url($edit_link), esc_html($post_title)),
esc_html(human_time_diff(get_post_time('U', false, $last_post), current_time('timestamp'))),
esc_html(strtolower($post_type_label))
);
else :
$new_post_link = admin_url('post-new.php?post_type=' . $post_type);
// translators: 1: User name, 2: New post link, 3: Post type label
$message = sprintf(
esc_html__('Hi %1$s. Get started by creating a %2$s.', 'your-text-domain'),
esc_html($name),
sprintf('<a href="%s">new %s</a>', esc_url($new_post_link), esc_html(strtolower($post_type_label)))
);
endif;
// Output the message within a paragraph tag
echo '<p>' . wp_kses_post($message) . '</p>'; // Use wp_kses_post for security
?>
Content Below the Table
This snippet shows how many revisions have been made in the last 7 days for the current post type. Maybe useful? Who knows.
File: {your_theme_folder}/admin-above-below/post-below-all.php
<?php
global $wpdb;
// Determine the current post type (default to 'post')
$post_type = isset($_GET['post_type']) ? sanitize_key($_GET['post_type']) : 'post';
// SQL to count distinct parent posts with revisions in the last 7 days
// This is more efficient than fetching all revision IDs
$sql = $wpdb->prepare(
"SELECT COUNT(DISTINCT P1.ID)
FROM {$wpdb->posts} as P0
LEFT JOIN {$wpdb->posts} as P1 ON P0.post_parent = P1.ID
WHERE P0.post_type = 'revision'
AND P1.post_type = %s
AND P1.post_status != 'trash'
AND P1.post_status != 'auto-draft'
AND P0.post_modified_gmt >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 7 DAY)",
// Use post_modified_gmt and UTC_TIMESTAMP for timezone consistency
$post_type
);
$count = $wpdb->get_var($sql);
if ($count === null) {
// Handle potential DB error
echo '<p>' . esc_html__('Could not retrieve revision count.', 'your-text-domain') . '</p>';
} else {
// translators: %d is the number of revisions
printf(
'<p>' . esc_html(_n(
'There has been %d revision in the past 7 days for this post type.',
'There have been %d revisions in the past 7 days for this post type.',
$count,
'your-text-domain'
)) . '</p>',
(int)$count // Cast to integer
);
}
?>
Download the Plugin {#download-plugin}
Here’s the code wrapped up in a plugin for easy installation. It includes logic to check the current screen and display content only for the relevant post types based on your template filenames.
(Link placeholder: Download Plugin .zip)
Installation:
- Copy the plugin folder from the .zip into your
/wp-content/plugins/
directory, or upload the zip via the WordPress admin Plugins > Add New > Upload Plugin. - Activate the plugin via the WordPress admin Plugins page.
- Create the
admin-above-below
folder in your active theme directory (/wp-content/themes/your-theme-name/
). - Copy the example template files (
post-above-all.php
,post-below-all.php
) from the plugin's/templates/
directory into your theme'sadmin-above-below
folder to get started. - Create additional content files for other post types using the format
{$post_type}-above-all.php
and{$post_type}-below-all.php
(e.g.,page-above-all.php
).