SEO & Structured Data

Permalink Structure Simulator: What That One Field Decides

The permalink field decides thirteen URL shapes at once, and most of them fail quietly when you change it. What each structure tag costs, what verbose page rules actually do, which collisions core resolves first, and which old URLs its canonical redirect will never rescue.

Permalink Structure Simulator: What That One Field Decides

Settings, Permalinks is one text field. The radio buttons above it are shortcuts that write into that field, and the two boxes below adjust two archives. Everything else on your site’s URL surface comes out of those few dozen characters.

Which is why the regret always arrives late. A blog runs three years on /%year%/%monthnum%/%postname%/, somebody decides the dates look stale in search results, and the structure becomes /%postname%/. Every link on the site keeps working, because every link is generated fresh from the new structure. The URLs in Google’s index do not. /2023/04/my-post/ now matches no rule: no post rule accepts three segments, no date rule accepts a non numeric third segment, and the one rule that does match is the page rule, which asks the database for a page at that path, gets nothing, and moves on. The request 404s until somebody writes a redirect.

Core does rescue some of the cases around that one. It just does not rescue that one, and the boundary between the two sets is invisible from the settings screen.

One field, thirteen decisions

The structure sets the single post URL, which is the part everyone thinks about. It also sets the page URL, the category and tag archives, the author archive, the year, month and day archives, pagination on all of those, the main feed, the per post comments feed, attachment pages and the search URL. Some inherit the static prefix in front of your first tag and some deliberately refuse it. Working out which by hand means reading WP_Rewrite::init() and four permastruct getters.

The simulator below does that derivation for you. Type a structure and it prints all thirteen URLs, flags the collisions, states whether the structure switches on core’s verbose page rules and what that costs, and diffs an old structure against a new one to produce both a plain old to new mapping and a set of Apache rewrite rules. It runs entirely in the tab: nothing is uploaded, nothing is stored.

WunderPaint
The Dynamic Design and Automation Studio
WunderPaint is a layered image editor for your WordPress media library. WunderPaint Studio is the same thing in any browser, free and without an account.

What the field actually derives

When WordPress boots, WP_Rewrite::init() pulls four values out of the one string. The front is everything before the first percent sign: substr( $structure, 0, strpos( $structure, '%' ) ). For /blog/%postname%/ the front is /blog/. The root is empty unless you are on PATHINFO permalinks, when it becomes index.php/. Trailing slashes are one call, str_ends_with( $this->permalink_structure, '/' ), so the slash at the end of your structure is the slash on every URL the site emits.

The date archives are stranger. Core does not read your date tags in place. It checks whether the structure contains one of exactly three orderings, %year%/%monthnum%/%day%, %day%/%monthnum%/%year% or %monthnum%/%day%/%year%, and falls back to the first if it finds none. Then it scans the first three tags, and if %post_id% is among them it pushes the whole date archive under front + date/, because a numeric id and a numeric year in the same position cannot be told apart. So /%post_id%/%postname%/ quietly gives you /date/2024/04/, and nothing on the settings screen says so.

Two asymmetries catch people. Pages take the root and never the front, so /blog/%postname%/ puts posts at /blog/whatever/ and pages at /whatever/; feeds and search behave the same way. And the category base reads backwards. In wp-includes/taxonomy.php the taxonomy is registered with 'with_front' => ! get_option( 'category_base' ) || $wp_rewrite->using_index_permalinks(). An empty base keeps the front, typing your own base drops it. On /blog/%postname%/, leaving the base alone gives /blog/category/news/ and typing topics gives /topics/news/.

The tags and what each one costs

Each structure tag is substituted for a regular expression and a query variable when rules are generated. Those substitutions are literal arrays in WP_Rewrite, $rewritecode, $rewritereplace and $queryreplace, lined up by index, plus two tags added at registration time: %category% becomes (.+?) because the category taxonomy is hierarchical, and %tag% becomes ([^/]+) because post tags are not.

