How to filter WordPress posts by presence of custom Gutenberg blocks

I’m working on a WordPress site where I need to filter posts based on whether they contain a particular Gutenberg block. I’ve managed to create a custom column in the admin that shows which posts have the block I’m looking for.

The issue is with the actual filtering functionality. I can display the dropdown menu and show the column data, but I can’t figure out how to make the query work properly. Most examples I find online deal with regular post meta fields, not block detection.

// Custom column for admin posts table
function add_block_status_column( $columns ) {
    $columns['has_special_block'] = 'Contains Block';
    return $columns;
}
add_filter( 'manage_post_posts_columns', 'add_block_status_column' );

// Display block status in column
function show_block_status() {
    if ( has_block( 'custom/special-content' ) ) {
        $status = "Yes";
        echo "$status";
    } else {
        $status = "No";
        echo "$status";
    }
}
add_action( 'manage_posts_custom_column', 'show_block_status', 10, 2 );

// Add filter dropdown
function add_block_filter_dropdown( $post_type ) {
    if ( $post_type === 'post' ) {
        $selected_value = filter_input( INPUT_GET, 'has_special_block' );
        ?>
        <select name="has_special_block">
            <option value="">All Posts</option>
            <option value="Yes" <?php echo selected( 'Yes', $selected_value ); ?>>With Block</option>
            <option value="No" <?php echo selected( 'No', $selected_value ); ?>>Without Block</option>
        </select>
        <?php
    }
}
add_action( 'restrict_manage_posts', 'add_block_filter_dropdown', 10, 2 );

This is where I’m stuck. The filtering part doesn’t work:

// Attempt to filter posts based on block presence
function filter_posts_by_block( $query ) {
    $filter_value = filter_input( INPUT_GET, 'has_special_block' );
    
    if ( is_admin() && ! empty( $filter_value ) ) {
        switch ( $filter_value ) {
            case 'Yes':
                // Need to show only posts with the block
                break;
            case 'No':
                // Need to show only posts without the block
                break;
        }
    }
}
add_action( 'pre_get_posts', 'filter_posts_by_block' );

How can I properly modify the query to filter posts based on block presence? Is there a way to store this information as post meta or do I need a different approach?

you could also use a direct database query approach if you dont want to mess with post meta. gutenberg blocks are stored in post_content so you can search for your block pattern directly:

function filter_posts_by_block( $query ) {
    global $wpdb;
    $filter_value = filter_input( INPUT_GET, 'has_special_block' );
    
    if ( is_admin() && !empty( $filter_value ) ) {
        if ( $filter_value === 'Yes' ) {
            $query->set( 'meta_query', array(
                'key' => 'post_content',
                'value' => 'wp:custom/special-content',
                'compare' => 'LIKE'
            ));
        }
    }
}

bit hacky but works without extra meta storage

In your implementation, the key issue arises from not having the block presence data appropriately stored for query purposes. While has_block() is effective for display, it cannot be relied upon when it comes to querying the database.

To rectify this, consider storing the block presence as post meta whenever the post is saved. This method provides a reliable way to query the presence of blocks in conjunction with pre_get_posts.

Here’s an example you can use:

// Store block presence as post meta on save
function store_block_presence_meta( $post_id ) {
    if ( wp_is_post_revision( $post_id ) ) return;
    
    $post = get_post( $post_id );
    $has_block = has_block( 'custom/special-content', $post );
    
    update_post_meta( $post_id, '_has_special_block', $has_block ? 'yes' : 'no' );
}
add_action( 'save_post', 'store_block_presence_meta' );

// Adjust your filter query accordingly
function filter_posts_by_block( $query ) {
    if ( ! is_admin() || ! $query->is_main_query() ) return;
    
    $filter_value = filter_input( INPUT_GET, 'has_special_block' );
    
    if ( ! empty( $filter_value ) ) {
        $meta_value = ( $filter_value === 'Yes' ) ? 'yes' : 'no';
        $query->set( 'meta_key', '_has_special_block' );
        $query->set( 'meta_value', $meta_value );
    }
}
add_action( 'pre_get_posts', 'filter_posts_by_block' );

Make sure to run this on existing posts to initialize the meta fields.