advanced-query-loop

ACF Bidirectional Relationship + Kadence Query Loop Recipe

A step-by-step guide for creating custom queries that filter posts based on ACF relationship fields using Kadence Advanced Query Loop blocks.

Prerequisites

  • Kadence Blocks Pro (for Advanced Query Loop)
  • ACF (Advanced Custom Fields) with relationship field
  • Code Snippets plugin (or access to functions.php)
  • Two related post types (example: person and story)

Step 1: Set Up Your ACF Relationship Field

  1. Create a bidirectional relationship field in ACF
  2. Set it to connect your two post types (person ↔ story)
  3. Note the field name (example: person_story_relationship)

Step 2: Create Your Advanced Query Loop Block

  1. Add an Advanced Query Loop block to your template
  2. Design your query card layout (how each post should look)
  3. Find the block ID:
    • Go to Kadence Blocks > All Queries
    • Hover over your query’s edit link
    • Note the ID from the browser status bar (example: 1076)

Step 3: Create the Custom Query Function

Add this code via Code Snippets (or functions.php):

phpadd_filter( 'kadence_blocks_pro_query_loop_query_vars', function( $query, $ql_query_meta, $ql_id ) {
    
    // Replace 1076 with your actual block ID
    if ( $ql_id == 1076 ) {
        
        $person_id = get_queried_object_id();
        $person_name = get_the_title($person_id);
        $first_name = explode(' ', $person_name)[0];
        
        // Replace 'person_story_relationship' with your ACF field name
        $connected_stories = get_field('person_story_relationship', $person_id);
        
        // If no connected posts, show a message
        if (empty($connected_stories) || !is_array($connected_stories)) {
            $no_results_post = new stdClass();
            $no_results_post->ID = 999998;
            $no_results_post->post_title = $first_name . " doesn't have any archived stories yet.";
            $no_results_post->post_content = '';
            $no_results_post->post_excerpt = '';
            $no_results_post->post_status = 'publish';
            $no_results_post->post_type = 'story'; // Replace with your target post type
            $no_results_post->post_date = current_time('mysql');
            $no_results_post->post_author = 0;
            
            add_filter( 'posts_pre_query', function( $posts, $wp_query ) use ( $no_results_post ) {
                return array( $no_results_post );
            }, 10, 2 );
            
            $query['post_type'] = 'story'; // Replace with your target post type
            $query['posts_per_page'] = 1;
        } else {
            // Show connected posts
            $query['post_type'] = 'story'; // Replace with your target post type
            $query['post_status'] = 'publish';
            $query['post__in'] = $connected_stories;
            $query['posts_per_page'] = -1; // Show all connected posts
        }
    }
    
    return $query;
}, 10, 3 );

Step 4: Customize for Your Use Case

Replace these values with your specifics:

  • Block ID: Change 1076 to your query loop’s actual ID
  • ACF Field Name: Change person_story_relationship to your field name
  • Post Types: Change story to your target post type
  • No Results Message: Customize the message text as needed

Step 5: Test and Troubleshoot

Testing Steps

  1. Test on a page with connected posts – should show filtered results
  2. Test on a page with no connections – should show your custom message
  3. Clear all caches – very important for seeing changes

Common Issues

Filter not working at all:

  • Check you’re using kadence_blocks_pro_query_loop_query_vars (note the _pro_)
  • Verify the block ID is correct
  • Ensure Code Snippets is activated

Wrong posts showing:

  • Verify your ACF field name is correct
  • Check that get_queried_object_id() returns the right page ID
  • Test what get_field() returns with debug code

Template not applying:

  • Clear all caches (browser, plugin, server)
  • Verify the query loop block design is saved properly

Debug Code Template

If something isn’t working, use this debug code to investigate:

