How to Create Custom Post Types in WordPress (2026 Guide)

Web developer creating a WordPress child theme on a laptop.

Posts and pages cover a lot, but they do not cover everything. The moment a site needs portfolio projects, team members, testimonials, properties, or courses — content that is not really a blog post and not really a page — forcing it into regular posts starts to hurt. Categories get messy, templates get conditional, and editors end up fighting the dashboard. That is exactly the problem WordPress custom post types solve: they give each kind of content its own home in wp-admin, its own archive, and its own templates.

In this guide you will learn how to create WordPress custom post types in 2026, both with code and with the free Custom Post Type UI plugin. You will also add a custom taxonomy, display your new content on the front end, and avoid the mistakes that most commonly break new post types.

What Are WordPress Custom Post Types?

WordPress ships with built-in post types: posts, pages, attachments, revisions, and navigation menu items. A custom post type is another type registered in the same system, living in the same posts table — so adding one needs no new database table or special architecture.

The difference is in how WordPress treats it. A portfolio post type can have its own admin menu, its own archive at /portfolio/, its own single-post template, and its own set of categories or tags. Editors see “Portfolios” in the sidebar instead of hunting through blog posts, and visitors get clean URLs that describe the content.

Before You Register a Post Type, Plan It

Planning saves hours of migration later, because changing a post type key after content exists is painful. Decide these four things first:

  1. The post type key. This is the internal name, such as portfolio. Keep it singular, lowercase, and under 20 characters. Do not use reserved names such as post, page, attachment, revision, or author.
  2. The labels. The plural name shown in the menu (“Portfolios”) and the singular name (“Portfolio”), plus labels for “Add New” and “Edit” screens.
  3. What it supports. Title and editor are the defaults. Add thumbnail, excerpt, revisions, or custom fields only if the content genuinely needs them — every extra box is clutter for editors.
  4. How it is organised. Does it need categories, tags, or both? A portfolio might need a hierarchical “Project Type” taxonomy; testimonials probably need neither.

Also decide where the code lives. Registering a post type in a theme means the content disappears from the admin (though it stays in the database) the moment you switch themes. A small custom plugin, or a must-use plugin, keeps the content type independent of design. If you are new to that separation, my guide to creating a WordPress child theme explains why theme files are the wrong home for site functionality. Test structural changes on a copy first — see how to set up a WordPress staging site.

Method 1: Create WordPress Custom Post Types with Code

Code gives you full control and a version-controlled definition. The core function is register_post_type(), hooked to init. Here is a working example for a portfolio post type, to put in a small custom plugin file:

add_action( 'init', 'amaardev_register_portfolio_cpt' );

function amaardev_register_portfolio_cpt() {
    $labels = array(
        'name'               => 'Portfolios',
        'singular_name'      => 'Portfolio',
        'menu_name'          => 'Portfolios',
        'add_new'            => 'Add New Portfolio',
        'add_new_item'       => 'Add New Portfolio',
        'edit_item'          => 'Edit Portfolio',
        'view_item'          => 'View Portfolio',
        'all_items'          => 'All Portfolios',
    );

    $args = array(
        'labels'             => $labels,
        'public'             => true,
        'publicly_queryable' => true,
        'show_ui'            => true,
        'show_in_menu'       => true,
        'has_archive'        => true,
        'show_in_rest'       => true,
        'supports'           => array( 'title', 'editor', 'thumbnail', 'excerpt', 'revisions' ),
        'rewrite'            => array( 'slug' => 'portfolio' ),
        'menu_position'      => 5,
        'menu_icon'          => 'dashicons-portfolio',
    );

    register_post_type( 'portfolio', $args );
}

Three arguments deserve special attention. show_in_rest => true enables the block editor and the REST API for this post type — without it, you get the classic editor experience and no REST endpoint, which breaks headless setups and many modern plugins. has_archive => true creates the archive page at /portfolio/. And rewrite sets the public URL slug, which can differ from the internal key if you ever need it to.

After activating the plugin, go to Settings > Permalinks and click Save Changes. This flushes the rewrite rules so WordPress recognises the new URLs. Skipping this step is the single most common reason a new post type returns 404 errors — if that happens to you, the WordPress 404 fix guide walks through the same flush plus the other usual causes.

Method 2: Create Custom Post Types with CPT UI