Table mapping the ten WordPress permalink structure tags to the regular expression core substitutes into a rewrite rule and the query variable it sets, showing that only %post_id% and %postname% identify a single post.

The last column is the one that matters. Only two tags identify a post on their own. %post_id% maps to p=, a primary key lookup that can never be ambiguous. %postname% maps to name=, unique in practice because WordPress appends -2 and -3 when a slug is taken. Everything else is a filter, not an identifier, and a structure built only from those cannot resolve to a single post at all. Pattern width matters too: %category% swallows slashes, so the boundary between a nested category and whatever follows it is a guess the regex engine makes lazily.

The verbose page rules question

This is the part repeated as folklore for fifteen years, usually as “never start your permalink with %postname%, it is slow”. The mechanism is real, the advice built on it is wrong, and the cost is measurable. At the bottom of WP_Rewrite::init() there is one test:

// Enable generic rules for pages if permalink structure doesn't begin with a wildcard.
if ( preg_match( '/^[^%]*%(?:postname|category|tag|author)%/', $this->permalink_structure ) ) {
	$this->use_verbose_page_rules = true;
} else {
	$this->use_verbose_page_rules = false;
}

Read [^%]* carefully, because that is the piece the folklore misses. A static prefix is made entirely of non percent characters, so it matches. /blog/%postname%/ turns verbose page rules on, exactly like /%postname%/ does. The advice to “put /blog/ in front so it does not start with a tag” buys nothing. What flips the flag off is a numeric tag first: /%year%/%postname%/ and /%post_id%/%postname%/ do not match, because the first tag they present is not one of those four.

The flag does two things. It reorders the rule array, merging page rules before post rules when verbose and after when not. And it changes how WP::parse_request() treats a match: for every rule whose query contains pagename=$matches[n], core calls get_page_by_path() with the captured path and, if there is no such page or its status is not viewable, continues to the next rule instead of accepting the match.

On this blog, running /%postname%/, the rewrite rules option holds 247 rules. Seven are core’s page rules, at indexes 221 to 227, immediately ahead of the post rules starting at 228. The generic page rule at 227 is (.?.+?)(?:/([0-9]+))?/?$, and it matches every post URL on the site. So on every post view core asks the database whether a page exists at that path, gets nothing, and falls through to the post rule at 240.

That question is one query: SELECT ID, post_name, post_parent, post_type FROM wp_posts WHERE post_name IN (...) AND post_type IN ('page','attachment'). The post_name column carries its own index in core’s schema, KEY post_name (post_name(191)), so it is an index lookup on a hundred rows and an index lookup on a hundred thousand. The result is cached under get_page_by_path:$hash in the post-queries group, salted with wp_cache_get_last_changed( 'posts' ).

That salt is where site size shows up. Without a persistent object cache the entry lives one request, so a verbose structure costs one extra indexed SELECT per post view, forever. With a persistent cache it survives across requests, but saving any post moves last_changed and invalidates the whole group. On a hundred page brochure site that happens rarely and the cache does its job; on a newsroom with ten thousand posts and a publish every few minutes, the salt moves faster than the cache fills. Either way it is one indexed query, not the reason anything is slow. The reordering is what actually bites, and it bites through collisions.

Collisions, in the order core resolves them

Rewrite rules are an ordered array and the first match wins. That single fact explains every collision below. On this blog the order runs: taxonomy rules near the top (the category rule category/(.+?)/?$ at index 43), then robots, feeds, search and author, then date archives (the bare year rule ([0-9]{4})/?$ at 214), then pages at 227, then posts at 240.

So a post whose slug is 2024 is unreachable on /%postname%/. Its URL is /2024/, the year archive rule matches at 214, and the post rule at 240 never gets a look in. The same holds for a page at /2024/, and for any top level slug of four digits. The editor lets you save it and shows you a permalink that goes somewhere else.

A category base collides in the other direction. Set the base to news while a page tree exists under /news/, and the category rules, sitting above the page rules, win: /news/press/ resolves to a category archive for a term slugged press, and your child page at that path is gone. Setting the category and tag bases to the same string, or to a segment core owns like page, feed or author, fails the same way with less warning.

