What is the Listing Builder?

FacetWP’s Listing Builder allows you to quickly and visually design a results listing.

It’s a convenient alternative to building your own WP archive pages or Custom WP_Query loops.

After building the listing template, you can display it on any WordPress page with a shortcode, or include it in an archive page with a PHP include. Then put any number of facets on the page to interact with it.

It is even possible to have multiple listing templates on the same page: one being the dynamic listing that the facets on the page interact with, the others being “static” listings.

If you prefer to code your post loops and query arguments by hand, you can still take advantage of the Listing Builder by using its Dev mode.

Create a listing template

Browse to Settings > FacetWP, click the “Listings” tab and then “Add new”.

The Listing Builder is covered in the video below, starting at 1:49.

Using Dev mode

FacetWP Listing Builder Dev ModeNew templates use the “visual” Listing Builder by default. However, if you need more flexibility and you’re experienced in WordPress coding, you can switch both the Display tab and Query tab independently from each other to “Dev mode”, by clicking the checkbox on the right.

In Dev mode, you can use PHP to build the listing template output and the query arguments.

Note that although you can freely switch each tab between the “visual mode” and Dev mode, the two modes are not related (except momentarily when you use the Convert to query args button). For the Display tab as well as the Query tab, a listing template uses one mode or the other: the mode in which you leave the tabs when you save the listing template, is used.

You can also mix modes. For example, you can use visual mode in the Display tab and Dev mode in the Query tab.

Using the Query tab

The Query tab offers an easy visual alternative to manually building a WP query with PHP. You can define which results (posts or other post types) should be fetched in the initial post listing, before any filtering is applied by the user. You can also set the number of posts per page and the sorting method, and apply filtering rules to narrow the results.

Set the post types to fetch

Listing builder - Query tab - fetch post types.
Fetch post types in the Query tab of the Listing Builder.

In the first line of the settings you can select one or more post types that have to be fetched.

Make sure that these post types are “searchable”, meaning that the exclude_from_search parameter of the register_post_type() function is set to false. Otherwise your posts will not get indexed.

Set the number of posts per page

Listing builder - Query tab - fetch post types.
Set posts per page in the Query tab of the Listing Builder.

Next, if you intend to use pagination (for example with a Pager facet), set the number of posts per page.

It is advisable to set the number to something reasonable, like 15-20 posts max. This will reduce the number of queries needed to load the data for all displayed posts, which will speed up filtering. If you use -1, all posts will be retrieved. Use this only if you have a limited number of posts. If you leave the field empty, the default number of posts per page is 10.

Add query sorting rules

Listing builder - Query tab - define sorting rules.
Define one or more sorting rules in the Query tab of the Listing Builder.

Click the “Add query sort” button to add a sorting rule. A sorting rule consists of a (custom) field to initially sort the listing by, and the sort order. ASC means ascending, e.g. A-Z. DESC means descending, e.g. Z-A.

The defined order determines the initial sort order (on page load or after using a Reset facet). If you add a Sort facet, you can offer the user other ways of sorting the listing.

You can add multiple sorting rules, by clicking the “Add query sort” button again. The sorting rules are applied in the order you define them, so subsequent rules can be used to apply a backup sort order. For example, if you want to sort by post date, but multiple posts share the same post date, you can sort by post date first, and by post title second.

To delete a sorting rule, click the red minus button on the right.

Add query filtering rules

Click the “Add query filter” button to add a filtering rule. With a filtering rule, you can narrow the posts that are shown in the listing.

Listing builder - Query tab - define filtering rules.
Define one or more filtering rules in the Query tab of the Listing Builder.

A filtering rule consists of:

  1. The (custom) field that will be used to compare against.
  2. A type selector (which appears for some customs fields). The possible values TEXT, NUMERIC, or DATE determine which compare operators are possible, and the way the values will be compared. See below for more info about these field types.
  3. A compare operator.
  4. One or more values to be used in the comparison.

You can add multiple filtering rules, by clicking the “Add query filter” button again. When multiple filtering rules are used, the logic operator between them is AND.

