WordPress logo repeated six times from top to bottom, becoming progressively fainter

I have been using WordPress since 2004. Back then, I was drawn to it by its power combined with its usability. WordPress has always had immense flexibility, as well as good usability. This set it head and shoulders above other blogging platforms of the era.

I have seen WordPress evolve from a plucky blogging tool, to a fully-fledged content management system, to today’s juggernaut that is said to power 43% of all websites.

No tool lasts for 20 years without inheriting an archaeology of quirks. The world, the web and WordPress have all changed over time. The things I want from having a web presence have changed too. So the way I have had to work with WordPress has changed.

For a long time, I have taken advantage of WordPress’s flexibility to go beyond the standard “post” and “page” content types. 20 years ago, I used a hacky technique of creating a special category, then using template tags and CSS to create microblog-style “asides” — tweet-length posts that many bloggers used before Twitter was invented.

There are now more sophisticated ways of working with content types in WordPress. The trouble is, with more than 20 years of history, there are multiple ways that are similar but different. A lot hinges on understanding exactly what is meant by “post”, and the subtle difference between “types” and “formats”. This all requires a bit of picking apart unless you’re paying careful attention.

I don’t specialise in web development. So when I get my hands dirty with PHP code as required when customising WordPress, things tend not to work first time.

My more recent experiments with creating custom content types in WordPress have led me down a rocky path of exploration. I have settled on an approach now, but I faced challenges along the way.

The main challenge is that researching the best ways to work with WordPress has always been tricky. That landscape is becoming ever worse, given how Google results on the topic are polluted with low-quality content, which often pushes unnecessary paid-for plugins.

More recently, I have found success in using generative AI (in my case, Gemini) to suggest what code can help me solve my problem. This has been a huge time saver in comparison to rummaging through WordPress.org’s own developer resources, and whatever Google manages to give me — often enshittified WordPress advice sites mixed in with 10-year-old Stack Overflow posts and 15-year-old forum threads.

All along the way, I have not seen a good summary of the different ways to apply custom content types in WordPress, and the pros and cons of each. So here is an overview of the common approaches I’m aware of, and my take on their suitability. Then, I will cover in detail the approach I have taken to work with custom content types on my own website.

Post formats

Many WordPress themes support post formats, which are a way of giving different types of posts different presentation styles. These post formats are not enabled by default in WordPress. You need to modify your theme’s functions.php file if you want to use them.

On the surface, post formats seem like a really powerful option. But the limitation is that you cannot create your own custom post format. Essentially, it just expands the list of standard post types to add 9 new ones.

The feature was introduced in 2011, when Tumblr was at its peak, and these post types mimicked the Tumblr style, where links, photos, quotes were all formatted and displayed differently.

But WordPress post formats are not a way to customise the structure of a post to your own needs. The semantic power is therefore weak. For example, the guidance for the link post format suggests:

The first <img> tag in the post could be considered the image.

But it is not automatically a way to use things like microformats, schema.org markup or Open Graph metadata.

Moreover, while there are good options for new post types here, if you have a need for a post format that is not in this list, you will need to explore a different option.

In the words of Otto: “Post Formats is just a taxonomy” — a taxonomy of 9 new labels for posts, and nothing more.

The theory behind this was that content should be portable across themes. If a user switches to another theme, but their new theme doesn’t support the old theme’s post formats, they will have a bad experience.

This logic didn’t really hold up. It ran counter to the fact that the flexibility offered by WordPress has traditionally been its most powerful and appealing feature. Moreover, the fact that post formats had to be enabled in each individual theme’s code meant that the risk of content not being portable or behaving differently across themes remained very real.

Really, this feels no different than the hacky “aside” implementation going back to the earliest days of WordPress. You don’t even need a separate taxonomy to do this — you could just work with any category in this way.

My theme does enable them, for reasons that will become clear. Effectively, I tag each of my posts with the most relevant post format. But not all of my custom post types map to these naturally.

Over time, post formats have reduced in prominence as a feature within WordPress. Automattic bought Tumblr in 2019.

IndieWeb Post Kinds

The IndieWeb is a movement to promote independent personal website publishing. Their community has developed a set of conventions and informal standards. One of these is post kinds, an IndieWeb take on the Tumblr-style post types.

The Post Kinds plugin enables these on a WordPress site. It works in a similar way to WordPress post formats, by registering a new taxonomy. But unlike post formats, it is more than “just a taxonomy”.

