WordPress: How to Make Custom Post Type Rewriting and Pretty URLs Work

Anybody who has created a custom post type in WordPress has probably noticed the 'rewrite' option, allowing you to define your own slug for this post type. You probably, as I did, relished at the idea of defining a nice and pretty URL structure for your WordPress site, separating content by URLs (e.g. separating portfolio and blog posts). So you create a page template where you fetch the custom post type posts, and then you discover it doesn’t work with pretty URLs in WordPress Settings turned on. Now what?

Many tutorials out there are outdated and show the wrong way to handle slugs and custom post type. I did it the wrong way a long time myself, until I sat down and researched it properly.

An explanation is best when using an example. Say we register a custom post type, my_portfolio, for portfolio posts, and set 'rewrite' => array('slug' => 'portfolio'). Now, we’d expect a page where all portfolio posts are displayed at example.com/portfolio, and when clicking on a single portfolio post we get the URL example.com/portfolio/this-is-a-portfolio-post. Single portfolio posts work beautifully right away. It’s the page displaying all portfolio posts that is the problem. Let’s take a look at what happens.

Non-working Option 1

The old way was to create a page template that does a custom query (new WP_Query etc.) on portfolio posts, and assign it to a page. You name the page Portfolio, and either WordPress or you assign the slug portfolio to the page. It doesn’t work. No matter what you do, WordPress will not use your page template.

Non-working Option 2

Since WordPress refuses to use your page template, you might try the default WordPress page templating: page-portfolio.php. According to WordPress’ hierarchy WordPress should use this template when displaying a page with the slug portfolio. You remove the assigned page template from your portfolio post and keeps the portfolio slug hoping WordPress will use your page-portfolio.php file. WordPress continues refusing to use your template.

Inconsistency Option

At this point you might try assigning a different slug to the page, e.g. work. WordPress will finally use your page template (remember to rename it to page-work.php or make it a assignable page template again). But keep in mind that single portfolio posts get example.com/portfolio/this-is-a-portfolio-post, and your portfolio overview will get example.com/work/. You could also do the other way around, by changing the rewrite on your custom post type register function to something like portfolio/work, which affects all single portfolio posts URLs, but not the page. If this inconsistency is okay for you, great. If not, read on.

The Solution

Let’s forget pages and page templates and look at archives templates. According to WordPress templating hierarchy you can create an archive specific to a post type (this is pretty new). Keep in mind that the registering of your custom post type needs to have the option has_archive => true.

Copy an existing template that has a normal loop, e.g. index.php, and rename the copy to archive-my_portfolio.php. If you navigate to example.com/portfolio/, WordPress will finally use your template! No need to make a custom query – you just use the standard loop code.

There are two downsides to this, but fortunately both are fixable: 1) The archive template will use the Reading setting “Blog pages show at most X posts”. Say you want to display 5 per page for your normal posts, but for portfolio posts you want to display 12 per page. And, 2) You can’t easily add a menu item to this archive, the WordPress Menu UI simply don’t have an option for this archive. You could hard-code the full URL and add it to the menu, but that’s generally not a good solution – say you maintain separate WordPress instances, one local, one test server and one production server with each their URLs.

Fix 1) Setting a Custom Number of Posts Per Page by Post Type

In your theme functions.php, add:

$option_posts_per_page = get_option('posts_per_page');
add_action('init', 'mytheme_modify_posts_per_page', 0);
function mytheme_modify_posts_per_page() {
    add_filter('option_posts_per_page', 'mytheme_option_posts_per_page');
}
function mytheme_option_posts_per_page($value) {
    global $option_posts_per_page;
    if (is_post_type_archive('my_portfolio')) {
        return 12;
    } else {
        return $option_posts_per_page;
    }
}

What this code does is first storing the Reading setting globally in $option_posts_per_page – this is the value we want for normal posts. At the init hook, we run a filter on this setting. In the filter function we simply check if we’re at my_portfolio custom post type archive. If we are, we return the number of posts per page we want for portfolio archives – in this case 12 (or any number you want). Otherwise, that’d be for normal posts or any other custom posts archive, we return the saved value – the Reading setting.

Fix 2) Add the Custom Post Type Archive to Menus Without Hard-coding the Full URL

No code required for this one. Create (unless you still have the page) a page with the slug portfolio. I know, it was exactly what was causing us problems earlier. Just preview the page and see that WordPress will not display the page or any page templates you have assigned, instead it will display the archive we created. So this page works as a placeholder and we simply add the page to our menus. No hard-coding of URLs necessary.