phpadd_filter( 'kadence_blocks_pro_query_loop_query_vars', function( $query, $ql_query_meta, $ql_id ) {
    
    if ( $ql_id == YOUR_BLOCK_ID ) {
        
        $page_id = get_queried_object_id();
        $connected_posts = get_field('YOUR_FIELD_NAME', $page_id);
        $debug_info = is_array($connected_posts) ? 'array_' . count($connected_posts) : 'not_array';
        
        $test_post = new stdClass();
        $test_post->ID = 999999;
        $test_post->post_title = 'DEBUG: Page ' . $page_id . ' has ' . $debug_info;
        $test_post->post_content = 'Connected IDs: ' . (is_array($connected_posts) ? implode(', ', $connected_posts) : 'none');
        $test_post->post_excerpt = '';
        $test_post->post_status = 'publish';
        $test_post->post_type = 'YOUR_POST_TYPE';
        $test_post->post_date = current_time('mysql');
        $test_post->post_author = 0;
        
        add_filter( 'posts_pre_query', function( $posts, $wp_query ) use ( $test_post ) {
            return array( $test_post );
        }, 10, 2 );
        
        $query['post_type'] = 'YOUR_POST_TYPE';
        $query['posts_per_page'] = 1;
    }
    
    return $query;
}, 10, 3 );

Key Success Factors

  1. Correct Filter Name: Must be kadence_blocks_pro_query_loop_query_vars
  2. Accurate Block ID: Get it from Kadence Blocks > All Queries
  3. Right ACF Field Name: Match exactly what you set in ACF
  4. Cache Clearing: Always clear caches when testing
  5. Context Awareness: Use get_queried_object_id() to get the current page ID

Multiple Post Types (Stories + Interviews)

If you need to query multiple related post types on the same page, you can handle them in one filter function:

Setup

  1. Create separate Advanced Query Loop blocks for each post type
  2. Note each block’s unique ID (example: stories = 1076, interviews = 1234)
  3. Create separate ACF relationship fields (or use the same field if they should share connections)

Combined Filter Code (Conflict-Free)

phpadd_filter( 'kadence_blocks_pro_query_loop_query_vars', function( $query, $ql_query_meta, $ql_id ) {
    
    // Stories query (block ID 1076)
    if ( $ql_id == 1076 ) {
        $person_id = get_queried_object_id();
        $person_name = get_the_title($person_id);
        $first_name = explode(' ', $person_name)[0];
        $connected_stories = get_field('person_story_relationship', $person_id);
        
        if (empty($connected_stories) || !is_array($connected_stories)) {
            $no_results_post = new stdClass();
            $no_results_post->ID = 999998; // Unique ID for stories
            $no_results_post->post_title = $first_name . " doesn't have any archived stories yet.";
            $no_results_post->post_content = '';
            $no_results_post->post_excerpt = '';
            $no_results_post->post_status = 'publish';
            $no_results_post->post_type = 'story';
            $no_results_post->post_date = current_time('mysql');
            $no_results_post->post_author = 0;
            
            // IMPORTANT: Use specific markers to prevent conflicts
            add_filter( 'posts_pre_query', function( $posts, $wp_query ) use ( $no_results_post ) {
                if ( $wp_query->get( 'post_type' ) === 'story' && $wp_query->get( 'stories_empty_marker' ) ) {
                    return array( $no_results_post );
                }
                return $posts;
            }, 10, 2 );
            
            $query['post_type'] = 'story';
            $query['posts_per_page'] = 1;
            $query['stories_empty_marker'] = true; // Unique marker prevents conflicts
        } else {
            $query['post_type'] = 'story';
            $query['post_status'] = 'publish';
            $query['post__in'] = $connected_stories;
            $query['posts_per_page'] = -1;
        }
    }
    
    // Interviews query (block ID 1234)
    if ( $ql_id == 1234 ) {
        $person_id = get_queried_object_id();
        $person_name = get_the_title($person_id);
        $first_name = explode(' ', $person_name)[0];
        $connected_interviews = get_field('person_interview_relationship', $person_id);
        
        if (empty($connected_interviews) || !is_array($connected_interviews)) {
            $no_results_post = new stdClass();
            $no_results_post->ID = 999997; // Different ID for interviews
            $no_results_post->post_title = $first_name . " doesn't have any archived interviews yet.";
            $no_results_post->post_content = '';
            $no_results_post->post_excerpt = '';
            $no_results_post->post_status = 'publish';
            $no_results_post->post_type = 'interview';
            $no_results_post->post_date = current_time('mysql');
            $no_results_post->post_author = 0;
            
            // IMPORTANT: Use specific markers to prevent conflicts
            add_filter( 'posts_pre_query', function( $posts, $wp_query ) use ( $no_results_post ) {
                if ( $wp_query->get( 'post_type' ) === 'interview' && $wp_query->get( 'interviews_empty_marker' ) ) {
                    return array( $no_results_post );
                }
                return $posts;
            }, 10, 2 );
            
            $query['post_type'] = 'interview';
            $query['posts_per_page'] = 1;
            $query['interviews_empty_marker'] = true; // Unique marker prevents conflicts
        } else {
            $query['post_type'] = 'interview';
            $query['post_status'] = 'publish';
            $query['post__in'] = $connected_interviews;
            $query['posts_per_page'] = -1;
        }
    }
    
    return $query;
}, 10, 3 );

