How to filter by custom post type on Gutenberg “Latest Posts’ block

It’s Not (Reasonably) Possible (Yet) 🙁

This was such an interesting question to dig into. In short, we can get really, really close to accomplishing this using Block Filters and some hackish duct-tape on the PHP side. But it falls apart in the home stretch.

It’s worth noting that the crux of the complication is specific to the Latest Posts block’s implementation – doing something similar to other core blocks is totally possible, depending on the block and modification.

To get this working properly in the editor without weird side-effects requires a degree of hackery that goes so far beyond an acceptable compromise – I strongly discourage doing so. I’ve detailed the specific point of failure in the last section of this answer.

There’s also a chance that a change in core might result in this solution automagically becoming fully-functional as-is at a later date, or adding the necessary extension points to complete the solution otherwise.

The best solution would be to deregister core/latest-posts in favor of a custom block, perhaps even using the Gutenberg repo as upstream in order to pull in new developments to the core block.


(But We Can Get Suuuuper Close)

Functional post types input control added to Latest Posts block

In short, it’s possible to add post-type filtering to the latest posts block in a manner that’s fully functional on the front-end and with working controls in the Block Editor. What we can’t do is get the block in the editor to display posts of the appropriate type.

Despite falling short of a stable solution, much of what went into this attempt has a lot of merit for other customizations and modifications – particularly on the JS side. There’s a lot that can be learned here to apply elsewhere, so I think it’s still worth examining the attempt and where it breaks down.

Register Additional Block Attributes

We can add a new postTypes array attribute to the Latest Posts block in order to store the selected post types using a blocks.registerBlockType filter:

import { addFilter } from '@wordpress/hooks';

function addPostTypesAttribute( settings, name ) {
  if( name !== 'core/latest-posts' )
    return settings;

  settings.attributes = {
    ...settings.attributes,
    postTypes: {
      type: 'array',
      default: [ 'post' ]
    }
  };

  return settings;
}

addFilter(
  'blocks.registerBlockType',
  'wpse326869/latest-posts-post-types/add-attribute',
  addPostTypesAttribute
);

Add Controls to Interact with New Attributes

Filter the Edit Component

The editor.BlockEdit filter allows us to interact with/modify the component returned from the block’s edit() function.

Shim Components and Functionality with a HOC

The @wordpress/compose package provides a handy utility for “wrapping” a component in a Higher-Order Component/”HOC” (a function which takes a component as an argument and returns an augmented or wrapped version of it), effectively allowing us to shim our own components and functionality into a pre-existing component.

Place Sidebar Controls with the SlotFills System

To add control components for a block to the sidebar, we simply add them as children of an <InspectorControls> component, and Gutenberg’s nifty SlotFills system will render them outside of the block in the appropriate location.

Select a Control

In my opinion, FormTokenField is the perfect component to allow the user to enter a number of different post types. But a SelectControl or ComboboxControl might be more appropriate for your use-case.

Some amount of documentation for most controls can be found in The Block Editor Handboox, but often times you might find that you need to scour the sources on GitHub to make sense of everything.

Implementation

All the above in mind, we can add controls to interact with our new postTypes attribute as such:

import { createHigherOrderComponent } from '@wordpress/compose';
import { Fragment } from '@wordpress/element';
import { InspectorControls } from '@wordpress/block-editor';
import { FormTokenField } from '@wordpress/components'
import { useSelect } from '@wordpress/data';

// A multiple Post-Type selection control implemented on top of FormTokenField.
const PostTypesControl = ( props ) => {
  const { value = [], onChange } = props;

  const types = useSelect(
    ( select ) =>  ( select( 'core' ).getPostTypes() ?? [] ).map( ( { slug, name } ) => ( { value: slug, title: name } ) ),
    []
  );

  const tokenIsValid = ( title ) =>  types.some( type => type.title === title );
  const titleToValue = ( title ) => title ? types.find( type => type.title === title )?.value || '' : '';

  return (
    <FormTokenField
      value={ value }
      onChange={ onChange }
      suggestions={ types.map( type => type.title ) }
      saveTransform={ titleToValue }
      __experimentalValidateInput={ tokenIsValid }
    />
  );
};

