WordPress allows you to modify and add columns to the list of posts, pages or any custom post type in admin panel. In this post we’ll look into how!

There are two hooks to consider: one filter for the column’s position and heading, and one action for the column’s output for each post. The post type is a part of the hooks’ names. Let’s look at them one by one, starting with the filter.

Column filter

The filter for modifying, removing or adding columns to post list in WordPress admin panel is manage_{$post_type}_posts_columns. Exchange {$post_type} with the desired post type. For example; if you want to edit columns for post type ‘post‘, the filter name would be manage_post_posts_columns. And for a custom post type ‘product‘, the filter name would be manage_product_posts_columns.

PS: WordPress has a column filter exclusively for post type ‘page‘: manage_pages_columns, but you’ll achieve the same result by using the filter manage_page_posts_columns.

Provided as argument to the filter you get the full array of all columns for that post type. Each column has an unique key and their values are the labels shown in the column header. Example of keys are ‘cb‘ for the checkbox column and ‘title‘ for the post title column. The order of the elements in the array determines the order of the columns.

What you need to do for adding a new column is simply adding a new key + value pair to the array and return it. You can manipulate the array as you wish – for instance reordering it.

Column content hook

Which hook you need to use for controlling the output of the column content depends on whether or not your post type is set to be hierarchical or not. A hierarchical post type has been defined as 'hierarchical' => true in the register_post_type. Any non-hierarchical post types, including WordPress’ built-in post type ‘post‘, use the hook name manage_{$post_type}_custom_column. Any hierarchical post types, including WordPress’ built-in post type ‘page‘, use the hook name manage_pages_custom_column (note: no injection of post type name in the hook name).

Provided as arguments to this hook you get the column name, which is the key mentioned in column filter above (e.g. ‘cb‘ for checkbox column), and secondly the post ID. This hook is run on every post, and the basic premise is that you check whether or not we are at the correct column type (by the key), and if we are, use the post ID to fetch post meta or similar and output what you want.

Examples

Let’s look at some code of practical examples.

Add a custom column to posts that displays a custom post meta

This is the simplest use of adding custom columns. Let’s say we want to add a custom column to post type ‘post’. We want it at the very end of the columns, called “Verified”, and it should output ‘Yes’ or ‘No’ depending on a custom post meta value. Note: This example does not include saving or updating the custom post meta.

In our functions.php or anywhere in our theme or plugin code, we’ll add the column itself by merging it into the column array, and in the hook for column output, we fetch the value of the post meta and output it.

add_filter('manage_post_posts_columns', function($columns) {
	return array_merge($columns, ['verified' => __('Verified', 'textdomain')]);
});

add_action('manage_post_posts_custom_column', function($column_key, $post_id) {
	if ($column_key == 'verified') {
		$verified = get_post_meta($post_id, 'verified', true);
		if ($verified) {
			echo '<span style="color:green;">'; _e('Yes', 'textdomain'); echo '</span>';
		} else {
			echo '<span style="color:red;">'; _e('No', 'textdomain'); echo '</span>';
		}
	}
}, 10, 2);

The output can be anything you want, I simply added a span around the output with different text colors for easy identification.

Add a custom column to a hierarchical custom post type that displays its ancestor post

When a post type is hierarchical, posts can have parent posts. Let’s say we have a hierarchical custom post type ‘subject‘ for school subjects where it’s a habit of making a lot of children posts, and even children of children posts. For better overview we want to add a column that displays the post’s ancestor parent (“root subject”). If the post is a top-level post, a simple ‘-‘ is shown, otherwise the column outputs the ancestor’s parent post title in a link to edit post.

Because we are referring to a hierarchical post type we need to use a different hook for outputting column content than the example above, but the process is exactly the same.

This code also shows an example how to inject a column into the middle of the columns array. We define that our column should come before ‘author’, and use PHP array functions to inject the element in the right position.

add_filter('manage_subject_posts_columns', function($columns) {
	$offset = array_search('author', array_keys($columns));
	return array_merge(array_slice($columns, 0, $offset), ['ancestor' => __('Ancestor', 'textdomain')], array_slice($columns, $offset, null));
});