It also adds metadata fields to the post kinds to create their structures. One really nice feature is that it adds relevant microformats markup to your posts, as is the IndieWeb way.

However, it also adds other things such as icons that don’t necessarily visually match my theme. The additional work to modify or remove these didn’t feel worth it. Ultimately I decided I wanted more control over how my custom content types are marked up and displayed.

It also suffers from the same limitation that turned me away from WordPress post formats — an inability to truly create your own custom content types.

But one great advantage of the plugin is that it enables you to mark posts as replies to posts from other blogs. From that perspective, it’s an interesting alternative to using pingbacks or implementing ActivityPub.

Jetpack custom post types

Jetpack offers a feature that sounds promising at first, but turns out to be even more limited than any of the above.

Custom post types enables you to add two more types — portfolios and testimonials. The premise of this is similar to custom post formats:

Since this is part of Jetpack, you can even switch themes without losing these custom post types!

If you want a custom post type that is not a portfolio or testimonial, this won’t meet your needs.

Something to do with Gutenberg, probably

Gutenberg, the highly visual editor that WordPress has been pushing for the past few years, offers lots of ways to create custom layouts.

I don’t normally use Gutenberg, but I did explore how I could use its features to achieve some of what I was looking for.

I felt like I was able to find a way to control the visual layout — and, to a degree, the structure — of individual content types. But I wasn’t sure I would be able to control more architectural aspects like how the archives behaved.

Moreover, I just don’t like Gutenberg. I have dabbled with it a few times since its introduction, but I have always ended up returning to the classic editor.

While Gutenberg does have some powerful features I have always ended up baulking at its bloated nature. I think of my blog as a text-based medium, and Gutenberg adds a whole layer of visual-centred cruft that I find unnecessary.

I was happier finding a way to make things work using WordPress’s classic text-based editor.

Creating your own custom post type plugin

The approach I have taken is the one that might seem a bit scary at first, but turns out to be straightforward once you know how — creating your own plugin.

This is as simple as creating a php file and uploading it to your plugins folder. Historically, I have added the required code to my functions.php file. But as my needs have developed, it has become easier to manage the post types as their own plugin file.

The WordPress function register_post_type enables you to create a new post type and define its characteristics.

Following the WordPress documentation for this is reasonably straightforward — but there are a few gotchas that caught me out.

Getting registered post types to appear in archives

I said above that WordPress by default only has two content types — post (for blog-style chronologically-ordered content) and page (for CMS-like pages that can be arranged in a hierarchy). It’s easy to think this because of how prominent they are. But there are actually lots of post types to power things like media attachments, navigation menu items and Gutenberg blocks.

For this reason, custom post types do not appear in your archives automatically. Extra code is required to enable this, and it took me a fair bit of troubleshooting to arrive at the exact solution that met my needs.

In my case, I want some — but not all — of my custom post types to behave just like blog posts, and appear in the same places as regular blog posts.

One example is Linknotes, which I occasionally use for short microblog-style posts about interesting links I want to share here. I want these to be structured differently and be visually distinct from regular blog posts. But I still want them to appear in the same places they would if they were a normal blog post:

  • Homepage
  • Archives for categories, tags and years
  • RSS feeds

I also want each custom post type to have an archive of its own.

Specific code is needed to achieve each of these.

I also found that in some cases posts would appear in the wrong place in the admin dashboard unless I got the code right.

Adding custom post types to the main query

To get a custom post type to appear on the homepage and archives, you need to include them in the main query.

/**
 * Add custom post types to the main WordPress query.
 */
function your_plugin_add_custom_post_types( $query ) {
    // This line is needed to keep things right in the admin interface - it only modifies the query if it is the main query and not in the admin dashboard.
    if ( ! is_admin() && $query->is_main_query() ) {

        // Target the homepage, category, tag and archive - but exclude it from custom post type archives.
        if ( ( is_home() || is_category() || is_tag() || is_archive() ) && ! is_post_type_archive() ) {

            // List the custom post types to include the in the query.
            $query->set( 'post_type', array( 'post', 'your_custom_post_type_x', 'your_custom_post_type_y' ) );
        }
    }
}
add_action( 'pre_get_posts', 'your_plugin_add_custom_post_types' );

if ( ! is_admin() && $query->is_main_query() ) is necessary to get things to work nicely in the admin interface.