// A HOC which adds a PostTypesControl to a block which has a `postTypes` attribute.
const withPostTypesControl = createHigherOrderComponent(
  ( BlockEdit ) => ( props ) => {
    const {
      name,
      attributes: { postTypes = [ 'post' ] },
      setAttributes,
    } = props;

    if( name !== 'core/latest-posts' )
      return <BlockEdit {...props} />;

    const setPostTypes = ( postTypes ) => setAttributes( { postTypes } );

    return (
      <Fragment>
        <BlockEdit {...props} />
        <InspectorControls>
          <PanelBody title="Post Types" initialOpen={false}>
            <PanelRow>
              <PostTypesControl
                value={ postTypes }
                onChange={ setPostTypes }
              />
            </PanelRow>
          </PanelBody>
        </InspectorControls>
      </Fragment>
    );
  },
  'wpse326869withPostTypesControl'
);

addFilter(
  'editor.BlockEdit',
  'wpse326869/latest-posts-post-types/add-controls',
  withPostTypesControl
);

Use the Attribute to Filter Posts Query

Were Latest Posts a static block type which rendered it’s content directly to HTML stored in the post_content, we’d probably use a blocks.getSaveElement filter here to use the attribute and modify the markup.

But Latest Posts is a dynamic block, meaning that when WordPress renders the block on the front-end it executes a PHP function in order to produce the content per page-load, just like a shortcode.

Unfortunately, looking at the source for the PHP function responsible for rendering the block, there’s no useful filter to modify the arguments which are sent to get_posts()

What we can do, however, is get a little hacky and use a block_type_metadata_settings filter to hijack the Latest Post block’s PHP render callback – this generally seems inadvisable; I probably wouldn’t distribute code leveraging such a hack. In our custom render callback, we can add a pre_get_posts filter to add our postTypes attribute to the query, execute the original render callback, and then remove the filter once more.

For ease of state management between these functions, I’ve chosen to use a singleton class to implement the above.

class WPSE326869_LatestPostsPostTypes {
    protected static $instance = null;
    protected $original_render_callback;
    protected $block_attributes;

    protected function __construct() {
        add_filter( 'block_type_metadata_settings', [ $this, 'filter_block_settings' ], 10, 2 );
    }

    public static function get_instance() {
        if( is_null( self::$instance ) )
            self::$instance = new self();

        return self::$instance;
    }

    public function filter_block_settings( $args, $data ) {
        if( $data['name'] !== 'core/latest-posts' )
            return $args;

        $this->original_render_callback = $args['render_callback'];
        $args['render_callback']        = [ $this, 'render_block' ];

        return $args;
    }

    public function latest_posts_query_types( $query ) {
        if( empty( $this->block_attributes['postTypes'] ) )
            return;

        $public_types = get_post_types(
            [
                'show_in_rest' => true,
            ]
        );

        $types = array_intersect( $public_types, $this->block_attributes['postTypes'] );

        $query->set( 'post_type', $types );
    }

    public function render_block( $attributes, $block_content, $block ) {
        $this->block_attributes = $attributes;

        add_filter( 'pre_get_posts', [ $this, 'latest_posts_query_types' ] );

        $block_content = (string) call_user_func( $this->original_render_callback, $attributes, $block_content, $block );

        remove_filter( 'pre_get_posts', [ $this, 'latest_posts_query_types' ] );

        return $block_content;
    }
}

WPSE326869_LatestPostsPostTypes::get_instance();

Summary & Point of Failure

What Works

All of the above in place, we end up with a function “Post Types” control added to the Latest Post block’s sidebar settings (with autocomplete suggestions, to boot!) which stores it’s values in a new attribute added to the block, and uses that attribute to appropriately adjust the block’s server-side query.

At this point, the modified block will successfully display post-type filtered latest posts on the front-end!

What Doesn’t

Back in the Block Editor, however, there’s a little wonkiness afoot. It all boils down to the edit() component using a REST API request hard-coded for the post post-type in order to quickly deliver visual updates in the editor instead of the block’s PHP render callback. The result is that the block does not display posts of the selected types within the editor.

You might think to use more pre_get_posts hackery to modify the results of that query – but doing so could well lead to the Gutenberg data stores incorrectly classifying the results as posts, which could cause all sorts of other issues.

The only functional approach I can see would be to totally abuse the editor.BlockEdit filter and completely replace the block’s edit() component. While that should be possible on paper, I feel it would be such an egregious no-no that I will not explore or demonstrate the possibility. Silently swapping out the implementation of core functionality on-the-fly is a sure-fire way to create bugs, confusion, and chaos.

What Might, Someday

I haven’t dug into it too far, but the PRs which introduced the Latest Posts Block and the Server Side Rendering solution both heavily reference each other. I haven’t researched why the Latest Posts block ultimately did not end up using the SSR component for rendering in the Block Editor. If it ever did however, I believe the solution implemented above would be fully functional.

It’s also possible that core will introduce more applicable block filters or even additional props to the Latest Posts block which would make this more plausible.

Leave a Comment