<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Guy-Argaman</title>
    <description>The latest articles on DEV Community by Guy-Argaman (@guyargaman).</description>
    <link>https://dev.to/guyargaman</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4186922%2F4925866c-56a4-427c-8fb4-8836427da55c.png</url>
      <title>DEV Community: Guy-Argaman</title>
      <link>https://dev.to/guyargaman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/guyargaman"/>
    <language>en</language>
    <item>
      <title>Why your custom post type URLs 404 (and how to never re-save permalinks again)</title>
      <dc:creator>Guy-Argaman</dc:creator>
      <pubDate>Thu, 15 Oct 2026 14:02:50 +0000</pubDate>
      <link>https://dev.to/guyargaman/why-your-custom-post-type-urls-404-and-how-to-never-re-save-permalinks-again-42h</link>
      <guid>https://dev.to/guyargaman/why-your-custom-post-type-urls-404-and-how-to-never-re-save-permalinks-again-42h</guid>
      <description>&lt;p&gt;You register a custom post type, publish a post, open it, and get a 404. Nothing is wrong with your code. You go to &lt;strong&gt;Settings → Permalinks&lt;/strong&gt;, click &lt;strong&gt;Save&lt;/strong&gt; without changing anything, and now it works.&lt;/p&gt;

&lt;p&gt;Every WordPress developer learns this ritual eventually. Clients learn it too, usually right after a deploy. I finally got annoyed enough to find out exactly why it happens and to automate it away. This is what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routes are described in code but served from a cache
&lt;/h2&gt;

&lt;p&gt;When you call this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;register_post_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'recipe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'public'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'rewrite'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'slug'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'recipes'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WordPress doesn't create any URL rules yet. It records a &lt;em&gt;permastruct&lt;/em&gt; (&lt;code&gt;/recipes/%recipe%&lt;/code&gt;) in the global &lt;code&gt;$wp_rewrite&lt;/code&gt; object. &lt;code&gt;add_rewrite_rule()&lt;/code&gt; and &lt;code&gt;add_rewrite_endpoint()&lt;/code&gt; work the same way: they describe routes and nothing more.&lt;/p&gt;

&lt;p&gt;The rules WordPress actually matches incoming URLs against are a big array of regex → query pairs, like &lt;code&gt;recipes/([^/]+)/?$ =&amp;gt; index.php?recipe=$matches[1]&lt;/code&gt;. Building that array is expensive, so WordPress builds it once and stores it in the &lt;code&gt;rewrite_rules&lt;/code&gt; option. Every request reads the stored copy.&lt;/p&gt;

&lt;p&gt;Nothing rebuilds the stored copy when your code changes. That only happens when something calls &lt;code&gt;flush_rewrite_rules()&lt;/code&gt;, and saving Settings → Permalinks is one of the few things that does. Until then, &lt;code&gt;/recipes/my-pie/&lt;/code&gt; is matched against rules that have never heard of recipes, and you get a 404.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "just flush on init" is the wrong fix
&lt;/h2&gt;

&lt;p&gt;The first fix everyone tries is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;register_post_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'recipe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* … */&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;flush_rewrite_rules&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Don't.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works, and it rebuilds the entire rule set and writes it to the database &lt;strong&gt;on every single request&lt;/strong&gt;. The function's documentation warns that flushing is expensive and belongs in activation hooks, not on every page load.&lt;/p&gt;

&lt;p&gt;The recommended pattern is to flush once in your plugin's activation hook. That's correct, but it only covers your own plugin being activated. It misses theme switches, code deploys where a slug changed, another plugin adding routes, and every site where someone edited a post type in a code snippet plugin. That's why the ritual survives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: rebuild only when the routes actually changed
&lt;/h2&gt;

