JPKCom Post Filter — Guide & Tips
Faceted taxonomy filtering for posts, pages and custom post types with JPKCom Post Filter — SEO-friendly URLs, AJAX, shortcodes, blocks and page builder support.
JPKCom Post Filter adds faceted taxonomy filtering to any WordPress archive page — for posts, pages and any custom post type. Every filter state is encoded in an SEO-friendly URL like /blog/filter/web-design+marketing/wordpress/, so it is shareable and bookmarkable; filtering runs via AJAX with a full no-JavaScript fallback.
Guide
Requirements
- WordPress 7.0 or newer (tested up to WordPress 7.1)
- PHP 8.3 or newer
- No dependencies: no ACF, no Bootstrap, no jQuery required
Page builder support is optional — the Gutenberg blocks, Elementor widgets and Oxygen elements load only when the respective builder is active.
Installation
- Upload the
jpkcom-post-filterdirectory to/wp-content/plugins/(or install the ZIP via Plugins → Add New → Upload Plugin). - Activate the plugin under Plugins → Installed Plugins.
- Under Post Filter → General, select the post types to filter.
- Under Post Filter → Filter Groups, add the taxonomies you want as filter dimensions.
- Go to Settings → Permalinks and click Save Changes to flush the rewrite rules.
Configuring filter groups
Each filter group maps a taxonomy to a URL position; the group order determines the order of the URL segments. Per group you set the taxonomy, label, target post types, a numeric sort position (Order) and an on/off toggle (Enabled).
You can register new taxonomies right from the Filter Groups page (the "Register as new WordPress taxonomy" option): slug, rewrite slug, hierarchical/flat, public, admin column and REST API visibility (required for Gutenberg) are all configurable.
Warning: Deleting a filter group that registered a custom taxonomy unregisters that taxonomy — all term assignments on posts are permanently lost.
Output: auto-inject, shortcodes, blocks
There are three ways to render the filter and list:
- Auto-inject (General → Auto-Inject Filter): the plugin automatically wraps the theme's archive loop with the filter UI — no shortcode or template edit needed. It hooks into
loop_start/loop_endon archive/blog pages (not on singular posts, search or pages). - Shortcodes:
[jpkcom_postfilter_filter],[jpkcom_postfilter_list]and[jpkcom_postfilter_pagination]. The interactive shortcode builder under Post Filter → Shortcodes generates ready-made snippets. - Builders: three Gutenberg blocks, three Elementor widgets and three Oxygen elements each (Post Filter, Post List, Post Pagination).
[jpkcom_postfilter_filter post_type="portfolio" layout="dropdown"]
[jpkcom_postfilter_list post_type="portfolio" layout="cards" limit="12"]
[jpkcom_postfilter_pagination post_type="portfolio"]Important: Shortcodes (and blocks/widgets/elements) always attach to the archive of the configured post type. They only work on an archive template page or on the page set as the "Posts Page" under Settings → Reading. On an arbitrary page (e.g.
/test/) filter clicks and AJAX jump back to the archive URL — use auto-inject there instead.
Layout & design
Post Filter → Layout & Design offers six tabs (Global, Filter, Posts, Pagination, Color Schemes, Advanced). You choose from four filter layouts (bar, columns, sidebar, dropdown), three list layouts (cards, rows, minimal) and four color schemes (Default, Dark, Contrast, Monochrome). The Advanced tab controls the stylesheet mode (Full / Variables only / Disabled), the Plus/Minus mode, reset button visibility and your own custom CSS, among others.
Posts as data: the Abilities API
Since version 1.3.0 the plugin registers two read-only abilities. The Abilities API is a WordPress core registry of machine-readable capabilities — AI assistants, MCP clients and REST automation can answer questions about your posts through it without scraping the front end:
| Ability | What it answers |
|---|---|
jpkcom-post-filter/list-filters | which taxonomies and terms a post type can be filtered by, with a post count per term |
jpkcom-post-filter/query-posts | a filtered, paginated query returning the matching posts plus a shareable filter URL |
The point of the pair is that the first removes the guesswork from the second: an assistant looks up the real taxonomy keys and term slugs, then filters with values that exist. A filter naming a taxonomy your site does not have is rejected with an error listing the valid ones — it never silently returns your whole archive instead.
Both are reachable at /wp-json/wp-abilities/v1/ and answer on GET:
GET /wp-json/wp-abilities/v1/abilities/jpkcom-post-filter/query-posts/run
?input[post_type]=post&input[filters][category][]=allgemein&input[per_page]=3query-posts accepts post_type, filters, page, per_page (1–50) and search. Terms within one taxonomy are combined with OR, different taxonomies with AND — the same logic as the filter bar on your site. A term slug that matches nothing is not an error: the query runs and the slug is reported under unknown_terms, so the caller can tell a typo apart from an empty result.
Worth knowing when reading a response: filter_url comes back empty whenever no front-end address would show exactly the posts listed beside it. There are five such cases: a post type without an archive page; a filter combination beyond your configured limits; a page after the first requested with a page size other than your site's own "posts per page"; a page past the last one; and — since 1.3.1 — an applied search term, because a filter URL cannot carry a search term and would show a larger, different list. The results are complete in every one of those cases; only the link is missing.
What is exposed, and to whom:
- As a base rule, only published posts: the post status is fixed in the code and a caller cannot set it as a parameter. That is not a guarantee, though, and the plugin's developer documentation has said so explicitly since 1.3.1:
post_statusis an ordinary query var, and anything that touches the query first — another plugin hookingpre_get_postswithout anis_main_query()guard, or the documentedjpkcom_postfilter_query_argsfilter — changes what comes back. There is no second read gate behind the query here (the sibling pluginjpkcom-acf-jobshas one). On a site with such interventions, do not rely on drafts being unreachable. - Only post types you have enabled for filtering, and only taxonomies you have configured as filter groups.
- Running an ability requires a logged-in user with the
readcapability. The mere list of abilities, with their descriptions and parameter schemas, is visible to any logged-in user including a subscriber — that is how WordPress core gates every ability, not something specific to this plugin.
To switch it off, in wp-config.php:
define( 'JPKCOM_POSTFILTER_ABILITIES', false );To keep the abilities but raise the bar for running them:
add_filter( 'jpkcom_postfilter_ability_capability', static fn(): string => 'edit_posts' );To keep them out of MCP clients while leaving the REST route intact, or the other way round, jpkcom_postfilter_ability_meta is the lever.
Tips & Tricks
- Clearing the cache was incomplete — fixed in 1.3.1: Clearing the cache never cleared the fastest layer of it. Saving a post, editing a term, saving the settings, and both "Clear cache" buttons in the admin all reported success while leaving stale query results in place until they expired on their own — up to an hour by default. This affects every site running APCu, whether or not you use the abilities — the dead deletion loop sat in exactly that branch.
- Change the URL endpoint: By default the filter sits under
/filter/. Change the path segment under General → URL Endpoint and then click Flush Rewrite Rules (or re-save your permalinks). - Behaviour without filter terms: Use Bare Endpoint Behaviour to set what happens when
/filter/is accessed without terms — 404, redirect to the blog homepage, or a custom URL. - Multiple filter instances per page: Each filter/list/pagination set is paired via the
post_typeattribute (data-jpkpf-post-type), so different post types can be filtered independently on one page. - Fragment responses instead of whole pages: Since version 1.2.0 a filter click is answered with just the swappable zones — the theme header, nav menus, sidebar widgets and the entire asset pipeline are skipped (measured on a test install: 60–72 % less transferred, 5–19 % less server time). The request goes through a dedicated
/jpkpf-fragment/URL segment rather than a query parameter, so a page cache that strips unknown parameters cannot serve a bare fragment to an ordinary visitor. The rewrite rules this needs are flushed once automatically after a version change — no manual permalink save required after the update. - HTML minifiers and fragments — fixed in 1.4.4: With an HTML-minifying optimisation plugin active, every filter click returned an empty response and the list reported "No posts found." — on a site whose posts all still matched. The fragment was cut out of the rendered page along HTML comment markers, and comments are exactly what a minifier removes; Autoptimize does so with its stock settings, and its output buffer runs before the plugin's. Measured on the verification install: 18,300 bytes with Autoptimize inactive, 0 bytes with it active, 18,300 again with its HTML optimisation switched off. Since 1.4.4 the markers are empty
<template>elements — no minifier removes elements, and the markers never reach the browser either way. On top of that, fragment requests ask the known optimisers to leave them alone, through Autoptimize's own filter and the cross-vendorDONOTMINIFYconvention. Add other vendors from an mu-plugin throughjpkcom_postfilter_fragment_noptimize_filters— the registration happens while the plugin is loading, so a regular plugin would be too late. And if a fragment response does arrive unusable, the script now falls back to a full page load instead of wrongly reporting "no posts". - Four cache layers: Object cache, transients, APCu and a PHP settings file cache keep output fast. Caches are invalidated automatically on
save_post/deleted_postor term changes; clear them manually under Post Filter → Cache. - Override CSS cleanly: All styles use CSS custom properties prefixed
--jpkpf-. Set them in your theme stylesheet, in the variable fields under Layout & Design, or via the custom CSS field. For full control, set the stylesheet mode to "Disabled". - Override templates per theme: The loader checks child theme → parent theme →
mu-plugins/jpkcom-post-filter-overrides/templates/→ plugintemplates/. For example, copytemplates/partials/list/list-cards.phptothemes/your-theme/jpkcom-post-filter/partials/list/list-cards.php. - Migrate settings: Under Post Filter → Import / Export you can export all settings (general, layout, cache, filter_groups) as JSON and re-import them on another environment.
- Constants in
wp-config.php: among others,JPKCOM_POSTFILTER_URL_ENDPOINT,JPKCOM_POSTFILTER_CACHE_ENABLED,JPKCOM_POSTFILTER_DEBUGandJPKCOM_POSTFILTER_MAX_FILTER_COMBOSare overridable. - Build Gutenberg blocks from source: The block editor scripts need a build step (
npm install && npm run build, output toblocks/build/). If the build directory is missing, the plugin skips block registration. - German translation: The translation catalogue was last generated at 1.1.2 and then trailed the code for twelve releases, so everything added since appeared in English on a German site. Since 1.4.1 it covers the whole plugin again, and 1.4.2 added 46 missing German translations — block titles with their descriptions and search keywords, the Elementor and Oxygen widget labels, several settings hints and the updater's security messages. The Abilities API texts stay English on purpose: they are read by AI clients and automation, not in the admin area, and their exact wording is what lets a caller correct a mistaken request in one attempt.
- Accessible & SEO-ready: Filter terms are real
<a>links (page reload without JS), the results zone usesaria-live, toggle buttons usearia-pressed. Each filter combination has a crawlable URL with a canonical set against duplicate content.
Further reading
- Source code on GitHub: https://github.com/JPKCom/jpkcom-post-filter (opens in a new tab)
- This project's changelog