Attachments are the one place core defends itself, and the defence is visible in get_attachment_link():

if ( is_numeric( $post->post_name ) || str_contains( get_option( 'permalink_structure' ), '%category%' ) ) {
	$name = 'attachment/' . $post->post_name; // <permalink>/<int>/ is paged so we use the explicit attachment marker.
}

The comment is the whole story. The generic post rule ends in (?:/([0-9]+))?/?$, which sets page= for multipage posts, so an attachment with a numeric slug hanging off a post would read as page 7 of that post. Core therefore inserts an explicit attachment/ segment when the slug is numeric, and when the structure contains %category%, because a lazy (.+?) next to a bare slug is not safely decidable.

Numeric attachment slugs are commoner than they sound, because slugs come from filenames and cameras produce filenames like 20240418.jpg. They also multiply: every pass of batch resize and format conversion writes new attachment records, each with its own slug and its own attachment page. Before changing a structure, the media library manager lists attachments with their usage, duplicates and orphans, which is the set you want to audit for slug collisions.

What changing it does to the old URLs

Core has two mechanisms for old URLs, and most advice treats them as one thing that usually works. redirect_canonical() handles the query string forms. On a 404 it takes max( get_query_var( 'p' ), get_query_var( 'page_id' ), get_query_var( 'attachment_id' ) ), and if that resolves to a public post it 301s to the current permalink. So /?p=123 keeps working forever, through any number of structure changes. It also normalises the trailing slash, so changing only that needs no redirects.

redirect_guess_404_permalink() is the second, and the one people over trust. Its first real line is if ( get_query_var( 'name' ) ). Everything below it, the post_name LIKE '...%' lookup and the refinement by year, month and day, runs only when a rule already set the name query variable. It works when the old URL still matches a post rule and only the surrounding path changed. It does nothing when the old URL matches no rule at all, because then nothing set name and the function returns false before it reaches the database.

Flow diagram of one request for an old dated URL after the permalink structure changed to %postname%, showing the rule match failing, get_page_by_path returning null, redirect_canonical finding no post id and redirect_guess_404_permalink returning false because no rule set the name query variable, ending in a 404.

That is the exact failure at the top of this article. Dropping date segments produces old URLs of a shape no rule under the new structure can match, so the guess never runs. The simulator’s change mode is built around that gap. Give it both structures and it lists only the URLs that actually move, then emits Apache rules for them: a tail carrying capture so feeds and pagination follow the same mapping, a negative lookahead so a day archive’s page 2 is not misread as a post, and a loop guard when the new structure only adds a static prefix or suffix. When the two differ only by the trailing slash it suppresses the rule pair and says why, because such a rule would 301 a URL to itself.

The flush

Rules are not computed per request. They live in a single autoloaded option called rewrite_rules, and WP_Rewrite::wp_rewrite_rules() is two lines: read the option, rebuild only if it is empty. The rebuild path refuses to save while wp_loaded has not fired, because plugins registering rules later would be missing from what it wrote. It hooks flush_rules onto wp_loaded and saves there.

flush_rewrite_rules( $hard = true ) is the public door. A soft flush rewrites the option only. A hard flush also rewrites the WordPress block in your .htaccess, or web.config on IIS, which is what puts the front controller rules back. If you have never read that block, the htaccess explainer walks through it line by line, including why it is a single catch all and not one line per permalink.

Here is the failure that makes a site look completely broken. Saving Settings, Permalinks performs a hard flush, so changing the structure through the admin is safe. Changing it with wp option update permalink_structure, or with update_option() in a migration script, or by restoring a database dump from a site with a different structure, does not. The option still holds rules generated from the old structure while get_permalink() already reads the new one, so every link on the site points at a URL no rule matches. Nothing is corrupt, the content is fine, and every page 404s. Loading the permalinks screen and pressing Save fixes it, which is why that became the folk remedy for everything: the Save handler calls set_permalink_structure() and flushes. The mirror mistake is flushing on init, which writes an option and a file on every request; it belongs in an activation hook.