add_action('manage_pages_custom_column', function($column_key, $post_id) {
	if ($column_key == 'ancestor') {
		$ancestors = get_ancestors($post_id, 'subject', 'post_type');
		$post_ancestor = end($ancestors);
		if ($post_ancestor != 0) {
			echo '<a href="' . get_edit_post_link($post_ancestor) . '">' . get_the_title($post_ancestor) . '</a>';
		} else {
			echo '-';
		}
	}
}, 10, 2);

Removing a column

Removing a column from a post type is pretty simple; all you need is to filter posts columns, remove the element from the array and return it. You don’t need to hook into the column output hook. For example; removing the default ‘date‘ column from post type ‘post‘:

add_filter('manage_post_posts_columns', function($columns) {
	unset($columns['date']);
	return $columns;
});

Changing default columns’ names or position

Let’s assume we have a custom post type ‘book‘, and we want to replace the default column name “Author” with “Publisher”. We simply filter the columns filter and give the key ‘author‘ a different value:

add_filter('manage_book_posts_columns', function($columns) {
	$columns['author'] = __('Publisher', 'textdomain');
	return $columns;
});

Reordering columns can be done by using PHP array functions. Keep in mind that they resulting array must be a associative array with column “IDs” as keys and their label as values. This is a simple example of taking out the ‘author‘ column and putting it at the very end, thus reordering the columns:

add_filter('manage_post_posts_columns', function($columns) {
	$taken_out = $columns['author'];
	unset($columns['author']);
	$columns['author'] = $taken_out;
	return $columns;
});

Setting a custom column as sortable

By default some of WordPress’ columns are sortable, for example the post title, comment count and date. It is possible to make your custom column sortable, but it requires a bit more code and hooking into WordPress’ post query hook in order to tell WordPress how to order by your post meta.

Let’s assume we have a custom post type ‘movie‘, and with the following code we add a custom column that displays the custom post meta ‘duration‘.

add_filter('manage_movie_posts_columns', function($columns) {
	return array_merge($columns, ['duration' => __('Duration', 'textdomain')]);
});

add_action('manage_movie_posts_custom_column', function($column_key, $post_id) {
	if ($column_key == 'duration') {
		$duration = get_post_meta($post_id, 'duration', true);
		echo (!empty($duration)) ? sprintf(__('%s minutes', 'textdomain'), $duration) : __('Unknown', 'textdomain');
	}
}, 10, 2);

In order to tell WordPress that we want our column to be sortable we need to hook into the filter manage_edit-{$post_type}_sortable_columns. We add our column to the filterable columns array, and then we define what the ‘orderby‘ value should be. We set an unique custom value there – the same name as our column – which we can refer to in our query hook later.

add_filter('manage_edit-movie_sortable_columns', function($columns) {
	$columns['duration'] = 'duration';
	return $columns;
});

If you refresh WordPress now, you should see that our custom column is indeed clickable and sortable, but it doesn’t sort right. That’s because WordPress doesn’t understand ‘duration‘ as an ‘orderby‘ value. That’s what we’ll fix by hooking into ‘pre_get_posts‘:

add_action('pre_get_posts', function($query) {
    if (!is_admin()) {
        return;
    }
 
    $orderby = $query->get('orderby');
    if ($orderby == 'duration') {
        $query->set('meta_key', 'duration');
        $query->set('orderby', 'meta_value_num');
    }
});

Because our custom post meta ‘duration‘ always will be a number, we can define ‘orderby‘ as ‘meta_value_num‘ for number comparison. Adjust the query arguments to fit your custom post meta values.

Disable sorting for default columns

Disabling sorting for a default column is pretty simple. All we need to do is hook into the filter manage_edit-{$post_type}_sortable_columns and remove the column we don’t want sorting on from the array. For example this removes sorting for the date column for post type ‘post‘.

add_filter('manage_edit-post_sortable_columns', function($columns) {
	unset($columns['date']);
	return $columns;
});