Relationship field filter - use slug as url parameter instead of ID?

I’ve got a relationship field on a Projects CPT which connects the associated Services, returning a Post Object.

On my Projects page, I’ve set up a filter, which uses the ACF field as the meta key and works correctly to filter the projects by service.

However, when the filter is applied, the appended url parameter displays the service post ID (e.g. ?_service=169). Is there a way to have this display the service name/slug instead to make it slightly more readable (e.g. ?_service=web-design)? Even if the filter query is still using the ID behind the scenes.

How do you have your permalinks setup? Are you using post name?

Yep, using post name. So typical service page url structure is …/service/web-design/

All works for me … this is my test setup: Watch 2026-02-17 16-00-22 | Streamable

Great, thanks for the video - definitely looks to be working there with Metabox. Makes me wonder if this is something to do with ACF’s implementation of the relationship field then, rather than Bricks.

Hmm, good question, I wonder. I will do a test in the morning with ACF and see if I get the same result or if I get the same issue you are facing. Does that work for you?

that would be great, thank you so much

Hey there,

Ok, so I tried your approach with ACF/SCF. The issue is that ACF relationship fields store term IDs, not slugs or names. Bricks’ Select Filter is passing these IDs directly to the URL. When you filter by a taxonomy category directly, Bricks knows to use the term slug, but with a relationship field, it’s just passing the stored value (the ID).

I have tried a couple code options, but the URL parameter is always the ID. So unless someone else has a cool ACF hack for this, I feel the better approach would be to use a custom Taxonomy, as they have better URL structure/cleaner, native filtering, better performance, and works amazing with Bricks.

Below you can see the video I did showing you how I could replicate the issue you were having (the reason why above) and a solution.

Video: https://drive.google.com/file/d/1p9Ll5sAi2rygYKAIT-UnwC-6kSZUgsk4/view?usp=sharing

Let me know.

1 Like

Thank you so much for spending the time looking into this - I really appreciate the video. I definitely agree that taxonomies are the ‘cleaner’ option to use most of the time, and they’re so easy to set up in Bricks.

I ended up spending a few hours yesterday coming up with a pretty convoluted, hacky solution which uses a few Bricks filters to do what you allude to in the video - capture the ID, convert it into a slug for the URL, and back into the ID for the actual query itself.

I’m sharing below should anyone else have the same issue. It’s probably not a great approach, but it’s working for now at least. In most cases though, like you say I think it’s better off to just use a taxonomy field or accept the ID in the URL. Thanks again for your help.

<?php
/**
 * ACF Relationship Filter – Human-readable URL Parameters
 *
 * Bricks Builder filters using ACF relationship fields store and transmit post IDs,
 * resulting in ugly URL parameters like ?_service=161. This code replaces those
 * IDs with readable slugs (e.g. ?_service=content-creation) across all filter
 * interactions and direct URL navigation.
 *
 * Before using this code, replace the following placeholders:
 *   [FILTER_ELEMENT_ID]  – Bricks element ID of your filter select element
 *   [QUERY_ELEMENT_ID]   – Bricks element ID of your query loop element
 *   [URL_PARAM_NAME]     – The URL parameter name assigned to your filter (e.g. _service)
 *   [POST_TYPE_SLUG]     – The post type slug of the related posts (e.g. service)
 */


// 1. Replace filter option values with post slugs instead of IDs
add_filter( 'bricks/filter_element/populated_options', function( $options, $element ) {
    if ( $element->id !== '[FILTER_ELEMENT_ID]' ) {
        return $options;
    }

    foreach ( $options as &$option ) {
        if ( ! empty( $option['is_all'] ) || ! empty( $option['is_placeholder'] ) ) {
            continue;
        }

        $post = get_post( $option['value'] );

        if ( $post && ! is_wp_error( $post ) ) {
            $option['value'] = $post->post_name;
        }
    }

    return $options;
}, 10, 2 );


// 2. Intercept Bricks REST API requests to translate between slugs and IDs as needed
add_filter( 'rest_pre_dispatch', function( $result, $server, $request ) {
    if ( $request->get_route() !== '/bricks/v1/query_result' ) {
        return $result;
    }

    $selected_filters = $request->get_param( 'selectedFilters' );

    if ( empty( $selected_filters ) || ! is_array( $selected_filters ) || empty( $selected_filters['[FILTER_ELEMENT_ID]'] ) ) {
        return $result;
    }

    $value = $selected_filters['[FILTER_ELEMENT_ID]'];

    if ( is_numeric( $value ) ) {
        // ID → slug: so the re-rendered select element shows the correct selected option
        $post = get_post( (int) $value );
        if ( $post && ! is_wp_error( $post ) ) {
            $selected_filters['[FILTER_ELEMENT_ID]'] = $post->post_name;
            $request->set_param( 'selectedFilters', $selected_filters );
        }
    } else {
        // Slug → ID: so the ACF relationship meta query finds results in the database
        $post = get_page_by_path( sanitize_title( $value ), OBJECT, '[POST_TYPE_SLUG]' );
        if ( $post ) {
            $selected_filters['[FILTER_ELEMENT_ID]'] = (string) $post->ID;
            $request->set_param( 'selectedFilters', $selected_filters );
        }
    }

    return $result;
}, 10, 3 );


// 3. JS: restore select displayed value after AJAX, and trigger filter on page load from URL param
add_action( 'wp_footer', function() {
    ?>
    <script>
    (function() {

        // After each filter query, re-apply the correct selected value to the select element
        document.addEventListener('bricks/ajax/query_result/displayed', function(e) {
            if (e.detail.queryId !== '[QUERY_ELEMENT_ID]') return;

            const filterInstance = window.bricksData?.filterInstances?.['[FILTER_ELEMENT_ID]'];
            if (!filterInstance?.currentValue) return;

            const select = document.querySelector('.brxe-filter-select[name="form-field-[FILTER_ELEMENT_ID]"]');
            if (!select) return;

            if (select.querySelector('option[value="' + filterInstance.currentValue + '"]')) {
                select.value = filterInstance.currentValue;
            }
        });

        // On page load, detect a slug in the URL and trigger the filter query
        document.addEventListener('DOMContentLoaded', function() {
            const slug = new URL(window.location.href).searchParams.get('[URL_PARAM_NAME]');
            if (!slug) return;

            // Hide "no results" message until the query completes to prevent a flash
            document.body.classList.add('service-filter-loading');
            document.addEventListener('bricks/ajax/query_result/displayed', function handler() {
                document.body.classList.remove('service-filter-loading');
                document.removeEventListener('bricks/ajax/query_result/displayed', handler);
            });

            // Small delay to allow Bricks to finish its own DOMContentLoaded setup
            setTimeout(function() {
                const filterInstance = window.bricksData?.filterInstances?.['[FILTER_ELEMENT_ID]'];
                const queryInstance  = window.bricksData?.queryLoopInstances?.['[QUERY_ELEMENT_ID]'];

                if (!filterInstance || !queryInstance) {
                    document.body.classList.remove('service-filter-loading');
                    return;
                }

                filterInstance.currentValue = slug;
                bricksUtils.updateSelectedFilters('[QUERY_ELEMENT_ID]', filterInstance);
                bricksUtils.fetchFilterResults('[QUERY_ELEMENT_ID]');
            }, 100);
        });

    })();
    </script>
    <?php
} );

No worries, glad to help where I can. Sorry I wasn’t able to provide a solution exactly, but you outcome sure looks like it fits your specific needs – and at the end of the day, if it is doing what you need, great job! =]