if ( ( is_home() || is_category() || is_tag() || is_archive() ) is the list of archive types I wanted my custom post types to appear in.

I found I had to list home, category, tag and (date-based) archive separately to get things working the way I wanted.

Then I needed to exclude is_post_type_archive, because otherwise all of my registered custom post types would appear in each post type archive — when of course I only want the custom post type archive to contain posts of the relevant type.

When listing the post types to be included, you need to remember to list 'post' so that your regular blog posts stay in the main query as well.

Adding custom post types to RSS feeds

A separate piece of code adds your new custom post types to RSS feeds:

function myfeed_request($qv) {
    if (isset($qv['feed']) && !isset($qv['post_type']))
        $qv['post_type'] = array( 'post', 'your_custom_post_type_x', 'your_custom_post_type_y' );
    return $qv;
}
add_filter('request', 'myfeed_request');

Again, remembering to list 'post' so that your regular blog posts stay in the RSS feed as well.

Adding structure to custom post types

I use custom fields to define the structure of my custom post types.

For example, linknotes store the link’s URL, title, source and author as separate fields. This allows me to add some extra semantic markup to the HTML output, as well as additional control over how the content is displayed and manipulated using CSS.

From blog posts to articles — the behaviour of the standard post

A major gotcha to all this is the way it affects the original (blog) post type. By adding all those other custom post types to the main query, you effectively destroy the regular way of viewing a list of regular blog posts.

Fixing this requires a few extra steps.

My theme is a modified version of Autonomie. This adds support for the “standard” post format, which effectively becomes the way the theme distinguishes regular blog posts from the custom post types I have created:

function your_theme_get_post_format() {
    return get_post_format() ? : 'standard';
}

On the front-end I call this “Article”, so I have a bit of extra code to call it Article. The way I did this builds on some code from the Autonomie theme, which adds a label for the “Attachment” and “Page” post types. I expanded this list to include a label for each of my custom content types.

Then it looks for any post formats. If a post is not any of those for some reason, it falls back to being called “Article” (this replaces the fallback used in the original Autonomie theme: “Text”).

function autonomie_get_post_format_string() {
    if ( 'attachment' === get_post_type() ) {
        return __( 'Attachment', 'autonomie' );
    } elseif ( 'page' === get_post_type() ) {
        return __( 'Page', 'autonomie' );
    } elseif ( 'your_custom_post_type_x' === get_post_type() ) {
        return __( 'Custom post type x', 'autonomie' );
    } elseif ( 'your_custom_post_type_y' === get_post_type() ) {
        return __( 'Custom post type y', 'autonomie' );
    } elseif ( get_post_format() ) {
        return get_post_format();
    } else {
        return __( 'Article', 'autonomie' );
    }
}

Then I needed to generate links to the archive pages for each of the custom post types. Again, I have built on the existing code in the Autonomie theme:

function autonomie_get_post_format_link( $post_format ) {
    if ( in_array( get_post_type(), array( 'page', 'attachment' ), true ) ) {
        return get_permalink();
    }

    if ( 'your_custom_post_type_x' == get_post_type() ) {
        return get_post_type_archive_link ( 'your_custom_post_type_x' );
    }

    if ( 'your_custom_post_type_y' == get_post_type() ) {
        return get_post_type_archive_link ( 'your_custom_post_type_y' );
    }

    if ( 'standard' !== $post_format ) {
        return get_post_format_link( $post_format );
    }

    global $wp_rewrite;

    $termlink = $wp_rewrite->get_extra_permastruct( 'post_format' );

    if ( empty( $termlink ) ) {
        $termlink = '?post_format=standard';
        $termlink = home_url( $termlink );
    } else {
        $termlink = str_replace( '%post_format%', 'standard', $termlink );
        $termlink = home_url( user_trailingslashit( $termlink, 'category' ) );
    }

    return $termlink;
}

The Autonomie theme already had the link for the Standard post format. By default, Standard uses a query parameter, rather than “pretty permalinks”. So the section of code at the bottom is required to rewrite these URLs.

The upshot of all this is that I have been able to use WordPress’s Standard blog post type and format, but give it the new label of “Article”, and create its own archive with a pretty permalink — despite the fact that I have mixed other content types into the main query.

A major restriction — You cannot add custom post types to Jetpack emails

There is no way to get Jetpack to send out custom post types to email subscribers. This feels like a major restriction, although perhaps it is wise not to send out an email for every linknote I create. It does however mean that email subscribers miss out on some of my writing.

I strongly recommend the RSS feed as the best way to follow my website.

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.

Webmentions (learn more about webmentions):