To delete a filtering rule, click the red minus button on the right.

Using compare operators

The table below gives an overview of the available compare operators and their behavior.

The available options depend on the type of source field you have selected. If you select a custom field as source, the options will depend on the field type you set: TEXT, NUMERIC, or DATE. For example, the > and < operators are not available when you use a custom field with TEXT set as it field type.

Operator Behavior if multiple values Comparison description
= OR The field EXISTS for the post, and the value IS [x] (OR [y])
!= AND The field EXISTS for the post, and the value is NOT [x] (AND is NOT [y])
> Only first value is used The field EXISTS for the post, and the value is GREATER THAN [x]
>= Only first value is used The field EXISTS for the post, and the value is GREATER THAN OR EQUAL to [x]
< Only first value is used The field EXISTS for the post, and the value is LESSER THAN [x]. Note: posts for which the field exists but is empty are considered to have a value of 0, so they will appear with this comparison.
<= Only first value is used The field EXISTS for the post, and the value is LESSER THAN OR EQUAL to [x]. Note: posts for which the field exists but is empty are considered to have a value of 0, so they will appear with this comparison.
IN OR The field EXISTS for the post, and the value is IN the list of values provided. Note: this is functionally the same as using the = operator with multiple values.
NOT IN AND The field EXISTS for the post, and the value is is NOT IN the list of values provided. Note: this is functionally the same as using the != operator with multiple values.
EXISTS n.a. The field EXISTS for the post (and the value is empty or not empty). Note: that a field exists in the settings of e.g. Advanced Custom Fields or Custom Fields Suite, does not mean the field exists for each post. A post needs to be saved for a custom field to exist for that post.
NOT EXISTS n.a. The field does NOT EXIST for the post. E.g. because the post has not been saved: see EXISTS.
EMPTY n.a. The field EXISTS for the post and is EMPTY.
NOT EMPTY n.a. The field EXISTS for the post and is NOT EMPTY.

Entering values to compare against

When adding values, make sure they are saved properly, otherwise the query filter will not work. Make sure that after entering a value, you always click the value in the dropdown that appears, or use “Enter”. A correctly entered value has an individual gray border and background with an “x” button. Click “Save changes” afterward.

Listing builder - Query tab - query filter - entering values correctly.
How to correctly enter values for a query filter in the Query tab of the Listing Builder.

Comparing text, numeric and date values

In most cases, if you select a custom field as source field, a type selector will appear. The options TEXT, NUMERIC, or DATE determine the field type and the comparison operators that can be used.

Listing Builder - Query tab - query filter field type selector.
Using the field type selector in a query filter in the Query tab of the Listing Builder.
Comparing numerical values

If you set the field type to NUMERIC, you specify the comparison to be between numerical values. Numbers are internally compared as decimals (DECIMAL(16,4)), so you can compare numbers with a precision of up to 4 decimals.

Note that the decimals in the source field and the comparison values both need to have a dot as decimal separator. Currently, FacetWP’s “Separator” setting is ignored in these comparison rules. A comma will not work: decimals behind a comma are ignored: 10,3 is interpreted as 10, both for the source field and for the values to compare against.

Comparing date values

If you want to narrow the post listing by using dates, there are only four comparison operators available: <, <=, >, and >=. In these, < means: before a certain date, and > after a certain date.

For the source field, you can use the Post Date, or any other custom field, as long as you set the field type selector to DATE.

The date value in the selected source field needs to be in “YYYY-MM-DD” format (Y-m-d in PHP).

The date value you are entering yourself on the right side of the filtering rule, can be any valid date/time string format that is accepted by PHP’s strtotime() function. Of course,YYYY-MM-DD will work, but a lot of other formats will work too, including relative formats. So these will all work (individually):

Listing builder - Query tab - query filter - valid date values.
Valid date values for a query filter in the Query tab of the Listing Builder.

Convert to query arguments

Listing builder - Query tab - convert to query args.
Convert to query args button in the Query tab of the Listing Builder.

