Changelog — JPKCom Post Filter
Versionsverlauf des WordPress-Plugins JPKCom Post Filter.
Versionsverlauf von jpkcom-post-filter (öffnet in neuem Tab). Die Einträge stammen unverändert aus der README des Projekts. Eine ausführliche Anleitung mit Tipps findest du im Projekt-Guide.
1.7.1
What changes on a running site after the update - read this first:
- Changed (behaviour, all sites): page 2 and later of a filter page now have a canonical to themselves (
…/filter/seo/page/2/), as Google recommends for paginated pages; page 1 still points to the term archive or the blog page. As a consequence, page 2 and later of a combination arenoindex, nofollowwithout a canonical again, as in 1.6.1. - Changed (behaviour, all sites): a filter page past its last page (
/blog/filter/seo/page/99/) now answers 404, like/blog/page/99/and/category/seo/page/99/. WordPress's canonical redirect sent it with a 301 to the unfiltered blog page. Filter pages that exist keep WordPress's redirects (a missing trailing slash and the like). - Changed (behaviour, sites with Rank Math or Yoast): filter pages no longer inherit a noindex from the blog page. A single-term filter page stays
indexwith its canonical to the term archive even if the blog page itself is set to noindex. Two settings still win: Discourage search engines in WordPress, and Rank Math's noindex paginated pages / noindex archive subpages for page 2 and later (applied exactly where Rank Math applies them). A search on a filter page (?s=) staysnoindex, and a comment reply link (?replytocom=)noindex, nofollow, as WordPress and both SEO plugins send them. - Changed (Yoast): Yoast's
rel="prev"/rel="next"links are no longer printed on filter pages - they pointed into the unfiltered blog (Rank Math's were already off). - Changed (Server Rules, only settings the page warns about): where the Server Rules page says a reject rule is left out because a server could not load it (very many filter groups or terms per group, see 1.6.1), the rule hash now changes with the update. Check now then reports rules you pasted by hand as stale: paste them again. A managed
.htaccessblock rewrites itself - on the next wp-admin page load, and also under WP-CLI, see below. For every other setting the rule hash and the reject rules are byte-for-byte the same as in 1.7.0, so deployed rules stay current. - Changed (managed
.htaccessblock): only one settings save at a time can update the block. A second save of General or Filter Groups (or an import) while another one is still between loosening and tightening the block is refused with a message to wait a few seconds and save again - two saves at once could leave the block stricter than the plugin. Nothing is stored in that case, and the refused save no longer writes its own, possibly outdated copy of the settings into the settings cache file (it could undo the other save's change there). An import holds the lock from its first settings group to its last, so it is never applied half; while another save runs it is refused as a whole. Set up waits the same way ("try again"), and so do the automatic block updates after a version change or a change of the posts page or permalinks. Sites without a managed block are not affected. - Changed (managed
.htaccessblock): an.htaccessthat exists but cannot be read keeps the block guarded. A save that loosens the filter URL settings is refused ("the file is not readable"), and deactivation and Remove no longer forget the block - before, the plugin treated the unreadable file as one without a block. The Server Rules page shows the file as "not readable". - Fixed (managed
.htaccessblock, WP-CLI): on a version change the plugin now also rewrites the block under WP-CLI (or on a server it cannot identify) - but only when the new block provably leaves out whole rules and changes nothing else, never when it would be stricter. That repairs a 1.6.0 block whose rule made Apache answer 500 for the whole site: afterwp plugin update jpkcom-post-filter, the nextwpcommand runs the new version and rewrites the block; removing the lines by hand (1.6.1 notes) is no longer needed. The text of the error page, translated into the language of whoever wrote the block, does not stand in the way. - Fixed (block themes): with auto-inject in a block theme such as Twenty Twenty-Five the filter bar was printed before
<!DOCTYPE html>. It now sits around the query block's post list, the theme's own query pagination gives way to the plugin's, and zero results show the bar and the message where the list would be. Classic themes print exactly what 1.7.0 printed. - Fixed: the screen reader announcement after a filter click counts the posts of a block theme's post template too (it announced no number there), and only the list's own posts - not those of a query block inside a post.
- Fixed: on a permalink structure without a trailing slash the canonical of a combination pointed to
/blog/, which redirects to/blog; it is now the URL the blog page itself declares. - Fixed: the Rank Math check behind the canonical targets now follows Rank Math exactly in two more cases: an empty category with sub-categories is not noindex, and "custom robots" switched on without a selection means index for taxonomies and pages.
- Fixed (developers): a
jpkcom_postfilter_canonicalcallback that returns an empty string now means "no canonical" and keeps the page's noindex, instead of leaving an indexable page without a canonical. Since 1.7.0 this filter also receives term archive and blog URLs, not only the filter page's own URL. - Fixed (managed
.htaccessblock): if writing the file failed half way and its previous content could not be put back, the message now says the.htaccessmay be incomplete and has to be checked or restored now - Apache answers every request with an error while it cannot parse the file. Before, it only advised checking permissions and disk space. On a full disk withdisplay_errorson, Set up and Remove no longer end in "headers already sent". - Fixed (Server Rules page): when the terms per group rule is left out only because the segment count rule is, the warning now says so instead of repeating the other rule's reason, and it only names the settings that actually bring the dropped rules back, per reason (fewer terms per group, for example, is no longer suggested where it cannot help). Measured: no rule the page generates reaches the compiled size limit of Ferron 3's regular expressions.
- Fixed (Server Rules, Check now): a redirect on the "stricter than the plugin" probe that does not come from the plugin itself - to https, to another host name, from a proxy - is now reported as "not checked" instead of "not stricter". Such a redirect happens before the server rules and proves nothing about them.
- Fixed (Server Rules check commands): the printed loops run in a subshell and no longer leave variables (
first,h,c,i,u) behind in your shell. The Ferron 2.8 tab no longer prints the header explanation and the "never stricter" loop: there is no reject rule to check there. - Changed (Server Rules, Apache with ModSecurity): a request the ModSecurity rate limit answers with 429 no longer writes an audit-log entry (
ctl:auditEngine=Offon the three limit rules); its one line in the error log stays. Neither does a filter URL the server rejects: a new rule in the ModSecurity snippet (4710014,pass,nolog) switches the audit log off for exactly the requests the reject rules answered - it never blocks and never changes a status. Measured with Debian's recommendedmodsecurity.conf, with and without OWASP CRS: 1.7 MB of audit log per 1000 answered 429s and 1 MB per 1000 server rejects before, none after; every other request keeps its audit entry. Paste the ModSecurity snippet again to get this; the reject snippet and the limits are unchanged, and so is the rule hash (apart from the settings above), so Check now keeps reporting deployed rules as current. - Fixed (developers): deleting a settings option (
wp option delete jpkcom_postfilter_general, say) now also deletes its settings cache file. The plugin reads that file before the database, so until the next save it kept running with the deleted values. With a managed.htaccessblock the block is first loosened for the defaults the plugin then falls back to, and brought to them afterwards. - Changed (developers, managed
.htaccessblock): deleting the General or Filter Groups option is refused when the block cannot be loosened first (read-only file, another save updating the block at that moment, a database error):wp option deleteends with an error naming the reason and exit code 1, anywhere else the request ends with HTTP 409. Option, settings cache file and.htaccessstay as they were - otherwise the server would reject filter pages the plugin serves with its defaults. - Fixed: reading the settings no longer writes the settings cache file; only saving does. A request that loaded the settings just before an admin saved or deleted them could write its outdated copy into a missing cache file (after Clear Settings File Cache, say), and every later request ran with the old values until the next save.
- Fixed (Cache page): the checkboxes Object Cache, Transient Cache and Settings File Cache can be switched off. An unchecked box sent nothing and the plugin read that as the default - on - so the three caches could never be disabled from the form (
wp-config.phpconstants still work as before). - Changed (General): Debug mode is now shown read-only. The box never did anything: debug logging and the debug templates follow the constant
JPKCOM_POSTFILTER_DEBUGinwp-config.phpalone (default:WP_DEBUG), and that is what the box now shows. - Faster wp-admin: on nginx, Caddy, Ferron, LiteSpeed, multisite and
DISALLOW_FILE_MODSsites the plugin no longer queries the database for the managed.htaccessflag on every admin page, and with a managed block that is current it no longer builds the rules there. - Purge the page cache after updating - cached filter pages keep the old canonical and robots until they are rendered again.
- Translations: German (de_DE and de_DE_formal) for every new string.
1.7.0
What changes on a running site after the update - read this first:
- Changed (behaviour, all sites): the canonical of a filter page with one term (
/blog/filter/seo/) now points to that term's archive (/category/seo/,/tag/…/or the archive of a custom taxonomy), which shows the same posts. Only when that archive is public, lists the filtered post type, is not set to noindex in Rank Math or Yoast and has no manual canonical of its own pointing elsewhere - otherwise the canonical stays on the filter page as before. Rank Math sets tag archives to noindex out of the box, so tag filter pages there keep their own canonical. - Changed (behaviour, all sites): a filter page with two or more terms now has a canonical to the unfiltered blog page (or post type archive) and is
index, nofollowinstead ofnoindex, nofollow: a noindex next to a canonical pointing elsewhere contradicts itself, and search engines may carry the noindex over to the target. If the blog page itself is set to noindex or has a manual canonical elsewhere in Rank Math or Yoast, or the site has a static front page and no posts page, nothing changes for these pages. Unknown terms and empty results staynoindex, nofollowwithout a canonical. - Changed: the canonical of a paginated filter page points to page 1 of its target. This also applies to the filter page's own canonical (
…/filter/seo/page/2/→…/filter/seo/). - The way back: set Canonical: Single Term and Canonical: Multiple Terms to Filter URL for the canonicals of 1.6.1 (apart from page 1 above), or to Off for no canonical at all.
- Purge the page cache after updating - cached filter pages keep the old canonical and robots until they are rendered again.
- Theme overrides of
partials/pagination/pagination.php: the plugin's copy now asksjpkcom_postfilter_should_nofollow()instead ofjpkcom_postfilter_should_noindex(). An override keeps working, but its page links on combinations are no longernofollowuntil it takes over that line. - Added: the settings Canonical: Single Term (
archive/filter/off) and Canonical: Multiple Terms (home/filter/off) under Crawlers & SEO, the constantsJPKCOM_POSTFILTER_CANONICAL_SINGLE/JPKCOM_POSTFILTER_CANONICAL_MULTI, the filtersjpkcom_postfilter_canonical_archive_indexableandjpkcom_postfilter_canonical_base_indexable, and two more arguments forjpkcom_postfilter_canonical(the kind of target and the filters). The server rules and the managed.htaccessblock are not affected. - Translations: German (de_DE and de_DE_formal) for every new string.
1.6.1
What changes on a running site after the update - read this first:
- Nothing changes for usual settings. With up to 8 filter groups and up to 5 terms per group every generated server rule, the managed
.htaccessblock and the rule hash are byte-for-byte the same as in 1.6.0 (checked for 680 layouts). Rules you deployed by hand need no update. - Fixed (Server Rules, very many filter groups or a high Max. Filters per Group): the generated reject pattern grows with the number of filter groups and terms per group, and beyond a certain size a server cannot load it. With 71 or more filter groups at 20 terms per group (37 or more if the last group has no taxonomy) PCRE cannot compile it - nginx then refuses the configuration, and a managed
.htaccessblock makes the whole site answer 500. With filter groups without taxonomy in between it grew past 8 000 characters - nginx reads at most 4 094 in one parameter, Apache at most 8 191 bytes in one.htaccessline. Caddy refuses more than 1 000 repetitions, and Ferron 3 silently never applies a pattern nested too deeply (from about 160 filter groups with every second one without taxonomy). Such a rule is now left out of every snippet, the curl commands, Check now and the managed.htaccessblock, for every server alike, and the Server Rules page says which rule and why. That is looser, never stricter: the plugin still rejects those URLs itself, only without the server's help. The limit for the pattern length is 4 000 characters - stricter than Apache alone would need, so an Apache block that still loaded on 1.6.0 with a pattern between 4 000 and 8 191 characters loses its server-side per-group rule when it is rewritten. - If you set up the managed
.htaccessblock with such settings on 1.6.0 and the site has answered 500 since, the order matters: update first without opening wp-admin (wp plugin update jpkcom-post-filter, or replace the plugin folder over SFTP), then remove the lines between# BEGIN JPKCom Post Filterand# END JPKCom Post Filterfrom the.htaccessby hand. 1.6.1 then writes a loadable block by itself on the next request. If you remove the lines while 1.6.0 is still installed, the next wp-admin page load writes the broken block back. Alternative:wp plugin deactivate jpkcom-post-filterremoves the block; then update, activate and click Set up again. - Fixed (Server Rules check commands): the rate-limit test printed a single request after the 25 sequential ones and expected it to show 429 and
Retry-After. Between the loop and that request the site-wide limit refills, so it often answered 200 and the limit looked broken. The loop itself now prints the status codes and, at the end, theRetry-Afterheader of the first 429 it received (nginx, Apache with ModSecurity, Caddy, Ferron 3, Ferron 2). Still plaincurlandsh. - Translations: German (de_DE and de_DE_formal) for the new warning.
1.6.0
What changes on a running site after the update - read this first:
- Changed (behaviour, all sites): the limit Max. Filters per Group now counts the terms of a group as they appear in the URL, before duplicates are removed or spellings cleaned up. A hand-built URL with more terms in a group than the limit - for example
a+a+bat a limit of 2, or extra upper-case or empty variants - is now answered with 410 (or 404) instead of a 301 to the cleaned-up URL. URLs the plugin builds itself (links, the filter script, the Abilities API) never look like that. Within the limit, other spellings are still redirected as before. - Changed (new installations only): the default of Max. Filters per Group is 2 instead of 3. Existing installations keep 3 on the update - the plugin stores 3 for them if the setting was never saved. A value you saved yourself is never changed.
- Added: Post Filter → Server Rules, a new page that generates web server rules from the current settings for nginx (incl. ISPConfig), Apache, Apache
.htaccess, Caddy/FrankenPHP, Ferron 3, Ferron 2.8 and OpenLiteSpeed: reject over-limit filter URLs before PHP runs, let/robots.txtreach WordPress, and rate-limit filter requests from visitors without cookies. Nothing changes on your server until you deploy the rules - except on Apache if you choose to let the plugin manage its.htaccessblock. - Added: managed
.htaccessblock on Apache (single site, writable file): set up and removed by button, placed directly before WordPress's block, removed on deactivation. Kept in step with every settings save - in wp-admin, through the import and through WP-CLI (wp option update): a save that loosens the filter URL settings loosens the block first, and while the block cannot be updated such a save is refused with an error, so the server never rejects a URL the plugin serves. Tightening through WP-CLI,wp-config.phpconstants or code filters follows on the next admin page load (until then the block is only looser than the plugin). The path stored at Set up is only trusted inside the site's own directories. - Added: Check now on the Server Rules page: shows whether robots.txt reaches WordPress, whether the server rule is deployed and current, and raises an alarm if it is stricter than the plugin.
- Added: a notice after every save (or import) that loosens the filter URL settings, reminding you to update server rules you deployed by hand - red when the last check found them active. Not shown while the plugin manages the
.htaccessblock itself. - Translations: German (de_DE and de_DE_formal) for every new string.
- Background: after 1.5.0 the rejected filter URLs on a production site still cost a full WordPress start each - about 70 % of the remaining PHP load. A web server rule can answer them for almost nothing; counting terms as they appear in the URL is what lets one simple, tested rule match exactly what the plugin rejects, on every server.
1.5.0
What changes on a running site after the update - read this first:
- Changed (behaviour, all sites): filter URLs have exactly one spelling. The terms of a group are sorted and de-duplicated; any other spelling (
b+a,a+a, upper case, a trailing_,…/page/1/) is answered with a 301 to the canonical URL. Existing links and indexed URLs keep working through that redirect. - Note (WPML): whether a term exists is now checked against all languages, so a term of a second language is never redirected away. The robots.txt rules are written for every language prefix (
/filter/*+,/en/filter/*+). - Fixed (multisite): all sites of a network shared one settings cache directory, so the site that saved last decided the URL endpoint, filter groups and limits of every other site. Each site now has its own subdirectory (
site-{id}); the old shared files are removed on the update. Single sites are unaffected. - Fixed (WPML/Polylang): the filter bar's term list was cached once per taxonomy, so the language that loaded it first decided which terms the bar showed in every language for up to an hour. It is now cached per language.
- Fixed (APCu): the query cache in APCu used the same keys on every site of a network and every WordPress installation served by the same PHP-FPM pool, so one site could be shown another site's cached posts. Keys are now scoped to the site.
- Fixed: going back or forward between
#anchorentries of a filter page could, if the background request failed, reload the page in an endless loop. - Fixed (accessibility): with auto-inject and the theme's own post loop, screen readers announced "No posts found" after every successful filter click, because the theme's posts carry none of the plugin's markers. The result count is announced now, and "No posts found" only when there really are none.
- Changed (behaviour, all sites): filter URLs beyond the configured limits - more terms per group than Max. Filters per Group, more groups than Max Filter Combinations, more segments than filter groups - are answered with 410 and a short page instead of being truncated and rendered. Terms that do not exist are redirected away (to the remaining selection or the archive) instead of rendering an empty page. Both happen before the database is queried. Switch off with Strict Filter URLs or
JPKCOM_POSTFILTER_STRICT_URLS = false. - Changed (behaviour, all sites): filter pages with more than one term, with unknown terms or with no results are now
noindex, nofollowand carry no canonical (headerX-Robots-Tagas well). Single-term filter pages stayindex, follow. Sites whose multi-term filter pages rank today will see them leave the index over the following weeks - set Indexable Filter Pages higher to keep them. - Changed (markup): term buttons for clients without JavaScript link to the term's own single-term page (Filter Links for Crawlers = single terms). Terms that would lead nowhere useful are rendered as
<button type="button">instead of<a>, styled identically. Visitors with JavaScript notice nothing: the script still combines terms freely. Without JavaScript, clicking a term now switches to that term instead of adding it; choose Combinations within the limits to keep the old behaviour (now capped and sorted). - Changed (interface): theme overrides of
partials/filter/*.phpkeep working but also keep the old unbounded links until they calljpkcom_postfilter_term_link_attributes()like the shipped templates. The reset link no longer carriesrel="nofollow". - Fixed: two canonical tags on every filter page with Rank Math or Yoast - the SEO plugin's (pointing at the blog page) and the plugin's own. The plugin's canonical now goes through the SEO plugin, so there is exactly one. Rank Math's noindex now uses its native keys, so Rank Math also drops its canonical and prev/next links on noindex pages.
- Fixed: pagination linked page 1 as
…/page/1/, a duplicate of the page, from every paginated list. - Fixed:
?jpkcom_filter_path=…(and the fragment and post-type variables) in a query string turned any URL into a filter or fragment response, bypassing every path-based rule. They are now taken only from the matched URL. - Fixed: with the settings cache switched off, every settings read rewrote the cache file - dozens of writes per page. Settings are now read once per request.
- Fixed: a filter click the server answers with an error no longer reloads the whole page onto that error. For a rejected combination and for a 429/5xx the current results, the filter buttons and the address bar stay on the last state that worked, and a message says why (after back/forward the page loads the URL the browser moved to, as before); a quick second click cancels the first request instead of racing it.
- Added: robots.txt rules for the filter combinations (robots.txt Rules:
Disallow: …/filter/*+), applied to every crawler alike - no bot is locked out of content or single-term pages. Only effective when the web server passes/robots.txtto WordPress; the settings page shows the lines for a physical file. - Fixed: custom post type archives under a permalink front (
/blog/%postname%/withwith_front): the filter rewrite rule left the front out, so the filter links the plugin built for them returned 404. Takes effect with the automatic rewrite flush after the update. - Changed (Abilities API):
query-postsno longer returns afilter_urlwhen a requested term does not exist, because the website now redirects that URL to a different selection. The unknown terms are still reported. - Added: Max. Terms in Total, a cap across all filter groups; optional query-string stripping on filter URLs; all caps and policies as
wp-config.phpconstants and filters (jpkcom_postfilter_{setting},jpkcom_postfilter_request_verdict,jpkcom_postfilter_robots_directives,jpkcom_postfilter_canonical,jpkcom_postfilter_term_href,jpkcom_postfilter_term_link_rel,jpkcom_postfilter_robots_txt_rules,jpkcom_postfilter_cache_query,jpkcom_postfilter_donotcache). - Performance: the query cache only stores selections of one term (keyed on the sorted selection), the plugin's options are autoloaded, and the rewrite-flush flag no longer costs a query per request.
- Background: on a production site ~840 000 filter requests a day reached WordPress, every URL unique, every one a full render. The crawlers found them by following the filter bar's own links, which pointed every term at "current selection + this term" in click order without limit. The URL space is now finite: at 10 terms in each of two groups, 20 single-term pages are crawlable instead of tens of millions of combinations.
1.4.7
- Added: updates for inactive JPKCom plugins. WordPress never runs the code of a deactivated plugin, so its own updater never ran and it was never offered an update. Every JPKCom plugin now ships updater 1.5.0; one copy answers for all installed JPKCom plugins, active or not, with the update row, the icon and "View details". The SHA-256 check applies to an inactive plugin exactly as to an active one. On multisite, at least one JPKCom plugin must be network-active or active on the main site
- Security: the stored list of plugin updates no longer contains JPKCom download URLs; they are added back only while a JPKCom updater is loaded, so a request without one cannot install an unverified package. The checksum check now also verifies a package another plugin supplied instead of accepting it, and recognises every GitHub address of a release file
- Changed: plugins installed as a Git (or other version-control) checkout or in a renamed folder are told about a new release but never updated automatically, because an update would delete the checkout or the renamed folder. Update information is fetched only for users who can update plugins, in WordPress's update cron and in WP-CLI, never for anonymous visitors
1.4.6
- Added: a plugin icon. The update manifest now points WordPress to the JPKCom logo, so the Updates screen (Dashboard → Updates) shows it next to this plugin instead of the generic pattern wordpress.org generates from the slug
1.4.5
- Security: the plugin header now carries
Update URI: https://github.com/JPKCom/jpkcom-post-filter. Without it, WordPress also asks wordpress.org for updates under this plugin's slug, so a same-named plugin published there with a higher version would be offered as an update of this one. With the header, wordpress.org answers nothing for this plugin and updates come only from this repository - Changed: the updater keeps its manifest cache, its failure marker and its fetch lock in site transients. On multisite the network now fetches the manifest once instead of once per site; on a single site nothing changes. The old per-site entries expire on their own within a day
- Changed: the release workflow downloads phpDocumentor from a fixed GitHub release and verifies its SHA-256 before running it, instead of executing whatever phpdoc.org serves that day. No effect on the shipped plugin
1.4.4
- Fixed: 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 AJAX fragment was cut out of the rendered page along HTML comment markers, and minifiers remove HTML comments; Autoptimize does so with its stock settings, and its output buffer runs before this 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. The markers are now empty
<template>elements. No minifier removes elements, and the markers never reach the browser either way. - Fixed: a fragment response the script could not use was rendered as "no posts found". That is never what it means — every list template writes its results container before it looks at the query and puts the empty-state message inside it — so a damaged response was shown as a valid, empty result set and the filter looked like it was working. The script now falls back to a full page load, which is slower and always shows what the server would have rendered for the same URL.
- Added: fragment requests ask the HTML optimisers to leave them alone, through Autoptimize's own filter and the shared
DONOTMINIFYconvention. Nothing an optimiser does to a fragment is useful — it has no head, no footer and no asset pipeline to optimise. Other vendors can be added from an mu-plugin throughjpkcom_postfilter_fragment_noptimize_filters; the registration happens while the plugin is loading, because Autoptimize decides once per request and has usually decided bywp.
1.4.3
- Fixed: the 1.4.1 release note said the German translation had fallen behind "for three releases". The actual figure is twelve: the catalogue was generated at 1.1.2, and 1.1.3, 1.1.4, 1.1.5, 1.1.6, 1.1.7, 1.2.0, 1.2.1, 1.2.2, 1.2.3, 1.3.0, 1.3.1 and 1.4.0 all shipped with it stale before 1.4.1 regenerated it. The wrong number understated how long the gap had gone unnoticed, which was the whole point of the note. The 1.4.1 entry has been corrected and marked. No code is affected — this release exists so the corrected note reaches the sites that read the published changelog.
1.4.2
- Added: 46 German translations that were missing. Affected were the three block titles and their descriptions and search keywords, the Elementor and Oxygen widget labels, several settings hints, and the updater's security messages — all of which appeared in English on a German site. 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.
- Fixed: the confirmation dialog shown when removing a filter group with its own taxonomy printed the six characters
\u2014instead of a dash. PHP reads that escape only in its braced form, so the plain one stayed in the text and was shown as-is. - Hardened: the translation check added in 1.4.0 reported every entry carrying a context — the block titles, descriptions and keywords, sixteen in total — as missing from the compiled catalogue, although they were present. It now reads the context the way WordPress stores it.
1.4.1
- Fixed: the German translation had not kept up with the plugin for twelve releases. The translation catalogue was last generated at version 1.1.2, so every text added since — including all of the messages the abilities return — appeared in English on a German site, with nothing anywhere to indicate it. The catalogue now covers the whole plugin again. The existing German translations are unchanged; the newly added texts are listed but not yet translated, so they still appear in English until they are. This entry as originally published said three releases. The figure is twelve — 1.1.3 through 1.4.0 all shipped with the stale catalogue; see 1.4.3
- Hardened: the build now fails when the translation catalogue falls behind the code. Regenerating it was never an automated step and was not on the release checklist, which is how it went unnoticed for twelve releases. It is now both.
1.4.0
- Changed: WordPress 7.0 is now the minimum. Up to 6.9 an unexpected error inside an ability callback ended the whole request with a blank page instead of a readable message, and the plugin carried its own guards against that. From 7.0 WordPress catches it itself. The guards stay in place, but the plugin is no longer tested against 6.9 and no longer claims to run there.
- Hardened: a build check now compares the list of input keys each ability accepts against the list it publishes in its schema. The two are written separately, and nothing compared them before. Had they drifted apart, a caller sending exactly what the schema describes would have been told the key does not exist — the plugin refusing a request that was correct. The check fails the build on either kind of mismatch.
- Changed: the "no input given" default of both abilities is now written directly as an empty object instead of being produced by a helper. What clients receive is unchanged, and it stays an object rather than an empty list.
1.3.1
- Fixed: the query ability is declared read-only, but reading a post's excerpt made WordPress' embed handler run — and with no post to attach its cache to, it stored the result as a published post of its own and fetched the linked address from the third-party site while doing so. One request could therefore add rows to the database and contact outside hosts, and any logged-in user was allowed to trigger it. The excerpt is now read with that handler switched off for the duration and switched back on afterwards, so the text you get back is unchanged.
- Fixed: 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, whether or not the abilities are used.
- Fixed: a search term longer than 1600 bytes was thrown away by WordPress itself, inside the query and without any warning, so the answer was the same one you would have got without searching at all — and nothing in the response said so. Such a term is now refused with a message naming the limit. The limit counts bytes, so accented characters and emoji use more than one each.
- Fixed: the shareable filter link was handed out even when a search term had narrowed the result set. The link cannot carry a search term, so it led to a larger, different list than the one reported beside it — in the clearest case, an answer of "no results" next to a link showing six posts. The link is now withheld in that case; the results themselves are still returned in full.
- Fixed: a filter sent at the wrong nesting level was neither applied nor rejected. The response was a normal, successful-looking answer containing every post on the site, with the filter section empty. Unrecognised input is now refused with a message naming both the key it rejected and the ones it accepts.
- Changed: two statements in the developer documentation were corrected. The claim that nothing unpublished can be reached through the abilities was unconditional and did not hold — the restriction lives in the query the plugin builds, and another plugin can change that query before it runs. The behaviour is unchanged; the description of it now matches.
1.3.0
- Added: the plugin now registers two read-only WordPress Abilities,
jpkcom-post-filter/list-filtersandjpkcom-post-filter/query-posts, in the sharedjpkcom-contentcategory. MCP clients, REST automation and the WordPress AI client can ask which taxonomies and terms a post type can be filtered by, then run a filtered, paginated query and receive both the results and a shareable filter URL. - Added:
JPKCOM_POSTFILTER_ABILITIES(defaulttrue) withdraws both abilities from REST and MCP when set tofalseinwp-config.php, plus the filtersjpkcom_postfilter_ability_metaandjpkcom_postfilter_ability_capabilityfor per-ability control. - Hardened: a filter naming a taxonomy that does not exist previously produced the complete unfiltered result set, because the query builder drops such a clause silently. The query ability now rejects it and names the valid taxonomies, so a caller can correct itself instead of presenting the whole site as a filtered answer.
- Hardened: the query ability always passes a positive page size. The shortcode default of
-1setsno_found_rows, which reports a total of zero regardless of how many posts exist. - Hardened: a single request can filter by at most 50 term slugs per taxonomy; anything beyond that is dropped rather than rejected, so an over-eager caller still gets an answer instead of an error. Without the cap one request could ask for thousands of slugs and turn into a query of the same size.
- Hardened: a search term passed to the query ability is answered from the database rather than from the query cache. Search text is free-form, so caching it would let a caller fill the object cache and APCu with one entry per phrase.
- Changed: the query ability returns an empty
filter_urlfor post types that have no archive page, such aspage. It previously returned a relative address like/filter/news/, which no rewrite rule serves — a link that would have 404'd. - Fixed:
filters,unknown_termsand each post'stermsare sent as{}when they hold nothing, rather than as[]. The output schema declares all three as objects, so a client validating the response against that schema rejected the empty case — which is the most common case forunknown_terms. - Fixed: both abilities can now be called with no input at all. Every parameter is optional, but a bare call was answered with
ability_invalid_input— "input is not of type object" — so the most natural way to ask "which filters does this site have?" was the one way that did not work. - Fixed: the query ability no longer hands out a
filter_urlthat leads somewhere else. The link is now returned only when opening it shows exactly the posts reported next to it, and is left empty in four cases: the post type has no archive page; the site caps the number of terms per taxonomy or the number of taxonomies below what was asked for, so the front end would honour only the first few; a page after the first was requested with a page size that differs from the site's own "posts per page" setting, which is the unit the page number in the URL is counted in; or the requested page lies past the last one, where the URL answers 404. The full result set is returned in every one of those cases — only the link is withheld. - Fixed: the top-level
defaultof both abilities' input schemas is now sent to MCP clients as{}rather than[]. The schema declares it as an object, and WordPress corrected the value on its own REST route but not on the one MCP clients read, so a client that validates schemas saw a contradiction. - Fixed: asking for a page beyond the last one reported
total: 0andtotal_pages: 0next to the page number that was requested — a response that contradicted itself and read as "this site has no posts". It now reports the real totals with an empty post list. - Fixed: caller mistakes — an unknown taxonomy or a post type that is not enabled — are answered over REST with HTTP 400 instead of 500. Both messages name the valid values so the caller can correct itself, which a 500 undoes by telling automation it hit a server fault and should retry the same request unchanged.
- Hardened: a filter group whose taxonomy is no longer registered — the usual cause is deactivating the plugin that provided it — is no longer offered as filterable. Filtering by it used to be accepted on the strength of the configuration alone and then answered with the complete unfiltered corpus; it now returns the unknown-taxonomy error naming the taxonomies that really exist.
1.2.3
- CI: the lint and guard workflow now also runs on pushes to
main. It only covered pull requests, so a direct push with bypass rights skipped every check - Changed: comments, workflow step names and CI output across the repository are now English throughout, and the developer notes in
CLAUDE.mdwere translated and trimmed. No effect on the shipped plugin
1.2.2
- Changed:
Tested up toraised to WordPress 7.1 - Changed: the bundled updater's runtime floor now matches the plugin's own minimum. It bailed out below WordPress 6.8 while the plugin header has required 6.9 for several releases, so the check could never fire on a supported installation
- CI: the release manifest's fallback values for
requiresandtestednow say 6.9 and 7.1. They only apply when the README metadata cannot be read, but a stale fallback would have published a minimum the plugin no longer supports
1.2.1
- Added: plugin banners (
assets/banner-1544x500.avif,assets/banner-772x250.avif) — a plain#3c4955surface with no lettering. The update manifest already advertised these two URLs, but nothing was published under them, so the plugin card in wp-admin had a broken banner
1.2.0
- Added: filter requests are answered with just the swappable zones instead of a complete page. The theme header, nav menus, sidebar widgets and the entire asset pipeline are skipped; the theme's loop still runs, because in auto-inject mode it produces the result markup. Measured on a test install: 60–72 % less transferred, 5–19 % less server time. The request goes through a
/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 - Fixed: the
noindexfor filter URLs with unknown term slugs never took effect on a Rank Math site. Rank Math discards everywp_robotscallback before writing its own tag, so the protection added in 1.1.7 was inert on exactly the sites it was written for. The rule now also hooks Rank Math and Yoast - Fixed: on a filter URL with no results the "no posts found" message and the whole filter bar were rendered below the footer. They now appear where the listing sits when there are results
- Fixed: using the browser's back button after filtering left the previous results on screen and the filter buttons pressed — the history entry created by the initial page load carries no state, and the handler ignored it
- Fixed: paginating a filtered list quietly stopped using AJAX. WordPress's canonical redirect did not recognise the fragment segment and appended the page it thought was missing, so the request 404'd and fell back to a full reload
- Fixed:
apcu_cache_info()raised a PHP warning wherever APCu is loaded but inactive for the running SAPI —apc.enable_clidefaults to 0, so every WP-CLI call produced one. Withdisplay_errorson it was printed into the response body - Added: rewrite rules are flushed once after a version change.
register_activation_hookdoes not fire on an update, so new rules would otherwise never reach the database - Added:
tests/browser-check.mjsdrives a real filter click in headless Chromium
1.1.7
- Fixed: the settings-directory containment check was a tautology — it compared
realpath( WP_CONTENT_DIR )with itself and never referenced the directory being validated, so it never fired. SinceJPKCOM_POSTFILTER_SETTINGS_DIRis overridable fromwp-config.phpand holds PHP files that are laterincluded, the path is now genuinely verified to resolve insidewp-content - Hardened:
jpkcom_postfilter_build_query_args()no longer copiesmeta_key,meta_value,meta_query,s,author,yearandmonthnumverbatim intoWP_Query. Each is now coerced to a safe shape andmeta_queryis not forwarded at all; callers that need it use thejpkcom_postfilter_query_argsfilter. No current caller passed user input, so this closes a latent path rather than an exploited one - SEO: filter URLs referencing term slugs that do not exist are now marked
noindex, follow. They still return 200 with zero results, so existing links keep working, but they no longer generate unlimited indexable, self-canonicalising thin-content URLs. The check reuses the cached per-taxonomy term list and is keyed by taxonomy, never by the requested slugs - Added:
tests/test-security.php— regression tests for all of the above, each written to fail against the previous implementation. Run in CI on every pull request
1.1.6
- Security: update packages are now verified before installation — the verified file is handed to WordPress instead of being downloaded a second time, so the bytes that were checked are the bytes that get installed
- Security: a missing or unfetchable SHA-256 checksum now aborts the update instead of installing unverified code (previously it silently skipped verification)
- Security: pinned every GitHub Action to a full commit SHA and added Dependabot with a 7-day cooldown, so a moved tag can no longer change the release build
- Security: tightened which download the updater claims, so sibling plugins cannot match each other's package
- Fixed:
sprintf()calls in the updater bound named arguments to a variadic parameter, which raisesArgumentCountErroron PHP 8.3 - Fixed: the "View Details" modal could fail with a
TypeErrorwhen the manifest omittedrequires_plugins - Performance: a failed manifest fetch is now cached for an hour instead of being retried on every admin request
- Added: CI workflow on every pull request (PHP lint, named-argument check, YAML validation, action-pinning guard)
- Housekeeping: removed stray editor backups and an unused
messages.mofrom the release package
1.1.5
- Raised "Tested up to" to WordPress 7.0
- Normalized license fallback defaults (updater and release workflow) to
GPL-2.0-or-laterwith the HTTPS license URI
1.1.4
- Security: updater prefers an exact match against the manifest
download_urlover the slug heuristic, so a tampered manifest can no longer bypass the checksum gate - Security: timing-safe checksum comparison (
hash_equals()) with anis_string()guard againsthash_file()failures - Security: manifest fetch via
wp_safe_remote_get()(SSRF defense-in-depth) - Fixed PHP warning and missing contributor names in the plugin detail popup (
display_namenow provided) - Fixed PHP warning/deprecation on
wp plugin listby completing theno_updatetransient entry (new_version,package,tested,requires_php)
1.1.3
- Plus/Minus Icon styling — New CSS variables (
--jpkpf-pm-color,--jpkpf-pm-font-size,--jpkpf-pm-font-weight) for independent styling of the +/– icons in filter buttons - Layout & Design → Filter tab — New "Plus/Minus Icon" section with Color, Font Size, and Font Weight fields
- Force no underline — New checkbox in Layout & Design → Advanced that applies
text-decoration: none !importantto all filter buttons and icons, fixing themes that force underlines on links - Translations — Updated de_DE and de_DE_formal translations for all new strings
1.1.2
- Plugin Updater — Fixed manual ZIP upload failing with "invalid URL" error by adding
wp_http_validate_url()check inverify_download_checksum() - Release Workflow — Fixed ZIP packaging without top-level directory; WordPress now correctly recognises the update by using a staging directory with the plugin slug
1.1.1
- .github/workflows/release.yml — "Build Gutenberg blocks" npm ci bugfix
1.1.0
- Gutenberg Blocks — Three native blocks (Post Filter, Post List, Post Pagination) with live server-side preview, InspectorControls, and Full Site Editor support
- Elementor Widgets — Three widgets (Post Filter, Post List, Post Pagination) in a dedicated category, loaded only when Elementor is active
- Oxygen Builder Elements — Three elements (Post Filter, Post List, Post Pagination) using the OxyEl API, loaded only when Oxygen Builder Classic is active
- Block pre-scan — Pagination blocks can be placed above or below the list block; the plugin pre-scans the block tree (FSE templates and post content) before rendering to ensure correct query availability
- AJAX pagination swap — Standalone pagination elements (blocks/shortcodes/Elementor) are updated during AJAX filter requests via
data-jpkpf-paginationattribute, keeping pagination links filter-aware - Pagination placeholder — When filter selection reduces results to a single page, pagination is hidden but preserved as a DOM placeholder; it automatically reappears when filters change back to multiple pages
- Auto-inject guard — Prevents duplicate pagination insertion in auto-inject mode during AJAX swaps
1.0.0
- Initial release
- Faceted filtering for any post type and taxonomy
- Four filter layouts: bar, columns, sidebar, dropdown
- Three list layouts: cards, rows, minimal
- SEO-friendly URL schema with WordPress rewrite rules
- AJAX filtering with
history.pushState - Auto-inject mode for archive/blog pages with configurable pagination position
- Shortcodes:
[jpkcom_postfilter_filter],[jpkcom_postfilter_list],[jpkcom_postfilter_pagination] - Interactive shortcode builder in admin
- Custom taxonomy registration from Filter Groups admin page
- Plus/Minus interaction mode for filter buttons
- Show More button with configurable threshold
- Four predefined color schemes (Default, Dark, Contrast, Monochrome)
- Three stylesheet modes (Full, Variables only, Disabled)
- Reset button visibility modes (Always, On selection, Never)
- Bare Endpoint Behaviour (404, redirect to home, custom URL)
- Max. Filters per Group limit (URL + JS enforcement)
- Four-layer caching (object cache, transients, APCu, file cache)
- Settings import/export (JSON)
- Translations: de_DE, de_DE_formal