FacetWP’s URL construction

When loading a listing template page for the first time, the page’s URL consists only of a base URL. For example, the URL of the Recipes demo page is:

https://facetwp.com/demo/recipes-demo/

When a user starts interacting with facets on that page, FacetWP adds variables to the page’s URL. Everything added after the base URL is called the “query string”.

Directly upon clicking a choice in a facet, a query variable representing that choice is added to the URL. Each additional facet interaction will update that facet’s query variable, or it will add a new one if it’s the first interaction with that facet. The whole set of query variables is preceded by a question mark.

For example, selecting “Cake” in the Recipes demo’s “Categories” facet adds the following to the base URL:

?_recipe_categories=cake

Even Pager facets and Sort facets append their current selection to the URL. For example, this is page 2 of a listing:

?_paged=2

And this is the URL after sorting by “Title a-z” with a Sort facet with the name “sortby”:

?_sortby=title_a_z

The only exception is the “load more” pager type of the Pager facet, which intentionally does not update its URL variables.

Multiple facets used

When selecting one or more choices for a second facet, query variables for that facet will be added to the URL too and separated from the previous ones with an ampersand: &.

For example, in the Recipes demo, after choosing “Cake” in the “Categories” facet, and “Almond” in the “Flavors” facet, the URL will look like this:

?_recipe_categories=cake&_flavors=almond

And the URL of page 2 of a listing that is sorted by title would look like this:

?_sortby=title&_paged=2

Multiple choices selected in a facet

If a facet has multiple choices selected, for example a Checkboxes facet, the choices will be separated in the URL with an encoded comma: %2C. For example, selecting two categories in the Recipes Demo’s “Flavors” facet will result in:

?_flavors=almond%2Clemon

Query variable construction

Each query variable – one for each facet – contains the following four items:

  • The prefix, which is a single underscore: _.
  • The facet’s “name”, e.g. flavors.
  • An = sign.
  • The facet’s choice(s) the user has made, e.g. almond%2Clemon, where multiple choices are separated by an encoded comma %2C.

FacetWP’s URL prefix

FacetWP’s query variables use an underscore _ prefix to prevent conflicts with WordPress and other plugins. WordPress has many reserved terms and reserved query variables, and unexpected things would happen if FacetWP would use them.

In the past FacetWP had a “URL prefix” setting, which let you choose between _ and fwp_. This setting was removed from the UI in version 3.5.3 for clean installs. If you have an older website with a FacetWP install that existed before this version (which was released on April 21, 2020), you will still see this setting.

Changing the URL prefix

Users sometimes ask how to change the _ prefix. In versions newer than v3.5.3, the _ prefix is fixed. It’s not possible anymore to change it in FacetWP’s settings.

If you absolutely need to change the prefix, for example back to fwp_ in a legacy site with a newer FacetWP install, the only way to change the setting is directly in the database.

First, make sure to back up your database. In the database, go to the wp_options table and find the facetwp_settings option. The value of that option contains a long JSON string. Towards the end of it, you’ll see the “prefix” setting, which you can change and then save.

Facet names in the URL

The first part of the query variable for each facet (after the _ prefix), is the facet’s “name” (yellow highlighted in this example):

?_recipe_categories=cake

In a facet’s settings, the “name” is what is shown in the gray input field:

A facet name field
A facet’s name field

The facet’s name is a technical name that is automatically generated from the facet’s “Label” field when it is first entered. But you can easily change the name, for example to shorten or simplify it when it is shown in the URL variable.

The auto-generated or customized name is sanitized on auto-generation or after changing and saving. Special characters are removed, and spaces and dashes are replaced by underscores.

Issues with certain facet names

One thing to keep in mind when choosing a name for your facets, is that WordPress has many reserved terms and reserved query variables. Giving your facet one of these reserved names will cause unexpected behavior.

For example, we have had users with a facet named “name”, which is a reserved term. Changing the name to “first_name” (and re-indexing afterward) fixed their issues. Similarly, don’t name your facet “length”; it will cause issues with certain JavaScript code.

Facet choices in the URL

As explained above, each choice a user selects in a facet adds that choice to the facet’s URL variable. For example, a facet with the name “flavors” with two selected choices will generate the following query string:

?_flavors=almond%2Clemon

A facet with two choicesIn this example, “almond” and “lemon” are the (technical) names of the two values selected. If the facet’s source is a taxonomy, this name is the term slug. If it is a custom field, it is the sanitized value entered in that field.

Sanitized and hashed facet choices