When you click the “Convert to query args” button, the Query tab is switched to “Dev mode” and the setting you have entered are converted to PHP query arguments.

In Dev mode, you can manually adapt the converted query arguments and add others.

This can be useful if your desired query is more complicated and you want to manually build a query with WP_Query arguments that are not covered in the Listing Builder’s settings.

Be aware that the button works only one way: it converts the visual settings to PHP code only at the moment you click. You can switch back to visual mode, but the two modes are independent. The mode you leave the Query tab in when you click “Save changes” is the mode that will be used for the listing template’s query.

Using dynamic tags

Within the Listing Builder you can use so-called “dynamic tags”, which are very useful in building more sophisticated layouts of the retrieved individual posts. A dynamic tag lets you pull in all kinds of post data, to be used in for example an HTML builder item or in a builder item’s setting field.

Besides the set of built-in tags the Listing Builder comes with, you can use your own tags, created from custom fields within the Listing Builder itself.

For more complex needs it is also possible to create custom tags by modifying the output of existing tags or by writing them from scratch.

Built-in dynamic tags

The following built-in tags are available:

Tag Output
{{ post:id }} Returns the post’s id
{{ post:title }} Returns the post’s title
{{ post:name }} Returns the post’s name (slug)
{{ post:type }} Returns the post’s type
{{ post:url }} Returns the post’s url
{{ post:image }} Returns the post’s featured image (in “full” size).
If you need another WordPress image size, you can can use the facetwp_builder_dynamic_tag_value hook.

Create dynamic tags within the Listing Builder

By creating a dynamic tag in the Listing Builder itself, you can pull in a post’s custom field value into a builder item’s field or setting.

The way this works is that you create a hidden builder item from the custom field you want to pull the value from, and use its “Unique name” as the dynamic tag name, to be used in other builder items’ fields. This may sound complicated but it’s actually very easy:

Let’s say you have a photo_url custom field and you want to wrap it into an <a> tag:

  1. First, add a new builder item and select the desired field:
Selecting a custom field as item source and creating a dynamic tag from the item's name.
Selecting a custom field as item source and creating a dynamic tag from the item’s unique name.
  1. Next, enter photo-url in the builder item’s “Unique name” field and click the “Hide item” checkbox. In the builder, the item will become grayed out with a “hidden” icon next to it, and this item will be hidden on the front-end. We hide it because this builder item is only used to generate the dynamic tag: the content of its “Unique name” field becomes the dynamic tag: {{ photo-url }}.
  2. Finally, add a new HTML builder item and set its “Content” to:
<a href="{{ photo-url }}">{{ photo-url }}</a>
Adding a custom dynamic tag to the Content field of an HTML item.
Adding a custom dynamic tag to the “Content” field of an HTML item.

FacetWP will now automatically replace all instances of {{ photo-url }} with the custom field’s value. You can even pull in multiple fields.

Create or modify custom dynamic tags

It is also possible to modify existing dynamic tags or create your own custom tags from scratch.

There are two hooks available for this (with the second one being the more efficient of the two):

You could for example create a tag to pull in content from a custom field and modify it. Or a tag to output the featured image url in any (default or custom) WordPress image size.

Besides modifying the output of built-in tags, you can also modify the dynamic tags you made within the Listing Builder itself. An example of this would be to modify a dynamic tag that pulls in data from a Price custom field, so that the output value of the tag is prepended with a currency sign.

See the above hook pages for more examples.

Display a listing template

Copy the shortcode

Copy shortcode buttonCopy shortcode in listings overviewAfter creating the listing template, click the red “Copy shortcode” button.

It’s also possible to copy shortcodes directly from the Listings screen. Click the cog/gear icon on the right side of the listing’s row and click “Copy shortcode” in the dropdown.

You can also create your shortcode manually, using the listing name (the name in the grey box). Each listing’s shortcode looks like this:

[facetwp template="the_listing_name"]

Place the shortcode

How to past a facet or listing shortcode into a WordPress Gutenberg Shortcode widget block.
How to past a facet or listing shortcode into a WordPress Gutenberg Shortcode widget block.

