Everything you need to know on how URLs work in WordPress

One of the reasons I believe WordPress won the CMS wars early on was how easy and quickly it was to implement “pretty” permalinks. Although a straight WordPress installation doesn’t have keyword rich URLs, it’s success was how it was able to implement them easily. Nowadays hosts have these URL’s switched on by default, but even in the early 2000’s, the worst you had to do was change a .htaccess file. Compared with the complexity of Joomla and Drupal, WordPress ease of setup made it a market leader and the choice for SEO’s, who drove a lot of the early adoption, as keyword rich URLs would often be enough to rank highly in Google for a number of terms back in the day.

It isn’t without some quirks, so in this article I’ll talk about how URLs work, why some URL structures are trickier to implement than others, and other potential stumbling blocks.

First a primer – as I’m going to talk a lot about variables within URLs. Variables in URLs are generally anything after a question mark, so if we take a string:-

In this example, there are two variables within the URL. variable1 has a value of “3” and variable2 has a value of “4”. Get it? Great, onto the next part.

The default WordPress URL

By default, this is how URLs look for individual posts:-

https://dwinrhys.com/?p=123

WordPress uses variables within the URL to tell the database where to look. So, for example here, the highlighted p within the URL tells WordPress to find a post with the ID of 123.

Now, even though all posts live within the same database, so each have their own unique ID, the variables for posts and custom post types are different. For pages, the URLs have the following structure.

https://dwinrhys.com/?page_id=551

This will pull the page ID of 551. I’m not 100% sure of the reasoning why p and page_id are different, but my guess would be because it would instruct WordPress for what template to use.

Custom post types work slightly differently, in that they use the custom post type slug and the name within the variables within the URL.

This is a custom post type of “location” looking for a post with the name London. Again, not sure why it works differently here, but it is what it is. Hopefully somebody can answer in the comments!

Similarly, for taxonomies, category uses a number based system with “cat” and a category ID, which is a number.

https://dwinrhys.com/?cat=456

Tag however differentiates by using the tag slug, in this case this will get all posts with the tag “news”.

https://dwinrhys.com/?tag=news

Finally custom taxonomies use the custom taxonomy name, and the slug. So this will grab all posts with the custom taxonomy “country” of “England”.

https://dwinrhys.com/?country=england

Despite this not being the URL structure on this site, all the above URLs still work. So despite using the raw permalink here – this link of https://dwinrhys.com/?p=5825 will go through to my WordPress Doom post.

There are loads of variables. Pretty much every variable defined in the WP_Query Construct could be used within the URL. For example, this link shows all the posts written in 2024. The only one that’s really common one the search parameter “s”, as even with permalinks enabled this still is used, primarily because indexing a bunch of search results is bad for SEO, and having them crawlable is a bad time.

How permalinks work in WordPress

Of course, most people new to WordPress probably don’t even realise that permalinks work this way, as by default a lot of installations have “pretty” permalinks switched on. This is switched on in the Settings > Permalinks.

Permalinks being switched on and “pretty” aids the usability of the site, and also allows better storing of hierarchical custom post types. The way custom post types work is that the URL is split into two locations – the base and the slug.

The base tends to be the first item (or number of items). This will aid WordPress in identifying what type of post it is finding. The slug will provide the exact post type it is. You can only have one slug for each post type – so if you have two blog posts entitled “update”, with a non date based archive, the slug for the later blog will be prefixed with a -2.

WordPress out of the box only allow you to set the permalinks for blog posts, categories & tags, so for in our example, we’ll set it so that the base for blog posts will be blog/, followed by the post name.

Now here’s the trick, WordPress reads the URLs back to front. So it will determine the slug first, and then use the base to narrow down the search. Let’s take this URL:-

https://dwinrhys.com/blog/hello-world/

You’d assume it would search for a blog post with a name of “Hello World”. Which it does, as you can see this using a tool like Query Monitor.

However, notice that there is are two rules underneath “All Matching Rules”, the first one is that it checks if there is a page with a slug entitled “hello-world” under another page entitled “blog”. WordPress assumes if there isn’t a base, the post type we are looking for is a page. This is what catches out a lot of people.

Hierarchical posts

For Hierarchical Posts such as pages, then outside the base the entire slug is treated as the page name and it’s searched for. So for a page entitled “freelance availability widget” with a parent of “my plugins”, the matched query for the pagename variable will be “my-plugins/freelance-availability-widget”.

However, the post name is just the last part of the domain – freelance-availability-widget. WordPress does take the slash, determine my-plugins as the parent, and finds a post with a post name of freelance-availability-widget and a post parent equal to my-plugins. This will mean you can have a post with the post-name of freelance-availability-widget totally fine.

Permalinks with custom post types and taxonomies

There are a number of settings that affect the permalinks when you register a custom post type using register_post_type. They are:-

  • has_archive – Determines if the post type has an archive. Can be true, false or a string. The slug will be the string if it’s a string, or the post type name if “true”.
  • rewrite – this can be an array or false. If you have set it to be an array it’ll need to have the following elements:-
    • slug – The slug for the base of the URL. A common reason to use this is that traditionally WordPress post types use underscores for spaces, but hyphens are tended to be used in URLs. So if you have a post type of "team_members", you’d want to set this to be "team-members".
    • with_front – Should you have set up posts to have a base (eg “blog“), custom post types will use the base (eg "blog/team-members/barry"). Setting this to false will remove it (so the URL will be “/team-members/barry“)
    • pages – whether the URLs should allow pagination. So "/team-members/page/2" would show the second page of team members. This needs has_archive to be true.

Taxonomies work similarly, in that they have a rewrite function, but with the hierarchical setting within register_taxonomy. This will allow you to have hierarchical rewrites. So for example, with the flag set to “true”, London within an location taxonomy will have a URL of /location/united-kingdom/england/. A hierarchical set to “false” will mean a London term will have the taxonomy URL of /location/england/. This will be the URL even if the England taxonomy item has the United Kingdom taxonomy as a parent.

Sure enough, having a base will notify WordPress that it is a custom post type, or a custom taxonomy.

A final thing to check

Once you have your custom post types in, be sure to flush the permalinks! Permalinks aren’t registered when the code is written, and need to be written to the database. if your permalinks aren’t working, then go to Settings > Permalinks and save. This will clear the permalinks. This is a common problem that occurs, so is a good first check!

Featured Image Photo by JJ Ying on Unsplash

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

To respond on your own website, enter the URL of your response which should contain a link to this post's permalink URL. Your response will then appear (possibly after moderation) on this page. Want to update or remove your response? Update or delete your post and re-enter your post's URL again. (Find out more about Webmentions.)