TL;DR: This article outlines the code needed to add a custom link on the All Posts screen that uses a custom piece of post metadata.

Note: A few months ago, I wrote an article on how to add a custom view to the All Posts screen. This article is not all together the same, but not all together different. Think of it as a more detailed and perhaps for more practical implementation of the concept.


Assume that you have a standard post type or a custom post type and you’re going to simply filter by a headline that you define using a mechanism that allows you to save data to the post_metadata table.

For example, let’s say that you have a post and it as a piece of meta data with:

  • a meta_key with the value of article_attribute
  • a meta_value with the value of headline

And you want to use this information to add a new Headlines link that automatically filters everything out except articles with that metadata.

Here’s how to do it.

Custom Link on All Posts Screen

Before getting started, it’s worth noting that there are two ways to go about tackling this problem:

  • We could add the link at the top of the All Posts page first and then add the functionality for filtering the data second,
  • Or we can do it the other way around where we add the backend logic first then add the All Posts page link.

I’m going to opt to start with the second option. There’s no reason why it has to be done this way. It’s my preference.


First, I need to hook into the pre_get_posts hook provided by WordPress. I’m not going to be using any namespaced classes or prefixed functions in this example (given that I’ve covered that content enough on this site), but I’ll have demo plugin for this linked at the bottom of the post.

Anyway, I’ll start off by adding an anonymous function attached to the aforementioned hook:

add_action(
  'pre_get_posts',
  function ( WP_Query $query ) {
    // ...
  }
);

Notice that the anonymous function accepts a single argument which is a reference to the current instance of WP_Query that’s running on the page. If you’re not familiar with that class, then I’d recommend reading any of these articles or the Developer Resources page.

In the function, I need to check for the presence of a meta_value in the query string. This is easy to do thanks to the filter_input function provided by PHP.

add_action(
  'pre_get_posts',
  function ( WP_Query $query ) {
    $meta_value = 'headline';

    if ( filter_input( INPUT_GET, 'meta_value' ) === $meta_value ) {
      $query->set( 'meta_key', 'article_attribute' );
      $query->set( 'meta_value', $meta_value );
    }
  }
);

This hook will look to see if the headline value is key for the meta_value key in the query string. If so, it then adds a meta_key and meta_value to the instance of WP_Query which will instruct WordPress to retrieve all of the posts with just that metadata.


After that I need to add a a link to the top of the All Posts page to trigger this functionality. To do this, I’ll leverage the views_edit-posts hook. This function will accept an array of anchors that will be displayed at the top of the page.

I refer to these as $views so that’s what the function will accept when I stub it out:

add_action(
  'views_edit-post',
  function ( array $views ) {
    // ...
    return $views;
  }
);

Note that it’s important to return the array back to WordPress so that it knows what to render even if no modification is made.

First, I need to determine if I’m currently on the custom page. If so, then I need to add the proper attributes to the anchor added to the top of the page:

// Determine if we're looking at the Headlines page.
$attributes = 'class=""';
if ( filter_input(INPUT_GET, 'meta_value') === 'headline' ) {
  $attributes ='class="current aria-current="page"';
}

After that, I need to actually add the Headlines view to the page. This will require the use of several functions:

  • array_push for adding a new link to the list of $views
  • sprintf for securely adding a new string
  • add_query_arg for adding a set of custom query arguments to the current page.

The next section of code will look like this:

// Build the anchor for the 'Headlines' view and add it to $views.
array_push(
  $views,
  sprintf(
    '<a href="%1$s" %2$s>%3$s 
      <span class="count">(%4$s)</span>
     </a>
    ',
    add_query_arg([
      'post_type'   => 'post',
      'post_status' => 'all',
      'meta_value'  => 'headline',
    ], 'edit.php'),
    $attributes
    __('Headlines'),
    count( /* ... */ );
);

But I’m not done yet. Notice specifically that I’m making a call to count at the end of the function. This is so that I can properly display the number of posts that have this attribute.

I’m going to write two helper functions for this then I’ll return back to the sizeof call.


Here’s a helper function for finding the number of results that have the specified meta_key and meta_value that we have for this type of post. Notice that I’m using $wpdb to make a direct database call and that I’m specifically using prepare to make sure I do it safely.

function get_headline_results() : array {
  global $wpdb;
  return $wpdb->get_results( // phpcs:ignore
    $wpdb->prepare(
      "
      SELECT post_id FROM $wpdb->postmeta
      WHERE meta_key = %s AND meta_value = %s
      ",
      'article_attribute',
      'headline'
    ),
    ARRAY_A
  );
}

Notice that it returns all of the results (not just the number) because this value will be passed into another function momentarily.

At this point, we could stop and simply look at the content that’s returned from the query but if we’re just concerned with the post post type, then we’ll need to account for that. Here’s one way to do it:

function filter_posts_from_pages( array $results ) : array {
  $post_ids = array();

  foreach ( $results as $result) {
    if ( 'post' === get_post_type( $result['post_type'] ) ) {
      $post_ids[] = $result['post_id'];
    }
  }

  return $post_ids;
}

With that, we can return this value back to the count function above.


The final version of the block that we started above should look something like this:

// Build the anchor for the 'Headlines' and add it to $views.
array_push(
  $views,
  sprintf(
    '<a href="%1$s" %2$s>%3$s <span class="count">(%4$s)</span></a>',
    add_query_arg(
      array(
        'post_type'   => 'post',
        'post_status' => 'all',
        'meta_value'  => 'headlines', // phpcs:ignore
      ),
      'edit.php'
    ),
    $attributes,
    __( 'Headlines' ),
    count(
      filter_posts_from_pages( get_headline_results() )
    )
  )
);

Which means the complete version of the function for adding a new headline looks something like this:

add_action(
  'views_edit-post',
  function ( array $views ) {
    // Determine if we're looking at the Headlines page.
    $attributes = 'class=""';
    if ( filter_input( INPUT_GET, 'meta_value' ) === 'headline' ) {
      $attributes = 'class="current aria-current="page"';
    }

    // Build the anchor for the 'Headlines' and add it to $views.
    array_push(
      $views,
      sprintf(
        '<a href="%1$s" %2$s>%3$s <span class="count">(%4$s)</span></a>',
        add_query_arg(
          array(
            'post_type'   => 'post',
            'post_status' => 'all',
            'meta_value'  => 'headline', // phpcs:ignore
          ),
          'edit.php'
        ),
        $attributes,
        __( 'Headlines' ),
        count(
          filter_posts_from_pages( get_headline_results() )
        )
      )
    );

    return $views;
  }
);

And, as mentioned from the outset, this plugin is not namespaced or organized in a way that I normally write code. Instead, it’s meant to demonstrate a way of achieving something quickly – a prototype of sorts.

So if you’re interested in seeing something like this in action, you can check out the repository on GitHub. Notice that it is the develop branch at the moment. Remember to view the README as it will give instructions on how to add data to the database for posts so that the Headlines anchor actually work rather than unconditionally showing a zero value.