Paste the shortcode into the body field of your page, or into a Text widget (Appearance > Widgets).

If you’re using the WordPress block editor, you can also paste shortcodes into a Shortcode block.

Display a listing template with PHP

Instead of with a shortcode, listing templates can also be placed directly in your page template (or PHP include) with the facetwp_display() function:

echo facetwp_display('template','the_listing_name');

Where to place the shortcode?

If you place the template shortcode (or PHP code) and your facets on a single page or post, your facets will just work.

But if you place the shortcode on a WP archive page, things will not function correctly out of the box. See below for a few options to get this working.

If you are unsure what a WP archive is, or how to determine if you are using one, read this.

Using a template shortcode on a WP archive

Generally, on WP archive pages, it would be better to not use shortcode templates, but to use the WP archive template query itself. But if you don’t want or cannot change the way things are set up, and want to keep using a template shortcode on a WP archive page, you have to tell FacetWP explicitly which query (not) to use.

FacetWP has built-in query detection that determines which query on the page is the main query to use for filtering. On WP archive pages, FacetWP by default will always prioritize the archive query ahead of any other query on the page, including the query of the shortcode template you placed on that page. This is the reason why a template shortcode on a WP archive will lead to unexpected results: FacetWP is using another query than the one defined in the Listing Builder.

There are a few possible approaches to this situation:

Option 1: Ignore the archive query

The first option is to use the facetwp_is_main_query hook to force FacetWP to ignore the archive query, and use the Listing Builder’s shortcode query instead. Add the following code to your (child) theme’s functions.php to do this:

add_filter( 'facetwp_is_main_query', function( $is_main_query, $query ) {
    if ( $query->is_archive() && $query->is_main_query() ) {
        $is_main_query = false;
    }
    return $is_main_query;
}, 10, 2 );
Option 2: Pre-filter results based on the archive query

The second option, which only works on category, tag, taxonomy term, and search archive pages, is to use the shortcode template but let FacetWP pre-filter its query with the current category, tag, term, or search term(s) from the archive itself.

This can be done by adding the facetwp_template_use_archive hook to your (child) theme’s functions.php. See that hook’s page for detailed info about using this approach.

For example, on the category.php archive template, you could place a template shortcode that fetches posts. Without this hook, on the /category/events archive page, after using facets, the filtered results will be fetched from all posts, including posts that do not have the category ‘events’. With this hook in place, the results will only contain posts within the category ‘events’.

Similarly, consider a search results page based on the search.php template that contains a template shortcode that fetches products. Without this hook, on the search results page with the URL /?s=hoodies, selecting facet choices will generate results from all products, including those that do not contain the search term ‘hoodies’. With this hook in place, the results will be pre-filtered with that search term, so users can use the facets on the page to further ‘drill down’ into those results.

Option 3: Use the archive query itself

Last but not least, if you are deliberately placing facets on a WP archive page, you could consider not using a Listing Builder shortcode after all.

On a WP archive you can let FacetWP auto-detect and use the archive’s native query. Just place some facets on the page and they will work. And if you need to customize the native archive query, you can easily adapt it with WP’s pre_get_posts filter.

Multiple listing templates on the same page

Introduced in FacetWP v4.0 is the possibility to add more than one listing template to the same page.

Each listing template can have its own grid layout, styling, and query. But only one of them can be “dynamic” and will react to the facets on the page. The others have to be “static” listing templates and will not react to facets.

Display static listing templates

Static listing templates can be added with a shortcode similar to dynamic templates, but with an extra “static” attribute:

[facetwp template="the_listing_name" static]

Static listing templates can also be placed directly in your WordPress archive templates, custom templates or includes, with PHP:

echo facetwp_display('template','the_listing_name', [ 'static' => true ]);

How to duplicate a listing template

Duplicate, copy or clone a FacetWP listing template Since FacetWP v4.0 it is very easy to clone/duplicate a listing template, including all its settings:

Go to Settings > FacetWP and click the Listings tab. On the right side of the listing template that you want to clone, click the cog/gear icon and click “Duplicate”. This instantly creates a full copy of the listing template and its settings, with the label and facet name having the word “copy” added.

You can then change the new template’s label, name and settings and click “Save changes”.

Responsive support

By default, the Listing Builder automatically changes to 1 column when the browser width <= 480 pixels. You can add additional “breakpoints” via CSS. For example, this CSS switches the layout to 2-columns if the browser width <= 780 pixels:

@media (max-width: 780px) {
    body .facetwp-template .fwpl-layout, 
    body .facetwp-template-static .fwpl-layout {
        grid-template-columns: repeat(2, 1fr);
    }
}

Using shortcodes in HTML items

A handy feature worth mentioning is that you can add your own shortcodes to the “Content” field of an HTML item. Shortcodes will be automatically parsed when the listing template is displayed:

How to use shortcodes in an HTML item in the Listing Builder.
How to use shortcodes in an HTML item in the Listing Builder.

Hide empty items

The layout builder automatically adds the CSS class is-empty to any items that contain no value. You could use this (within your theme’s style.css) to hide these empty container elements with CSS, e.g.

.fwpl-item.is-empty {
    display: none;
}

In a post listing or grid, you’d usually want to link each post item (or elements of it, like the post title or a “read more” link) to the post’s permalink. In the Listing Builder, there are a few ways to do this:

The following types of builder items have their own “Link” setting which you can use to link the item to the post URL (or a custom URL):

If you are using a builder item without a “Link” setting, you can use any of the following solutions:

Add a post link to listing builder items with the Prefix and Suffix fields.
Add a post link to listing builder items with the Prefix and Suffix fields.

Most builder item types have a “Prefix” and “Suffix” settings. These two settings can be used together to wrap a link tag around the item, using the built-in {{ post:url }} dynamic tag.

Just add <a href="{{ post:url }}"> to the “Prefix” field, and </a> to the “Suffix field.

Another approach would be to use an HTML builder item. These can be very useful if you want to include the contents of one or more custom fields from Advanced Custom Fields or Pods in your item, or if your builder item is more complex. HTML items can be extra powerful if you combine them with built-in or custom dynamic tags.

An example: let’s say you want to create a link to the post URL, with the link text coming from an ACF custom field:

  1. First, click the “+” icon and create a builder item from your ACF field. Give the item a name with the “Unique name” field (or use the generated name), and enable the “Hide item” setting (to prevent it from being displayed itself):
Add a hidden builder item from an ACF field in the Listing Builder.
Add a hidden builder item from an ACF field in the Listing Builder.
  1. Then, add an HTML builder item, and use its “Content” field to create a link element. For the link URL in the href attribute, use the built-in {{ post:url }} dynamic tag. For the link text, use the “Unique name” of the builder item created in the first step. This name can be used as a custom dynamic tag by wrapping it with double curly brackets: {{ my-field-name }}:
<a href="{{ post:url }}">{{ my-field-name }}</a>

The settings then should look like this:

Add an HTML item in the Listing Builder with a built-in and a custom dynamic tag.
Add an HTML item in the Listing Builder with a built-in and a custom dynamic tag.

It’s also possible to use the facetwp_builder_item_value hook to add links:

For example, you could add a link to each builder item, like this:

<?php
add_filter( 'facetwp_builder_item_value', function( $value, $item ) {
  return '<a href="' . get_permalink() . '">' . $value . '</a>';
}, 10, 2 );

Obviously, don’t do this when one or more of your builder items already have a link (created with one of the methods above), or you’d get invalid, nested link tags.

The above example can be adapted to only add a link to one specific builder item, identified by its “Unique name”. Note that you can also use dynamic tags in the code. To illustrate this, in the example below we used a built-in dynamic tag for the post url, instead of get_permalink():

<?php
// replace 'my-item-name' with the name of your builder item
add_filter( 'facetwp_builder_item_value', function( $value, $item ) {
  if($item['settings']['name'] == 'my-item-name') {
    return '<a href="{{ post:url }}">'. $value. '</a>';
  }
  return $value;
}, 10, 2 );