Where the simulator stops

The tool models one site with default rewrite settings. It does not know about custom post types, custom taxonomies, anything registered through add_rewrite_rule(), a translation plugin that prefixes paths, or multisite subdirectory installs. All of those add rules, and rules added through extra_rules_top land above everything core generates, so a plugin can win a collision the simulator would not predict. Treat the output as the core baseline and check the real array with wp rewrite list.

Three smaller limits are deliberate. The slug preview approximates sanitize_title() and is not byte for byte identical to core’s remove_accents() table. The search sample is one word on purpose, because encoding a multi word query is a claim the tool cannot make correctly. And plain permalinks need real ids, so term 7, page 2 and author 1 stand in, labelled as placeholders.

The generated rules are Apache only. Where a structure contains %category%, the (.+?) capture can be genuinely ambiguous against a tail, so the rules are ordered most specific first and the header says the first match wins. Output is built with createElement and textContent, never innerHTML, so nothing you type can become markup.

Deciding before you save

On a new site the honest summary is that /%postname%/ is fine, and its verbose page rules cost is one indexed query per post view. If that bothers you, /%year%/%postname%/ or /%post_id%/%postname%/ removes it, at the price of a longer URL and, in the second case, date archives under /date/. What is not fine is a structure with no unique component, or a category base sitting on a real page tree, because those fail silently and only where nobody clicks.

On a site with history, the change is not the work. The change takes a second. The work is knowing which URLs move, which of those core rescues, and which need a rule you write, and that is a list you want before the save rather than reconstructed from a crawl report a fortnight later. Permalinks belong to the same family as mail authentication: a single string, edited once, failing quietly somewhere you never look. If your contact form mail is in that category too, the SPF, DKIM and DMARC builder covers it the same way.

Run the old structure and the new one through the simulator, take the mapping, and check the four things core will not do for you: an old URL shape that matches no new rule, a category or tag base that shadows a path, a slug that collides with a date fragment, and an attachment page whose slug is numeric. Then save, confirm the rules were flushed, and spot check one URL from each of the thirteen shapes, not just the post you happened to be looking at.

Permalink Structure Simulator: What That One Field Decides

Table of Contents

Learn it by building something

Every week one thing you can make the same afternoon, from dynamic templates to 3D type. Written down step by step.

One mail a week, and then it ends.
Unsubscribe in one click.

Design Fundamentals

Building a Colour Palette From One Decision

Five swatches picked separately will fight each other. One hue plus arithmetic will not. A color palette generator that runs entirely in your browser, and the HSL and contrast maths that sit behind it.

SEO & Structured Data

WordPress OG Image: The Shop Window You Never See

WordPress core never writes an og:image tag, so a wrong or missing link preview is always somebody else's output. Here is the fallback chain a plugin walks, the registered size trap that makes a valid tag point at a missing file, and two curl commands that settle it.

Troubleshooting

HTTP Error When Uploading Images to WordPress: The Real Causes

The red bar reading HTTP error is not a verdict on your image. It is the uploader reporting that the POST to async-upload.php came back unusable, and six unrelated failures produce exactly that. Here is how to tell them apart in about ten seconds.

Speed & Performance

WordPress Lazy Loading Images: What Core Does and When It Hurts

WordPress has added loading="lazy" itself since 5.5, and it skips the first three media elements on purpose, because the top one is usually the largest contentful paint element. Here is the real mechanism: the functions, the threshold of 3, fetchpriority, and the places where the logic never runs.

Photo Editing

Spend your JPEG bits where the eye looks, not evenly across the frame

An encoder gives the hedge behind your subject as many bits as the subject. A saliency model knows better, but a browser will not let you set quality per region, so the encoder is not steered: its input is prepared instead. On the sample that was 41 per cent off the file at an unchanged quality setting.

Photo Editing

Content Aware Resize: Changing a Photo’s Shape Without Squashing Anyone