Sometimes you may encounter puzzling URL strings that look like this:

?_flavors=almond%2C12361053c4e8dd156950643ae742a789

A facet with a choice that will be hashedThis is caused by FacetWP’s internal function that automatically sanitizes facet choices that contain potentially URL-unsafe characters.

The above query string is the result of a facet named “flavors”, with two choices selected:

almond and lemon & lime

In this example, the ampersand & in the second choice is the cause of that choice being converted into a 128-bit (md5) hashed value, resulting in a string of alphanumerical characters in the URL (yellow highlighted). Additionally, because there are two choices in this example, the hashed choice is preceded by an encoded comma: the %2C part of the string.

Prevent hashing of facet choices

If you want to prevent hashing of your facet’s choices, be aware of the following rules for the source fields and terms of your facets’ choices:

  • Allowed are: alphabetical (a-z) and numerical (0-9) characters, underscores _, dashes - and dots ..
  • Any other type of character, including commas , and ampersands &, will trigger the facet choice to be hashed in the URL.
  • Spaces will be replaced by dashes -.
  • Multiple consecutive dashes will be replaced with a single dash.
  • The maximum number of characters is 50, above that the choice will be truncated.

If you can’t prevent users from breaking the above rules when entering values, or if manually fixing all source fields/terms is too much work (or impractical because of imported content), an alternative solution is to use the facetwp_index_row hook to replace the offending characters before the sanitizer function runs.

The following code replaces commas , and ampersands & with dashes -, when the choices are indexed. Make sure to re-index after adding this code to your (child) theme’s functions.php:

add_filter( 'facetwp_index_row', function( $params, $class ) {
  if ( 'your_facet_name' == $params['facet_name'] ) { // replace 'your_facet_name' with the name of your facet
    $value = $params['facet_value'];
    $value = str_replace( ',', '-', $value );
    $value = str_replace( '&', '-', $value );
    $params['facet_value'] = $value;
  }
  return $params;
}, 10, 2 );

The main reason for FacetWP’s query string is to enable the user to bookmark, forward or link to pages with specific facet combinations already pre-selected.

When a URL with a query string is opened for the first time, FacetWP uses its FWP.loadFromHash() function to load the post listing and facets directly from the URL’s query string, resulting in a page with all relevant facets reflecting the choices contained in their query variables.

We are occasionally asked if FacetWP supports “pretty” URLs, for example /make/audi/model/a4/ instead of ?_make=audi&_model=a4.

The answer is no, FacetWP only supports permalinks with URL parameters. “Pretty” permalinks quickly lose their appeal when multiple facet selections are made and/or multiple facets are combined. In which case there is no parent-child relationship between the subsequent sections of the URL and pretty permalinks would not make sense anymore.

Pretty permalinks would also be bad for SEO, since search engines would see many different URLs with the same content.

Preserve facet selections across pages

FacetWP automatically generates a query string when a user interact with the facets. The whole URL including the query string acts like a permalink: it allows users to bookmark, link to, or forward the page URL with the facets choices pre-selected.

However, when navigating to other pages on the site, the facet selections are lost. Fortunately, it’s possible to preserve the facets’ state across pages, by using cookies. See our tutorial to learn how to accomplish this.

In this context it is also good to know that it is possible to pre-select facet choices for specific URLs.

Disable FacetWP’s query string

Sometimes we get asked if it’s possible to turn off FacetWP’s URL variables.

One reason mentioned for this is that it would be better for SEO. Another reason is that turning them off restores the functionality of the browser’s “Back” button. With the query variables in place, clicking the “Back” button leads to previous filtering results. Without them, the previous page is loaded.

Although we recommend against it, it is possible to completely disable the query variables. FacetWP does not need them for facets to function properly.

However, be aware that when you opt to turn query variables off, users will lose the ability to use the URL (with pre-selected facet choices) for bookmarking, linking, and forwarding. Also using the browser’s “Back” button will not restore the state of the previous facet selections anymore.

To disable the query string, add the following code to your (child) theme’s functions.php:

add_action( 'wp_footer', function() {
?>
<script>
document.addEventListener('facetwp-refresh', function() {
    if (! FWP.loaded) {
        FWP.setHash = function() { /* empty */ }
    }
});
</script>
<?php
}, 100 );

FacetWP’s URL and SEO

A question that comes up occasionally is if FacetWP’s URL affects SEO in any way.

Disclaimer: we are not SEO experts, but there are a few logical answers:

Facets themselves are invisible to search engines. So search engine spiders will not “click” on facet choices and generate all kinds of URLs with query variables that get indexed. So there is no risk of creating “duplicate content” this way (which would be bad for SEO). The exception would be if you have links in your site to URLs with filtered facet combinations, for example in a menu or post content. Those links would get indexed and will show up in search engine results and Google Analytics.

Further, WordPress itself, and SEO plugins like Yoast SEO and RankMath, add “canonical link elements” to the page, which tell search engines that similar URLs are actually the same, preventing duplicate content issues. For pages with facets, the canonical URL is the same for every FacetWP URL, regardless of what facet choices are selected (including pagination and sorting). So there is no issue with duplicate content this way.

Set FacetWP URLs with query strings to “noindex” with Yoast SEO

If you want to make absolutely sure that URLs with FacetWP query variables are not indexed, and you are using the Yoast SEO plugin, you can use the wpseo_robots filter to change the robots meta tag from index to noindex for pages with facet choices in the URL.

Add the following code to your (child) theme’s functions.php:

add_filter( 'wpseo_robots', function( $robots ) {
  if ( function_exists( 'FWP' ) && ! FWP()->request->is_refresh && ! empty( FWP()->request->url_vars ) ) {
    return str_replace( 'index', 'noindex', $robots );
  }
  return $robots;
} );

The above code changes the robots meta tag only for directly loaded URLs with a FacetWP query string containing facet choices, including FacetWP pagination and sorting.

Worth mentioning is that Yoast SEO automatically removes the canonical link tag when the robots meta tag is set to “noindex”, because it is not necessary for that situation.

FacetWP pagination, SEO and accessibility

If you are using AJAX-based pagination on your page, like FacetWP’s Pager facet, be aware that the individual posts on the subsequent paged pages, and the paged pages themselves, will not be seen and indexed by search engines. The Pager facet’s <a> tags do not have href attributes with links that the search engine spider can follow. Also, users without JavaScript enabled will not be able to see and use any pagination on paged archives that you are using FacetWP on.

For SEO, this is not necessarily problematic, as long as all individual posts are reachable for the search engine spider through other archive pages on the site, like post-type archives and term archives. Another way to make sure all pages are found is by implementing a sitemap and pointing to it in your robots.txt file.

If you don’t want to rely on other archive pages or your robots.txt file for SEO of your paged pages, a recommended approach is to give search engines “non-JavaScript/AJAX” pagination links that they can follow: anchor links with href attributes. And then hide those pagination links from users, who will get served the AJAX-based pagination. There are a few ways of doing that.

Use WooCommerce pagination

If you are using WooCommerce, on shop/product archive pages you can use WooCommerce’s built-in pagination instead of a Pager facet. FacetWP has built-in support for WooCommmerce pagination, meaning that FacetWP listens for the click events on the WooCommerce pagination’s href anchor links and uses them for its AJAX pagination. This is the best of both worlds: users get AJAX pagination, and search engine spiders (and users without JavaScript enabled) can follow the href links.

Use WP-PageNavi pagination

If you don’t use WooCommerce, or for non-product archive templates, you could add pagination with the WP-PageNavi plugin.

It is possible to make FacetWP work directly with WP-PageNavi’s pagination. With the code below in place, FacetWP will listen for the click events on WP-PageNavi’s href anchor links and use them for its AJAX pagination. Similar to the way WooCommerce pagination works with FacetWP, this is the best of both worlds: users get AJAX pagination, and search engine spiders (and users without JavaScript enabled) can follow the href links.

Add the following code to your (child) theme’s functions.php to make this work:

// Note: if your website is not in English, change "page" in the match() function to whatever it is in your language.

function fwp_pagenavi_support() {
  ?>
  <script>
    (function($) {
      $(document).on('facetwp-refresh', function() {
        if (! FWP.loaded) {
          $(document).on('click', '.wp-pagenavi a.page', function(e) {
            e.preventDefault();
            var matches = $(this).attr('href').match(/\/page\/(\d+)/);
            if (null != matches) {
              FWP.paged = parseInt(matches[1]);
            }
            FWP.soft_refresh = true;
            FWP.refresh();
          });
        }
      });
    })(jQuery);
  </script>
  <?php
}
add_action( 'wp_head', 'fwp_pagenavi_support', 50 );

The WP-PageNavi plugin has the option to customize the pagination links so that all /paged/.. pages are individually linked (not only with “prev”, “next” or “last” links). This helps to give the search engine spider direct “one-click” access to all paged pages, even if there are many.

