SOLARISE
DEV

WordPress How-To: Add Custom Columns to Admin Post Lists

Boost WordPress admin efficiency with custom columns

Originally published: November 28th, 2017. Updated on: April 29th, 2025.

I often find myself needing to display additional data alongside the default Post listings in the WordPress admin area (wp-admin). While WordPress doesn't offer as many layout hooks for the admin list tables as it does for the front-end or post editor, there are more than enough to get the job done for adding custom columns. It's quite flexible, really.

(Image placeholder: Default WordPress Posts admin table)

The goal here is to insert a couple of new columns into the standard "Posts" list table. We'll look at two key action/filter hooks:

  1. manage_{$post_type}_posts_columns: Filters the columns displayed for a post type. Used to add, remove, or reorder column headers.
  2. manage_{$post_type}_posts_custom_column: Action hook fired for each custom column, allowing you to output its content.

Using these, we can add useful information at a glance. Sure, plugins exist that offer similar functionality, but they can be limited, and I usually prefer small code snippets for specific tasks like this to keep things tidy and avoid unnecessary plugin overhead.

Example 1: Adding a ‘Word Count’ Column

Depending on the site, a word count column might be useful. Let's add one that counts words in the main post content. (Counting words from custom fields would require more creative coding).

Step 1: Add the Column Header

We use the manage_posts_posts_columns filter (note the _posts part, specifically for the 'post' post type) to add our new column header. Let's insert it just before the default 'Tags' column.

<?php

/**
    * Add 'Word Count' column header to the Posts list table.
    *
    * @param array $columns Existing columns.
    * @return array Modified columns.
    */
function my_plugin_add_word_count_column_header(array $columns): array
{
    // Define the new column
    $new_column = ['word_count' => __('Word Count', 'your-text-domain')];

    // Specify where to insert it (before 'tags')
    $insert_before_key = 'tags';

    // Find the position of the 'tags' column
    $tag_column_position = array_search($insert_before_key, array_keys($columns), true);

    // If 'tags' column exists, insert before it
    if ($tag_column_position !== false) {
        $columns = array_slice($columns, 0, $tag_column_position, true) +
                    $new_column +
                    array_slice($columns, $tag_column_position, null, true);
    } else {
        // Otherwise, just add it to the end (before 'date')
            $date_column_position = array_search('date', array_keys($columns), true);
            if ($date_column_position !== false) {
                $columns = array_slice($columns, 0, $date_column_position, true) +
                        $new_column +
                        array_slice($columns, $date_column_position, null, true);
            } else {
                // Fallback: add to the very end
            $columns = array_merge($columns, $new_column);
            }
    }

    return $columns;
}
add_filter('manage_posts_posts_columns', 'my_plugin_add_word_count_column_header');

?>

Step 2: Populate the Column Content

Now, we use the manage_posts_posts_custom_column action hook to output the data for our word_count column.

<?php

/**
    * Display content for the custom 'Word Count' column.
    *
    * @param string $column_name The name of the column.
    * @param int    $post_id     The ID of the current post.
    */
function my_plugin_display_word_count_column_content(string $column_name, int $post_id): void
{
    // Check if it's our custom column
    if ('word_count' === $column_name) {
        $post = get_post($post_id);
        if ($post) {
            // Get post content
            $content = $post->post_content;
            // Basic word count - counts spaces. Use strip_tags to avoid counting HTML.
            // For more accuracy, regex or mb_split might be better.
            $word_count = str_word_count(strip_tags($content));
            echo esc_html($word_count);
        } else {
            echo '—'; // Indicate error or missing post
        }
    }
}
add_action('manage_posts_posts_custom_column', 'my_plugin_display_word_count_column_content', 10, 2);

?>

Example 2: Listing All Contributors to an Article

WordPress's revisions feature tracks changes made by multiple users. We can use this data (wp_get_post_revisions()) to list everyone who has contributed to a post, not just the original author.

Step 1: Add the Column Header (and Remove 'Author')

Let's add a 'Contributors' column and remove the default 'Author' column since it becomes somewhat redundant. We'll place 'Contributors' before the 'Categories' column.

<?php

/**
    * Add 'Contributors' column and remove 'Author' column from Posts list table.
    *
    * @param array $columns Existing columns.
    * @return array Modified columns.
    */