Key Points for Multiple Post Types

  • One filter handles all blocks – cleaner than multiple separate filters
  • Each block needs unique ID – target them individually with if ( $ql_id == XXXX )
  • Use different fake post IDs – prevents conflicts between no-results messages (999998, 999997, etc.)
  • CRITICAL: Use unique markers – each post type needs its own marker (stories_empty_marker, interviews_empty_marker) to prevent posts_pre_query conflicts
  • Separate ACF fields recommended – unless you want shared relationships

Adding More Post Types

To add additional post types (photos, documents, etc.), follow this pattern:

php// Photos query (block ID 1345)
if ( $ql_id == 1345 ) {
    $person_id = get_queried_object_id();
    $person_name = get_the_title($person_id);
    $first_name = explode(' ', $person_name)[0];
    $connected_photos = get_field('person_photo_relationship', $person_id);
    
    if (empty($connected_photos) || !is_array($connected_photos)) {
        $no_results_post = new stdClass();
        $no_results_post->ID = 999996; // Unique ID
        $no_results_post->post_title = $first_name . " doesn't have any archived photos yet.";
        $no_results_post->post_content = '';
        $no_results_post->post_excerpt = '';
        $no_results_post->post_status = 'publish';
        $no_results_post->post_type = 'photo';
        $no_results_post->post_date = current_time('mysql');
        $no_results_post->post_author = 0;
        
        add_filter( 'posts_pre_query', function( $posts, $wp_query ) use ( $no_results_post ) {
            if ( $wp_query->get( 'post_type' ) === 'photo' && $wp_query->get( 'photos_empty_marker' ) ) {
                return array( $no_results_post );
            }
            return $posts;
        }, 10, 2 );
        
        $query['post_type'] = 'photo';
        $query['posts_per_page'] = 1;
        $query['photos_empty_marker'] = true; // Unique marker
    } else {
        $query['post_type'] = 'photo';
        $query['post_status'] = 'publish';
        $query['post__in'] = $connected_photos;
        $query['posts_per_page'] = -1;
    }
}

Remember: Each new post type needs:

  1. Unique block ID
  2. Unique fake post ID (999996, 999995, etc.)
  3. Unique marker name (photos_empty_marker, documents_empty_marker, etc.)
  4. Corresponding ACF relationship field

Final Notes

  • This method works for any ACF relationship field setup
  • The query preserves your designed template layout
  • Custom “no results” messages provide better UX than empty sections
  • Always test on both connected and unconnected posts
  • You can add as many post types as needed by following the same pattern

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *