4

When adding the "Latest Posts" block within Gutenberg, is it possible to filter by custom post type?

Here are the options I can see:

enter image description here

Would I need to create a custom block for this type of functionality?

I've found a good discussion here, but I've ended up down a bit of GitHub rabbit hole.

1

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.

2
  • Wow, @bosco, what a fantastic answer! 🙂 Thank you so so much for taking the time to answer in detail, I can see you put a lot of time into this.
    – Sam
    Jun 16 '21 at 18:09
  • 1
    It was an excellent question, well worth researching! I learned a lot from it :)
    – bosco
    Jun 16 '21 at 18:15
2

Why not create your own custom post display block using this easy to use plugin lazyblock https://wordpress.org/plugins/lazy-blocks/ ?

  1. Simple steps are here, first create your custom loop using wp_query, you can take help of https://generatewp.com/wp_query/ here is my custom loop for portfolio post type I had created

    // WP_Query arguments
    $args = array(
        'post_type' => array( 'uni_portfolio' ),
    );
    
    // The Query
    $query = new WP_Query( $args );
    // The Loop
    if ( $query->have_posts() ) {
        while ( $query->have_posts() ) {
            $query->the_post();
            // do something
        }
    } else {
        // no posts found
    }
    // Restore original Post Data
    wp_reset_postdata();
    
  2. Then create a new blank block in lazy block plugin, its very simple and you can look at documentation here https://lazyblocks.com/documentation/blocks-creation/ , you can add few fields as per your need to, for example how many post to display etc.

  3. Then add just this function utilizing your above wp_query to create output of your block, I have used one field number of post to control post count in the block

add_filter( 'lazyblock/custom-post-loop/frontend_callback', 'uni_block_output_custompostloop', 10, 2 );
if ( ! function_exists( 'uni_block_output_custompostloop' ) ) :
        function uni_block_output_custompostloop( $output, $attributes ) {
            ob_start();
    // your query start from here
                $args = array( 'post_type' => array( 'uni_portfolio' ), 'posts_per_page' => esc_html( $attributes['uni_number_of_post'] ), );
                $query = new WP_Query( $args );
                    if ( $query->have_posts() ) {
                        while ( $query->have_posts() ) {
                            $query->the_post();
                            if ( has_post_thumbnail()) : // Check if thumbnail exists
                                the_post_thumbnail();
                            endif;
                            ?>
                            <?php the_terms($post->ID, 'portfolio_category') ?>
                            <h3><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
                        <?php
                        }
                    } else {
                        echo "<div>no posts found</div>";
                    }
                wp_reset_postdata();
    // your query code end here
            return ob_get_clean();
        }
endif;

Notice lazyblock/custom-post-loop is the slug of the block you created using lazyblock plugin

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.