One photograph has to be a wide header, a square thumbnail and a tall story card. Cropping loses the edges, stretching lies about proportions, letterboxing buys space it never uses. Seam carving spends the quiet parts of the frame instead: what a seam is, why the search needs a table, and exactly where the method falls apart.

Download the free WunderPaint Plugin for WordPress

The WunderPaint workspace with the layers panel, adjustment sliders, text style presets and the asset library along the bottom

The Image Editor & Design Studio

Everything described here can be done in the browser, on your own site. The live demo runs the full editor with nothing to install.

Free

Chaos Art

Autonomous painters make one-of-a-kind abstract art in 3D space - gestures, art movements, painterly media, and embeds that paint a new original for every visitor.

Pro

Particle Strokes

Paint with swarms of light: twenty-two movements, a stamp you draw yourself, and curves that give a stroke a shape - the swarm keeps painting for a few seconds after you let go.

Pro

City Diorama

Any place on earth as a miniature you could hold: real streets, water and building footprints raised into a 3D diorama - or wrapped around a sphere as your own tiny planet.

Free

Papercut Art

Layered paper pictures with real depth - a photo sliced along its actual depth into up to twenty layers, parametric landscapes, animals and clouds you can shape, letters with real counters, and a look that runs on three dials.

Pro

3D Earth Studio

A hyperrealistic globe - day and night with city lights, live clouds, atmosphere halo, country borders and highlights, click-to-place markers with flight-route arcs, satellite orbits, seamless rotation video and a live website embed.

Free

Mystic Studio

Turn a birth date into wall art - a real natal chart with houses and aspects, the moon of that night, zodiac and Chinese zodiac posters, numerology cards and a synastry wheel for two, in eight artful themes.

Free

Marble Bath

Marble paper on a virtual water bath - drop, rake and comb real Ebru patterns with gestures, flowers and classic recipes, razor-sharp at any size and re-editable as a layer.

Free

Day Ring

Turn a day into a beautiful circular schedule - colour-coded time blocks as arcs around a 24-hour clock, with concentric rings for overlaps, emoji, templates and a legend.

Free

Code Shot

Turn code into a gorgeous, share-ready image - syntax highlighting, editor themes, window frames and diff highlighting - then drop it into your design as a re-editable layer.

Pro

3D Solar System Studio

Build a date-accurate 3D solar system - real planet positions for any date, photoreal textures and one slider from artistic to true scale - then drop it into your design as an editable layer.

Pro

3D Molecule Studio

Build a real 3D molecule - from a curated library, the periodic table or a SMILES string - then style it, measure it and drop it into your design as an editable layer.

Pro

3D Textile Studio

Drop your design onto cloth that behaves like the material you pick: silk falls soft, felt holds its shape, flag fabric snaps in the wind. Hang it, blow it and drape it, then lay the finished drape back into your document as a picture.

Pro

3D Particle Studio

Point the engine at any layer and it becomes a cloud of particles that keeps its colours, flowing through a sphere, a galaxy or your own outline. Keep the frame you like as a still, or embed the running engine so it keeps moving on your page.

Free

Origami

Put your own picture on the paper and watch that very sheet fold itself into a crane or a box. Every step is a station you can stop at and turn around in 3D, which is exactly where printed diagrams leave you alone.

Pro

3D Flip Studio

A hardcover you can leaf through, a limp magazine, a strewn pile of sheets, a sticker peeling off its backing. The curl is real geometry, so the print never slides across the paper.

Free

Handwriting Fonts

Draw the alphabet here or fill in a printed sheet and photograph it. What comes out is a genuine font family, installed into your site and available in every picker.

Pro

Step Guides

Turn any picture into an instruction. Every mark is pinned to a place in the image, so arrows still point at the right thing after the callout has been dragged somewhere else.

Free

Text Art

One studio, sixteen art types: ASCII and emoji art, brick, dice, cube, sticky-note, LED, ceramic and keycap mosaics, word portraits, text flows, silhouettes, element tiles and more.