function my_plugin_add_contributors_column_header(array $columns): array
{
    // 1. Remove the default 'Author' column
    unset($columns['author']);

    // 2. Define the new 'Contributors' column
    $new_column = ['contributors' => __('Contributors', 'your-text-domain')];

    // 3. Specify where to insert it (before 'categories')
    $insert_before_key = 'categories';

    // Find the position of the 'categories' column
    $category_column_position = array_search($insert_before_key, array_keys($columns), true);

    // If 'categories' column exists, insert before it
    if ($category_column_position !== false) {
        $columns = array_slice($columns, 0, $category_column_position, true) +
                    $new_column +
                    array_slice($columns, $category_column_position, null, true);
    } else {
            // Fallback: add it before the 'date' column
            $date_column_position = array_search('date', array_keys($columns), true);
            if ($date_column_position !== false) {
                $columns = array_slice($columns, 0, $date_column_position, true) +
                        $new_column +
                        array_slice($columns, $date_column_position, null, true);
            } else {
                // Fallback: add to the very end
            $columns = array_merge($columns, $new_column);
            }
    }

    return $columns;
}
// Note: Use a different priority if combining with the word count filter,
// or combine the logic into a single function.
add_filter('manage_posts_posts_columns', 'my_plugin_add_contributors_column_header', 11); // Higher priority

?>

Step 2: Populate the Column Content

We hook into manage_posts_posts_custom_column again. This time, we fetch revisions, get the unique author IDs, and display links to their user edit profiles.

<?php

/**
    * Display content for the custom 'Contributors' column.
    *
    * @param string $column_name The name of the column.
    * @param int    $post_id     The ID of the current post.
    */
function my_plugin_display_contributors_column_content(string $column_name, int $post_id): void
{
    if ('contributors' === $column_name) {
        $author_ids = [];

        // Get the original post author first
        $post = get_post($post_id);
        if ($post && isset($post->post_author)) {
                $author_ids[] = (int) $post->post_author;
        }

        // Get authors from revisions
        $revisions = wp_get_post_revisions($post_id, ['fields' => 'post_author']); // More efficient
        if (!empty($revisions)) {
            foreach ($revisions as $revision) {
                if (isset($revision->post_author)) {
                    $author_ids[] = (int) $revision->post_author;
                }
            }
        }

        // Get unique author IDs
        $unique_author_ids = array_unique($author_ids);

        $author_links = [];
        if (!empty($unique_author_ids)) {
            foreach ($unique_author_ids as $author_id) {
                $user_info = get_userdata($author_id);
                if ($user_info) {
                    $display_name = $user_info->display_name;
                    // Link to user's edit page if current user can edit users
                    if (current_user_can('edit_users')) {
                        $link = get_edit_user_link($author_id);
                        $author_links[] = sprintf(
                            '<a href="%s">%s</a>',
                            esc_url($link),
                            esc_html($display_name)
                        );
                    } else {
                            // Otherwise, just display the name
                        $author_links[] = esc_html($display_name);
                    }
                }
            }
        }

        if (!empty($author_links)) {
            echo implode(', ', $author_links);
        } else {
            echo '—'; // No authors found
        }
    }
}
add_action('manage_posts_posts_custom_column', 'my_plugin_display_contributors_column_content', 10, 2);

?>

End Result:

After adding both sets of code (or the combined version below), your Posts list table should look something like this:

(Image placeholder: Modified WordPress Posts admin table showing 'Word Count' and 'Contributors' columns)

The Code in Full (Combined and Optimized)

Instead of separate functions for each column, it's cleaner to combine the logic. You know, keep things tidy.

<?php

// --- Column Headers ---

/**
    * Modify the Posts list table column headers.
    * Adds 'Word Count' and 'Contributors', removes 'Author'.
    *
    * @param array $columns Existing columns.
    * @return array Modified columns.
    */