Add a link to the whole post item block

Each post in a post listing normally consists of several builder items. For example, a post block could consist of a post title, a featured image, and an excerpt item.

With all of the above methods you can add links to individual builder items, like the post title. But what if you want to add a link to the whole post block instead of to the individual builder items it consists of?

There are two ways of accomplishing this:

Build the whole post block in one HTML builder item

One approach is to build the whole post block in one HTML builder item and use the {{ post:url }} built-in dynamic tag to add a link to the surrounding <a> tag.

Then use one or more other builder items for everything you want displayed within the link, by using their “Unique name” as custom dynamic tag. Make sure to set each of these builder items to be hidden, so they are not displayed in the listing.

The following example code for a HTML item generates the post title, the featured image and the excerpt inside the link. It assumes two hidden builder items: one for the featured image and one for the excerpt. The {{ post:title }} dynamic tag is already built-in.

<a href="{{ post:url }}">
    <span class="post-title">{{ post:title }}</span>
    <span class="featured-image">{{ my-featured-image-item }}</span>
    <span class="excerpt">{{ my-excerpt-item }}</span>
</a>

Using this code, the template settings would look like this:

Use only one HTML builder item for the whole post.
Use only one HTML builder item for the whole post.

Link the whole post block with Javascript

Another approach is to link only one of the builder items, for example the post title, using any of the above methods. Then, we can use a bit of JavaScript to find that link and use it in a click event attached to the whole post block (which has the fwpl-result class).

The following example shows how to do this. The code assumes that the post block has a builder item with the “Unique name” my-item-name, which contains a link:

<?php
// replace 'my-item-name' with the name of your linked builder item
add_action( 'wp_footer', function() {
?>
<script>
(function($) {
    $('.facetwp-template').on('click', '.fwpl-result', function() {
        window.location.href = $(this).find('.my-item-name a').attr('href');
    });
})(fUtil);
</script>
<?php
}, 100 );

Now each whole post block links to its post url. The only issue left is that when you hover over the post block, it does not look or behave like a link: the cursor does not change, and there is no feedback for the user that this is actually a link.

We can fix this with a few lines of CSS. The example code below applies a light gray background when hovering the post block, and sets the cursor to look like it is a real link:

/* Style the hover behavior of each post block */
.facetwp-template .fwpl-result:hover {
    background: #eee;
    cursor: pointer;
}

/* Optional: (re)style the hover of the only link */
.facetwp-template .fwpl-result:hover .my-item-name a {
    color: red;
    text-decoration: none;
}

Add a “No results found” text

Listing templates made with the Listing Builder currently do not output anything if there are no results.

Add the following code to your (child) theme’s functions.php to output a text, e.g. “No results found”.

The optional second part can be added to output facet value(s) to the text. This can be useful if you have limited facets, or for example only a Search facet. The output will then include the facet value(s): “No results found for [facet value(s)]”.

Other possible output can be HTML or PHP, for example a shortcode:

add_filter( 'facetwp_template_html', function(  $output, $class ) {
  if ( $class->query->found_posts < 1 ) {
    
    $output = 'No results found';

    // Optional: add below extra code if you want to output any facet value(s) from the filters,
    // resulting in the text: "No results found for [facet value(s)]"
    // Change "my_facet_name" to the name of your facet:

    if ( isset( FWP()->facet->facets['my_facet_name'] ) ) {
      $keywords = FWP()->facet->facets['my_facet_name']['selected_values'];
      $keywords = is_array( $keywords ) ? implode( ' ', $keywords ) : $keywords;
    }
    if ( !empty( $keywords ) ) {
      $output .= ' for ' . $keywords;
    }

    // OR: instead of the above, output HTML:
    $output = '<h2>Nothing found</h2>';

    // OR: instead of the above, output PHP, for example a shortcode:
    $output = do_shortcode('[my-shortcode]');
  }
  return $output;
}, 10, 2);

See also