&lt;p&gt;What we want is "flush whenever the described routes differ from the ones the cached rules were built from". That's cheap to check, because everything the rules are built from is sitting right there in &lt;code&gt;$wp_rewrite&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_loaded'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'permapilot_maybe_flush'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;PHP_INT_MAX&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;permapilot_maybe_flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$fingerprint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;permapilot_fingerprint&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'permapilot_fingerprint'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$fingerprint&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// A soft flush rebuilds the rules in the database without rewriting .htaccess.&lt;/span&gt;
    &lt;span class="nf"&gt;flush_rewrite_rules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;update_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'permapilot_fingerprint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$fingerprint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;permapilot_fingerprint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="nv"&gt;$wp_rewrite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;wp_json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;$wp_rewrite&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;extra_permastructs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Post types and taxonomies.&lt;/span&gt;
        &lt;span class="nv"&gt;$wp_rewrite&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;extra_rules_top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// add_rewrite_rule( …, 'top' ).&lt;/span&gt;
        &lt;span class="nv"&gt;$wp_rewrite&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;extra_rules&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// add_rewrite_rule( …, 'bottom' ).&lt;/span&gt;
        &lt;span class="nv"&gt;$wp_rewrite&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// add_rewrite_endpoint().&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;wp_loaded&lt;/code&gt;, not &lt;code&gt;init&lt;/code&gt;.&lt;/strong&gt; Plugins and themes register routes on &lt;code&gt;init&lt;/code&gt;, at all sorts of priorities. By &lt;code&gt;wp_loaded&lt;/code&gt;, everyone is done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A soft flush.&lt;/strong&gt; Passing &lt;code&gt;false&lt;/code&gt; rebuilds the rules in the database and leaves &lt;code&gt;.htaccess&lt;/code&gt; alone. WordPress's &lt;code&gt;.htaccess&lt;/code&gt; block is a generic catch-all anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When nothing changed, the whole thing costs one autoloaded option read and one hash per request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The edge case that almost made it slow
&lt;/h2&gt;

&lt;p&gt;Then I found a problem. Some plugins register a post type only when &lt;code&gt;is_admin()&lt;/code&gt; is true. On those sites, admin requests and front-end requests describe &lt;em&gt;different&lt;/em&gt; routes, so the fingerprint flips on every request, and every request rebuilds the rules. That's exactly the "flush on init" disaster I was trying to avoid.&lt;/p&gt;

&lt;p&gt;I wrote a test that reproduced it first: alternate 10 front-end requests with 10 &lt;code&gt;admin-ajax.php&lt;/code&gt; requests and count the rebuilds. The result was &lt;strong&gt;20 rebuilds in 20 requests&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The fix is a cap of 5 rebuilds per 10 minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$now&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$rebuilds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'permapilot_rebuilds'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$time&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$now&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="no"&gt;MINUTE_IN_SECONDS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$rebuilds&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real route changes still apply on the next request. A flip-flopping site can never rebuild more than 30 times an hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens to the old URLs?
&lt;/h2&gt;

&lt;p&gt;Fixing new URLs raised the opposite question: when you rename a slug from &lt;code&gt;recipes&lt;/code&gt; to &lt;code&gt;recipe&lt;/code&gt;, what happens to every link pointing at &lt;code&gt;/recipes/pie/&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;I expected 404s, and was surprised. WordPress's 404 handler, &lt;code&gt;redirect_guess_404_permalink()&lt;/code&gt;, guesses a post with a matching slug and 301-redirects to it. So old &lt;em&gt;post&lt;/em&gt; URLs mostly survive, but the guess is fuzzy: a typo like &lt;code&gt;/recipe/pi/&lt;/code&gt; also gets sent to "pie". And it only ever looks for posts. Rename a taxonomy from &lt;code&gt;genre&lt;/code&gt; to &lt;code&gt;genres&lt;/code&gt; and every &lt;code&gt;/genre/jazz/&lt;/code&gt; link is a plain 404, forever.&lt;/p&gt;

&lt;p&gt;That gap is what I built a paid add-on for, Permapilot Pro. It remembers each post type's and taxonomy's old URL patterns and sends exact 301 redirects, for terms too. It also emails you when a route disappears after an update and visitors start landing on 404s there. The free plugin is complete on its own; this is the part I charge for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing it against a real WordPress
&lt;/h2&gt;

&lt;p&gt;Rewrite bugs live in the gaps between plugins, caches and requests, so mocks wouldn't have caught any of the bugs above. Every test runs against a real WordPress in Docker, set up by &lt;code&gt;@wordpress/env&lt;/code&gt;. A small must-use plugin lets each test "change the site's code" by flipping an option, and the test then requests the URL like a browser would:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a newly registered post type's posts are reachable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;option&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;update&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pa_fixture_cpt_slug&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;book&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;post&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;create&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--post_type=book&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--post_status=publish&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--post_title=Dune&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--post_name=dune&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/book/dune/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the plugin, that test gets the classic 404. With it, a 200, and nobody touched Settings → Permalinks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Permapilot is free on WordPress.org, has no settings, and works the moment it's activated:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://wordpress.org/plugins/permapilot/" rel="noopener noreferrer"&gt;https://wordpress.org/plugins/permapilot/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you can break it, I'd love to hear how. And if you rename slugs on live sites, &lt;a href="https://checkout.freemius.com/plugin/41571/plan/72192/" rel="noopener noreferrer"&gt;Permapilot Pro&lt;/a&gt; keeps the old URLs working.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