If you don’t want to expose the user to a pagination that is set up this way, you could consider using it only as a hidden, secondary navigation meant for search engines, while giving the user AJAX-based pagination with a Pager facet:

Use secondary non-JavaScript pagination and hide it

Another approach is to use a Pager facet to create the AJAX pagination for the user, but to add a secondary set of non-JavaScript/AJAX pagination just for search engines (and users without JavaScript enabled). Then you can hide this secondary pagination from the user with CSS, but only when they have JavaScript enabled, so it is still accessible to users without JavaScript enabled.

This secondary navigation can be added with any plugin, like the above-mentioned WP-PageNavi plugin, with WordPress’ built-in pagination functions, or with pagination functionality that comes with your theme.

You could hide the secondary navigation simply by targeting it with a display: none; CSS rule. But then users without JavaScript enabled would not be able to see and navigate paged pages. So a better solution is to make that CSS rule dependent on whether the user has JavaScript enabled.

Detecting JavaScript can be done in many ways. The following is a very clean and simple solution (by Paul Irish), based on Modernizr :

First, give the <html> tag in your header.php file the attribute class="no-js".

Then place the following script as high as possible within the <head> tag in your header.php file. It needs to run as soon as possible, before the browser can render anything else:

<script>(function(H){H.className=H.className.replace(/\bno-js\b/,'js')})(document.documentElement)</script>

This code will replace the no-js class of the <html> tag with js if JavaScript is enabled.

These classes can now be used in conditional CSS rules. The following CSS code will hide the secondary pagination, but only when JavaScript is enabled (this is for WP-PageNavi, adapt the class as needed):

.js .wp-pagenavi { display: none; }

Add a “noindex” robots meta tag on paged pages or not?

In the past, it used to be good practice to add a noindex robots meta tag to all paged pages, to prevent them from showing up in search engine results. This is no longer the case, as explained in this article of Yoast SEO about pagination best practices.

Functions and objects to get and manipulate the URL

When you are customizing FacetWP, be aware of the following built-in JavaScript objects and functions to get and manipulate the URL.

Say you have this page URL:

https://facetwp.com/demo/recipes-demo/­?_recipe_categories=cake&_flavors=almond

You can use the following methods to get and manipulate the different parts this URL consists of:

Get the URI

To get the URI (Uniform Resource Identifier), which is the part of the URL without the domain name and the query variables, you can use the FWP_HTTP.uri variable:

add_action( 'wp_footer', function() {
?>
<script>
document.addEventListener('facetwp-loaded', function() {
    console.log(FWP_HTTP.uri);
});
</script>
<?php
}, 100 );

The above code will output the following string to the Console, without the beginning and trailing slashes:

demo/recipes-demo

Get the URI with PHP

If you need to get the URIs with PHP, you can use the following:

FWP()->helper->get_uri();

A use example can be found in our tutorial on How to Pre-Select Facet Choices.

Get the query variables

To get the query variables – which are generated after interaction with facetsas an object, you can use the FWP_HTTP.get variable:

add_action( 'wp_footer', function() {
?>
<script>
document.addEventListener('facetwp-loaded', function() {
    console.log(FWP_HTTP.get);
});
</script>
<?php
}, 100 );

The above code will output the following object to the Console, containing the page’s $_GET variables:

{_recipe_categories: "cake", _flavors: "almond"}

Get the query variables with PHP

If you need to get the query variables with PHP, the following variable contains them as an array, after directly loading a page URL with facet selections in its query string:

FWP()->request->url_vars

A usage example can be found in the above robots meta tag code example.

Get the query string

To get the query variables – which are generated after interaction with facetsas a string, you can use the FWP.buildQueryString() function, which builds and returns the URL query string:

add_action( 'wp_footer', function() {
?>
<script>
document.addEventListener('facetwp-loaded', function() {
    console.log(FWP.buildQueryString());
});
</script>
<?php
}, 100 );

The above code will output the following string to the Console, containing the page’s whole query string (without the preceding question mark):

_recipe_categories=cake&_flavors=almond

This function can be useful to determine if any facets are currently in use. An example would be to show/hide the post listing based on whether there are facet query variables in the URL.

See our code snippets for more examples.

Two other URL-related functions are FWP.setHash() and FWP.loadFromHash().

The first one can be used to update the query string, but also to disable it. The second one dynamically re-creates all facet selections when a URL with a query string populated with pre-selected facet choices is entered by a user:

FWP.setHash(); // Updates the permalink URL
FWP.loadFromHash(); // Populates facet data from URL data (happens on pageload)

See also