If you would rather not write PHP, the free Custom Post Type UI plugin registers post types through forms in the dashboard. The steps are:

  1. Install and activate Custom Post Type UI from Plugins > Add New.
  2. Go to CPT UI > Add/Edit Post Types.
  3. Enter the post type slug (for example, portfolio), the plural label (“Portfolios”), and the singular label (“Portfolio”).
  4. Under Additional Labels, fill in the “Add New” and “Edit” labels so the admin reads properly.
  5. In Settings, enable Show in REST API, Has Archive, and the supports your content needs (title, editor, thumbnail, excerpt).
  6. Click Add Post Type, then visit Settings > Permalinks and save to flush rewrite rules.

CPT UI also exports your definition as PHP code (Tools screen). Design in the interface, then move that code into a custom plugin if you want a leaner production site — the content itself is stored as normal WordPress posts, so moving to code with the same key loses nothing.

Adding a Custom Taxonomy

A post type defines what the content is; a taxonomy defines how it is grouped. Categories are hierarchical (a term can have children), while tags are flat. For a portfolio, a hierarchical “Project Type” taxonomy with terms like Web Design, Development, and Speed Optimisation makes more sense than reusing blog categories.

With code, register the taxonomy alongside the post type, hooked to init as well:

add_action( 'init', 'amaardev_register_portfolio_taxonomy' );

function amaardev_register_portfolio_taxonomy() {
    $labels = array(
        'name'          => 'Project Types',
        'singular_name' => 'Project Type',
    );

    $args = array(
        'labels'            => $labels,
        'public'            => true,
        'hierarchical'      => true,
        'show_in_rest'      => true,
        'show_admin_column' => true,
        'rewrite'           => array( 'slug' => 'project-type' ),
    );

    register_taxonomy( 'project_type', array( 'portfolio' ), $args );
}

In CPT UI the equivalent is CPT UI > Add/Edit Taxonomies: enter the taxonomy slug, attach it to the portfolio post type, set it to hierarchical, and enable REST support. Either route gives you a taxonomy box on the portfolio edit screen and archive pages such as /project-type/web-design/.

Displaying Your Custom Post Type

Registration alone does not put content on a page — you still need a template or a query. WordPress follows its template hierarchy automatically: single-portfolio.php controls single items and archive-portfolio.php controls the listing, falling back to single.php and archive.php when those files do not exist.

For a custom listing inside another page, use WP_Query:

$portfolio_query = new WP_Query( array(
    'post_type'      => 'portfolio',
    'posts_per_page' => 6,
    'orderby'        => 'date',
    'order'          => 'DESC',
) );

if ( $portfolio_query->have_posts() ) {
    while ( $portfolio_query->have_posts() ) {
        $portfolio_query->the_post();
        the_title( '<h2>', '</h2>' );
        the_excerpt();
    }
    wp_reset_postdata();
}

That last line matters: wp_reset_postdata() restores the global post after a custom loop, so sidebars and footers do not accidentally reuse your portfolio item.

Common Mistakes to Avoid

  • Forgetting to flush permalinks. New post types and taxonomies 404 until rewrite rules are regenerated. Save the Permalinks screen after every registration change.
  • Registering in the theme. Switch themes and the post type vanishes from the admin. Keep registration in a plugin.
  • Changing the key later. Renaming portfolio to work orphans every existing item under the old key. Pick the key carefully, then leave it alone; change only the public rewrite slug if the URL needs to change, with redirects for old URLs.
  • Leaving out show_in_rest. Without it, the block editor and REST API do not work for the post type, and several plugins will silently ignore it.
  • Using a reserved or conflicting slug. Check that your key and rewrite slug do not clash with an existing post type, page, or plugin before you publish content under them.
  • Overloading supports. Enable only what editors need — extra boxes are admin noise.

Which Method Should You Choose?

Use code when you are building a site you will maintain, when the definition belongs in version control, or when you need custom capabilities and fine-grained control. Use CPT UI when you are prototyping, when a non-developer needs to define content, or when you want the export as a starting point for code. Both produce the same result in the database, and the official register_post_type() reference documents every argument if you want to go beyond the examples here.

Need help restructuring a WordPress site around proper post types, taxonomies, and templates? Get in touch — I can map your content to the right structure first.