function my_plugin_modify_post_table_headers(array $columns): array
{
    $new_columns = [];

    foreach ($columns as $key => $title) {
        // Add Contributors before Categories
        if ($key === 'categories') {
            $new_columns['contributors'] = __('Contributors', 'your-text-domain');
        }
        // Add Word Count before Tags
        if ($key === 'tags') {
                $new_columns['word_count'] = __('Word Count', 'your-text-domain');
        }
        // Skip the original Author column
        if ($key !== 'author') {
            $new_columns[$key] = $title;
        }
    }

        // Fallback if categories or tags were missing
    if (!isset($new_columns['contributors'])) {
        // Add before date as fallback
        $date_pos = array_search('date', array_keys($new_columns), true);
        if ($date_pos !== false) {
                $new_columns = array_slice($new_columns, 0, $date_pos, true) +
                            ['contributors' => __('Contributors', 'your-text-domain')] +
                            array_slice($new_columns, $date_pos, null, true);
        } else {
                $new_columns['contributors'] = __('Contributors', 'your-text-domain'); // Add at end
        }
    }
        if (!isset($new_columns['word_count'])) {
            // Add before date as fallback
            $date_pos = array_search('date', array_keys($new_columns), true);
            if ($date_pos !== false) {
                $new_columns = array_slice($new_columns, 0, $date_pos, true) +
                            ['word_count' => __('Word Count', 'your-text-domain')] +
                            array_slice($new_columns, $date_pos, null, true);
            } else {
            $new_columns['word_count'] = __('Word Count', 'your-text-domain'); // Add at end
            }
    }


    return $new_columns;
}
add_filter('manage_posts_posts_columns', 'my_plugin_modify_post_table_headers');


// --- Column Content ---

/**
    * Display content for custom columns in the Posts list table.
    * Handles 'word_count' and 'contributors'.
    *
    * @param string $column_name The name of the column.
    * @param int    $post_id     The ID of the current post.
    */
function my_plugin_display_custom_post_table_content(string $column_name, int $post_id): void
{
    switch ($column_name) {
        case 'word_count':
            $post = get_post($post_id);
            if ($post) {
                $word_count = str_word_count(strip_tags($post->post_content));
                echo esc_html($word_count);
            } else {
                    echo '—';
            }
            break;

        case 'contributors':
            $author_ids = [];
            $post = get_post($post_id);
            if ($post && isset($post->post_author)) {
                    $author_ids[] = (int) $post->post_author;
            }
            $revisions = wp_get_post_revisions($post_id, ['fields' => 'post_author']);
            if (!empty($revisions)) {
                foreach ($revisions as $revision) {
                    if (isset($revision->post_author)) {
                        $author_ids[] = (int) $revision->post_author;
                    }
                }
            }
            $unique_author_ids = array_unique($author_ids);
            $author_links = [];
            if (!empty($unique_author_ids)) {
                foreach ($unique_author_ids as $author_id) {
                        $user_info = get_userdata($author_id);
                        if ($user_info) {
                            $display_name = $user_info->display_name;
                            if (current_user_can('edit_users')) {
                                $link = get_edit_user_link($author_id);
                                $author_links[] = sprintf('<a href="%s">%s</a>', esc_url($link), esc_html($display_name));
                            } else {
                                $author_links[] = esc_html($display_name);
                            }
                        }
                }
            }
            echo !empty($author_links) ? implode(', ', $author_links) : '—';
            break;
    }
}
add_action('manage_posts_posts_custom_column', 'my_plugin_display_custom_post_table_content', 10, 2);

?>

Custom Columns for Custom Post Types

Yes! You can apply this same functionality to any custom post type (CPT). Simply replace posts in the hook names with your CPT slug.

For a CPT named your_cpt, the hooks would be:

  • manage_your_cpt_posts_columns (for headers)
  • manage_your_cpt_posts_custom_column (for content)

Use the same callback functions (or create separate ones if the logic needs to differ significantly for the CPT), adjusting the filter/action tags accordingly.

<?php
$post_type = 'your_cpt'; // Replace with your CPT slug

// Example for CPT headers
add_filter("manage_{$post_type}_posts_columns", 'my_plugin_modify_post_table_headers');

// Example for CPT content
add_action("manage_{$post_type}_posts_custom_column", 'my_plugin_display_custom_post_table_content', 10, 2);
?>

And voila! Custom column content fun for all the family (and all the post types).

Robin Metcalfe

About the Author: Robin Metcalfe

Robin is a freelance web strategist and developer based in Edinburgh, with over 15 years of experience helping businesses build effective and engaging online platforms using technologies like Laravel and WordPress.

Get in Touch