Before
A page 404s and nothing in the admin says which rule took the URL first.
After
The rules in match order, with the winner, the loser, an example path and the slug to change.
Most unexplained 404s have one cause: WordPress matches rewrite rules in order and stops at the first fit, and nothing in the admin shows you that order. This plugin shows the order, explains each conflict by naming the winner, the loser and an example path, and tells you which slug to change. The second half is about getting staging's work to production: a signed configuration export and selective content sync, instead of copying a whole database over the orders placed since that copy was taken.
WordPress matches a request against a list of rewrite rules, in order, and stops at the first one that fits. That single sentence is behind most unexplained 404s: a rule registered earlier swallows a URL a later rule was meant to handle, and the second rule quietly stops working. Plugins keep adding rules, and nothing in the admin ever shows the order — so the person looking for the cause of a 404 has nothing to look at.
The Rules tab shows that list in match order and puts conflicts at the top. The detection is concrete rather than theoretical: the plugin builds URLs the losing rule was meant to serve and checks whether an earlier rule takes them first. The result names four things — which rule wins, which loses, an example path, and usually one rewrite slug to change. The source of each rule is inferred from the query vars it sets, and the interface labels that as an inference rather than presenting it as fact.
The redirect manager takes 301, 302, 307 and 308, and by default only sees requests WordPress could not already serve, so a page that really exists is never shadowed. Regular expressions are compiled and timed before they are stored, and one that takes more than 50 milliseconds on a probe input is refused — because the real problem is not the pattern but the input: that pattern runs on every request, and any visitor can send a long URL. Off-site destinations are refused too, unless you allow the host deliberately, because a redirect manager that accepts any destination is an open redirect.
The second half moves work between two sites. The usual way is to copy staging's whole database over production, which destroys every order placed since that copy was taken. Here the transfer is selective: a signed configuration file for Elementor, ACF, Rank Math, Yoast, WooCommerce and WordPress itself, and a sync for pages, posts, Elementor templates, ACF field groups, menus and the product catalogue. Dry run is the default, every import is previewed first and the last five can be rolled back, and the run log says what was skipped and why.
Because this plugin lets two live sites write to each other, the security of the transfer is the product rather than a footnote. Every request between them is signed with HMAC-SHA256 over the method, the route, the query string, a timestamp, a single-use nonce and a digest of the body; leaving any of those out opens a specific attack. But a signature only says who wrote something, not that its contents are safe — a legitimate site that has itself been compromised will sign anything with a valid key. So two deny-lists sit outside the reach of any filter: one that never lets active_plugins, siteurl or the auth keys through, and one that never synchronises orders, refunds, subscriptions, coupons, customers, users, sessions or payment tokens.
Before
A page 404s and nothing in the admin says which rule took the URL first.
After
The rules in match order, with the winner, the loser, an example path and the slug to change.
Before
The only way to get staging's settings onto production is to copy the whole database — which erases every order placed meanwhile.
After
Only what you ticked moves, with a preview and a dry run. Orders, customers, users and payment tokens are excluded in code.
Before
A redirect manager that accepts any destination and any pattern is both an open redirect and a way to slow the site down.
After
Patterns are compiled and timed before they are stored, and off-site destinations are refused unless you allow the host deliberately.
The first place to look. Anything listed under Conflicts is a URL on your site that 404s for no apparent reason.
Each conflict says which rule wins, which one never runs, and which rewrite slug to change. Then flush and look again — once every thirty seconds.
Generate the shared secret on one site and paste it into the other, press Test, run Compare, then a dry run before bringing anything across. Every run is logged.
Sites whose post types and taxonomies come from several plugins · sites where some URLs started 404ing after a plugin was activated · shops that build the catalogue and the pages on staging first · teams entering Elementor and ACF settings twice by hand · agencies keeping a client site alongside a staging copy · anyone who changed the permalink structure and cannot see which rule now covers which · teams who are not allowed to overwrite production's database with a staging copy.
Both halves sit inside the plugin itself. No Composer at runtime, no cloud service, and no jQuery.
The list is read from the stored routing table rather than regenerated — because the stored one is what matching actually uses, and that distinction is the whole point when somebody is debugging a 404. The source of each rule is inferred from the query vars it sets, and shown as an inference.
The plugin builds URLs the losing rule was meant to serve and checks which rule takes them first. Every finding carries a severity: high means a post type or taxonomy has become unreachable altogether. The scan is quadratic and therefore bounded, at 400 rules by default.
Needed after registering a new post type or changing the permalink structure. It takes seconds on a large site and the button invites repeated clicking, so it is allowed once every thirty seconds. The .htaccess file is left alone; a plugin rewriting a server config file unasked is a good way to take a site offline.
301, 302, 307 and 308, with $1-style capture groups in the destination when the source is a regular expression. By default it runs only for requests WordPress could not already serve, so a page that really exists is never covered up. The Test button says whether a redirect matches and exactly where it sends.
Every pattern is compiled and run against long probe inputs; one that hits PCRE's backtrack limit or takes more than 50 milliseconds is refused with an explanation rather than left to slow the site on every request. The plugin owns the delimiter and the modifiers so a pattern cannot arrive carrying its own, and $1 substitution is plain str_replace rather than preg_replace, so a captured group containing regex syntax cannot influence the result.
Otherwise anyone who can add a redirect gets to send your visitors to their page with your domain in the link. External destinations are refused unless the host is allowed through the hd_ras_allowed_redirect_hosts filter, and scheme-relative destinations are checked separately because they are the usual way past a check like this.
Seven groups: site basics, Elementor, ACF, Rank Math, Yoast, WooCommerce display settings and this plugin itself. It detects which of them are present on the site and does not offer the rest. An import verifies the signature before it parses the file — parsing something unverified is itself an attack surface.
They are encrypted with AES-256-GCM, and each purpose gets its own key derived with HKDF. If the file has to open on a site that has never been paired with this one, set a passphrase: the key is derived with PBKDF2-SHA256 over 310,000 rounds and twelve characters is the minimum, because guessing a passphrase happens offline, as often as the guesser likes.
The preview says exactly what would change, and Apply stays disabled until you have taken one. If the file changes between the preview and the confirmation, the plugin notices and asks for a fresh preview. The last five imports can be rolled back, and a rollback goes through the same allowlist.
Compare this site with a paired one and bring across only what differs: pages, posts, Elementor templates, ACF field groups, menus and the product catalogue. The comparison runs on a manifest and a fingerprint, so syncing an unchanged site costs one small response rather than a full transfer. Dry run is the default.
The first never lets active_plugins, template, siteurl, home, users_can_register, the auth keys and salts or this plugin's own identity be written from a configuration file. The second never synchronises orders, refunds, subscriptions, coupons, customers, users, sessions, payment tokens or any post whose status begins wc-. The product_visibility taxonomy is out too, because that is where WooCommerce keeps stock status.
HMAC-SHA256 over the scheme version, the method, the route, the timestamp, the nonce and a digest of the body. Timestamps outside a five-minute window are refused, nonces are single-use and claimed atomically so two simultaneous copies of a replayed request cannot both succeed, and signatures are compared in constant time. The nonce is spent only after the signature verifies, or anyone could burn a legitimate sender's nonces by guessing.
Every address the plugin connects to is checked first: loopback, private ranges and cloud metadata endpoints are refused. Host names are resolved before the decision — evil.example.com can point at 127.0.0.1 — and the connection is pinned to the address that passed the check, so the name cannot be repointed in between.
Every sync and every import is recorded with a run id and the outcome for each object. The things that were deliberately skipped are in there with their reason — a post with no slug, a type on the deny-list, an object already identical at both ends. A log that shows only the successes hides exactly what needs seeing.
All of them in docs/hooks.md, each with an example. The four that widen behaviour — hd_ras_allow_private_url, hd_ras_update_hosts, hd_ras_allowed_redirect_hosts and hd_ras_option_groups — are marked there with what they cost, so nobody switches one on without knowing the trade.
A complete fa_IR translation ships with the plugin, and the admin loads admin-rtl.css when is_rtl(). Patterns, URLs and keys are marked dir="ltr" so they do not reverse into nonsense on a right-to-left page. The tabs carry WAI-ARIA semantics and move with the arrow keys. No jQuery — vanilla ES6, on this plugin's own screen only.
| Capability | Detail | Included |
|---|---|---|
| Rules in match order | Read from the stored routing table, not regenerated | |
| Inferred source per rule | From the query vars it sets, and labelled in the interface as an inference | |
| Conflict detection by building URLs | The URLs the losing rule was meant to serve are built and tried | |
| A severity per conflict | High means a post type or taxonomy has become unreachable altogether | |
| An example path per conflict | Plus the rewrite slug to change; sample URLs are labelled as worked examples | |
| The conflict scan is bounded | 400 rules by default, through hd_ras_conflict_scan_limit | |
| Flush with an enforced interval | Once every thirty seconds, and .htaccess is never touched | |
| 301, 302, 307 and 308 | With an option for case-sensitive matching | |
| $1 capture groups in the destination | Plain str_replace, highest index first, deliberately not preg_replace | |
| Patterns validated before storage | A 50-millisecond budget on a probe input, and 2000 characters | |
| The plugin owns the delimiter and the flags | A pattern cannot arrive carrying its own | |
| Loops refused, chains reported | A chain is sometimes deliberate; a loop never is | |
| A Test button before you trust it | Says whether it matches and exactly where it sends | |
| Only requests WordPress could not serve | The default, so a page that really exists is never shadowed | |
| Off-site destinations refused | Unless the host is allowed deliberately through hd_ras_allowed_redirect_hosts | |
| Seven settings groups | Site basics, Elementor, ACF, Rank Math, Yoast, WooCommerce and this plugin itself | |
| AES-256-GCM on values that look like secrets | With a separate key per purpose, derived with HKDF | |
| Passphrase key through PBKDF2 | SHA-256, 310,000 rounds, twelve characters minimum | |
| The signature is verified before the file is parsed | Parsing something unverified is itself an attack surface | |
| The allowlist is checked again before each write | A signature says who wrote the file, not that its contents are safe | |
| Preview, and rollback of the last five imports | If the file changes in between, a fresh preview is required | |
| Six content sets | Pages, posts, Elementor templates, ACF field groups, menus and the product catalogue | |
| Matched on post type and slug | Never on a numeric id; a post with no slug is skipped | |
| A fingerprint decides what differs | Stock level and sales count are deliberately outside it | |
| Dry run by default | It reports without writing anything | |
| Manifest comparison | Four numbers: only there, different, identical, only here | |
| HMAC-SHA256 on every request | Method, route, timestamp, nonce and body digest, within a five-minute window | |
| Single-use nonces, claimed atomically | And spent only after the signature verifies | |
| Permissions per pairing | A read-only pairing cannot write anything here | |
| A guard on outbound addresses | Loopback, private ranges and cloud metadata refused, resolved before the decision and pinned to that address | |
| Secrets are write-only | The private key and the site secret live in their own option, outside what the exporter walks |
| Item | Minimum |
|---|---|
| WordPress | 6.2 or newer, tested to 6.8 |
| PHP | 8.0 to 8.3 |
| OpenSSL | Required to encrypt sensitive values in a configuration file. For signing, the plugin uses it where present and otherwise authenticates files with the shared secret, and the interface says which is in use. The sync channel needs neither. |
| A second site | For syncing only: two sites running the pro tier, the same shared secret at both ends, and REST access between them. The rules auditor and the redirect manager work on a single site. |
| Capability | manage_options to open the screen and for every write, changeable through hd_ras_manage_capability |
One that runs several plugins, each registering its own post types, taxonomies or rewrite rules — which is where an unexplained 404 shows up and nobody knows which rule took the URL. Its second audience is a team working on staging that has to get the result onto production without overwriting production's database with an older copy. For a ten-page site with no custom post types it has little to say.
The auditor runs only when you open its screen; it measures nothing on ordinary requests. The redirect manager runs on template_redirect and by default only for requests WordPress could not already serve, so a request that hits a real page never reaches it, and the active redirect list is cached. Expensive patterns are not allowed to be saved in the first place, which is exactly why that check exists. A module you switch off is never loaded at all.
Because it is derived from the losing rule's own pattern: the plugin builds the URLs that rule was meant to serve and tries them against the full list, and if a rule higher up takes them first the conflict is real and you get the example path. Two things are not hidden either: the source of each rule is an inference from its query vars and is labelled as one, and the sample URLs are worked examples rather than observed traffic. The list is also read from the routing table WordPress last computed — if it looks stale, that is the point, so flush and compare.
Two common reasons. One is backtracking: patterns are tested against long inputs before they are saved, and one that takes more than 50 milliseconds or hits PCRE's backtrack limit is refused. Nested quantifiers are the usual cause; (.+)+ is not what you meant, (.+) is. The other is including delimiters: write ^/old/(.*)$, not a version wrapped in ~ with flags after it. The strictness is there because this pattern runs on every request and any visitor can send an eight-kilobyte URL.
Because a redirect manager that accepts any destination is an open redirect: anyone who can add a redirect gets to send your visitors to their own page, with your domain in the link. If it is deliberate, add the host through the hd_ras_allowed_redirect_hosts filter — which puts the decision in code rather than in a text field.
No. Orders, refunds, subscriptions, coupons, customers, users, sessions and payment tokens are excluded in code, and no setting or filter reaches that list. The reasoning: a staging site is a copy taken at some past moment, and anything transactional that happened on production since exists only there; copying staging's version over it does not merge the two, it destroys one. For the same reason stock level and sales count are outside the comparison fingerprint and never move.
That is exactly what a signature does not solve: it only says who wrote the file or the request. So there are two separate deny-lists, checked after verification and again before each write, and no filter reaches either — one stops active_plugins, siteurl, home, users_can_register and the auth keys, the other stops transactional data. The practical advice is in the guide: on production, keep the pairing with staging read-only, so staging can compare but cannot write anything.
Up to five minutes of drift is tolerated; beyond that requests are refused and the message says so. The window is deliberately narrow, because a wide one is what makes a captured request replayable. If you see that error, check NTP on one of the two servers.
For encrypting sensitive values in a configuration export, yes. For signing a file, the plugin uses OpenSSL where it is available and otherwise authenticates the file with the shared secret, and the interface says which is in use rather than quietly being weaker. The sync channel between two sites needs neither; that is HMAC.
Both halves, both active: rules in match order, conflict detection with an example path, a safe flush, and the full redirect manager with pattern validation, loop and chain detection and the Test button — plus the transfer half: signed configuration export and import, encryption of sensitive values, and selective content sync with staging. Syncing needs this same package installed at both ends, because the side that answers is part of the same plugin.
No. The plugin ships its own small autoloader and has no runtime dependencies. The only address it connects to is the paired site you nominate yourself; there is no licence check and no built-in updater. Updates arrive through Zhaket.
Nothing is deleted unless you switched on "delete all data when the plugin is removed", which is off by default. The single exception is the replay markers, which are single-use values with no meaning to anyone. Deactivating clears scheduled events and rebuilds the routing table without this plugin's rules, but never touches stored data.
Plugin bug fixes, help with installation and with pairing two sites, and help reading the conflicts and deciding which slug to change. Redesigning the site's permalink structure, fixing the plugin that registered the conflicting rule, and full site migrations fall outside support and are agreed separately.