14+ years building on WordPress / Replies in under 5 hours
WordPress 15 min read · Updated October 2026

How to Create Custom Post Types in WordPress (With or Without Code)

Photo of Ajay Khandal
Ajay Khandal
WordPress Developer
TL;DR

You can create a custom post type in WordPress without code using the Custom Post Type UI plugin, with Pods or ACF for custom fields and a page builder for display. With code, call register_post_type() on the init hook inside a site-specific plugin, set 'show_in_rest' => true so the block editor and REST API work, and flush rewrite rules once on activation to avoid 404s. Add register_taxonomy() for grouping, custom fields for extra data, and single-{post_type}.php or WP_Query to display the entries.

You can add a custom post type to WordPress in two ways. A plugin like Custom Post Type UI does it through a settings screen with no code at all, or you can register it yourself with a few lines of PHP using register_post_type(). This guide covers both methods start to finish, then taxonomies, custom fields, templates, WP_Query and the mistakes behind most custom post type problems I get asked to fix.

What is a custom post type in WordPress?

A custom post type is a content type you register in WordPress alongside the built-in posts and pages. Portfolio items, properties, events, job listings and testimonials are typical examples. Each one gets its own admin menu, its own URLs such as /portfolio/project-name/, an optional archive page and its own templates.

Out of the box, WordPress gives you post and page for content. A blog post has a date, an author, categories and tags. A page is hierarchical and timeless. That covers a lot, but a real estate listing has bedrooms, bathrooms, square footage and a location, and a job posting has a department, a salary range and a closing date. You can force those into regular posts, but you end up fighting the data model all the time.

When should you use a custom post type?

Three questions settle it for most sites:

  1. Does this content need its own URL structure? If portfolio items should live at /portfolio/project-name/ and have their own archive, a custom post type with its own rewrite slug is the right fit.
  2. Does it carry attributes that categories and tags can’t hold, such as an address, a price, a date range or a link to another post? Then you need custom fields attached to a custom post type.
  3. Should it sit apart from blog posts in the admin? A custom post type gets its own menu entry, list table and permission settings, which helps when different people manage it.

If the content is really a blog post on a different topic, with the same fields and structure, a category is simpler. Custom post types add template files, query logic and REST settings, so I only add one when the data calls for it.

How to create a custom post type in WordPress without code

The quickest no-code route is the free Custom Post Type UI plugin, which has more than a million active installs. You fill in a slug and labels on a settings screen, save, and the new post type appears in wp-admin straight away. Pods or ACF then add custom fields, and page builders handle display.

Here’s the full no-code path:

  1. Install and activate Custom Post Type UI from Plugins > Add New, then go to CPT UI > Add/Edit Post Types.
  2. Set the post type slug (for example “book”) and the plural and singular labels. Keep the slug lowercase and within 20 characters, because WordPress rejects post type keys longer than that.
  3. Choose the basic settings: public visibility, whether it has an archive page, and which core features it supports (title, editor, featured image). Leave “Show in REST API” set to True so the block editor loads for the new type.
  4. Save. The new post type shows up as its own menu item in wp-admin immediately. If its front-end URLs return a 404, go to Settings > Permalinks and click Save Changes once.
  5. Add custom fields with Pods or ACF.
  6. Register a custom taxonomy if the post type needs categories of its own.
  7. Display the entries with a page builder widget or a field plugin’s shortcode.

Adding custom fields without code

A post type on its own only gives you a title and a body. Most real use cases need structured fields (price, SKU, event date). Two honest options:

  • Pods handles the post type, its fields and its taxonomies all in one plugin, which is the simpler setup if you’re starting from scratch and don’t already have a fields plugin installed.
  • Advanced Custom Fields (ACF) pairs with CPT UI: CPT UI defines the post type, ACF defines the fields attached to it. This is the better choice if you already use ACF elsewhere on the site, since you’re not adding a second, overlapping tool.

Either way, avoid running Pods and ACF for the same post type. Pick one field system per post type, for the same reason you wouldn’t run two SEO plugins together. For a fuller comparison including Meta Box as a third option, see ACF Pro vs Metabox vs Pods.

Adding taxonomies without code

If your post type needs categories of its own (“Cuisine” for a Recipes post type, “Genre” for Books), both CPT UI and Pods let you register a custom taxonomy the same way as the post type itself, through a settings screen. Attach the taxonomy to your custom post type and it appears as a filterable field in the editor, the same way categories work for regular posts.

Displaying your custom post type without editing theme files

Most page builders (Elementor, Divi, Bricks) can pull in custom post type content dynamically once the post type exists: point a dynamic list or grid widget at your new post type and no template file is needed. Many custom-field plugins also ship a shortcode for embedding a specific entry or a filtered list directly in the block editor. Between the two, most display needs are covered without ever opening a PHP file.

How to create a custom post type in WordPress with code (register_post_type)

To create a custom post type with code, call register_post_type() inside a function hooked to the init action, and put that code in a small site-specific plugin. Pass a post type key, labels, 'public' => true and 'show_in_rest' => true, then flush rewrite rules once so the new URLs resolve.

Why put it in a plugin? A post type registered in the theme (usually in functions.php) disappears from the admin the moment the theme changes, and the content becomes unreachable with it. The WordPress Plugin Handbook recommends a plugin for exactly this reason. If you’ve never made one, a single PHP file in wp-content/plugins/ is enough (the guide to creating a WordPress plugin walks through the file header).

<?php
/**
 * Plugin Name: My Site Content Types
 * Description: Registers the Portfolio post type.
 */

function mysite_register_portfolio_cpt() {
    $labels = array(
        'name'               => 'Portfolio Items',
        'singular_name'      => 'Portfolio Item',
        'add_new_item'       => 'Add New Portfolio Item',
        'edit_item'          => 'Edit Portfolio Item',
        'view_item'          => 'View Portfolio Item',
        'search_items'       => 'Search Portfolio Items',
        'not_found'          => 'No portfolio items found.',
        'not_found_in_trash' => 'No portfolio items found in trash.',
    );

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

    register_post_type( 'portfolio', $args );
}
add_action( 'init', 'mysite_register_portfolio_cpt' );

// Flush rewrite rules once, when the plugin is activated.
function mysite_content_types_activate() {
    mysite_register_portfolio_cpt();
    flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'mysite_content_types_activate' );

The activation function registers the post type first and then calls flush_rewrite_rules(), so /portfolio/ URLs work as soon as the plugin is switched on. The WordPress docs call flushing an expensive operation, which is why it lives on the activation hook and never on init.

Key register_post_type() arguments explained

All of these are documented in the register_post_type() reference.

public: whether the post type is meant to be used publicly, in the admin and on the front end. It defaults to false, and publicly_queryable, show_ui, show_in_nav_menus and exclude_from_search take their defaults from it. Most custom post types want true; leave it false for internal data that should never have a front-end URL.

has_archive: creates an archive page, here at /portfolio/. It defaults to false, which means only single entry URLs exist. With true, the archive uses the rewrite slug; pass a string instead if you want a different archive slug.

show_in_rest: the argument beginners miss most often. It defaults to false. Setting it to true includes the post type in the REST API at /wp-json/wp/v2/{post_type} and makes it available in the block editor. Leave it off and the edit screen falls back to the classic editor, and REST-based integrations and headless frontends can’t see the content.

supports: the core features the post type uses. Common values are title, editor, thumbnail (featured image), excerpt, author, page-attributes (for hierarchical types), revisions and custom-fields. Include custom-fields if you plan to expose post meta through the REST API.

rewrite: controls the URL structure. 'slug' => 'portfolio' sets single entry URLs to /portfolio/{post-name}/. Without it, WordPress uses the post type key as the slug.

One naming note: the post type key ('portfolio' above) must be 20 characters or fewer, using lowercase letters, numbers, dashes and underscores. On a production site, prefix it (for example acme_portfolio) so it can’t clash with a plugin that registers its own “portfolio”, and keep the readable URL through the rewrite slug.

How to add a custom taxonomy to a custom post type

A custom taxonomy groups the entries of a custom post type, the way categories and tags group blog posts. Register it with register_taxonomy() on the same init hook, pass your post type key as the second argument, and set show_in_rest to true so the taxonomy panel appears in the block editor.

function mysite_register_portfolio_taxonomy() {
    $labels = array(
        'name'          => 'Project Types',
        'singular_name' => 'Project Type',
        'search_items'  => 'Search Project Types',
        'all_items'     => 'All Project Types',
        'edit_item'     => 'Edit Project Type',
        'update_item'   => 'Update Project Type',
        'add_new_item'  => 'Add New Project Type',
        'new_item_name' => 'New Project Type Name',
        'menu_name'     => 'Project Types',
    );

    register_taxonomy( 'project_type', 'portfolio', array(
        'labels'       => $labels,
        'hierarchical' => true,    // true = category-like, false = tag-like
        'show_in_rest' => true,    // needed for the block editor panel
        'rewrite'      => array( 'slug' => 'project-type' ),
    ) );
}
add_action( 'init', 'mysite_register_portfolio_taxonomy' );

The second argument to register_taxonomy() accepts a single post type key or an array of them, so one taxonomy can serve several post types. If you register it in the same plugin as the post type, call mysite_register_portfolio_taxonomy() inside the activation function too, before the flush, so its URLs work on day one.

How to add custom fields to a custom post type

Custom fields store the extra data each entry needs, such as a price, an address or an event date. You can use WordPress core post meta, a field plugin like ACF, Meta Box or Pods, or a custom database table when the data is large and heavily relational. Most sites I build use a field plugin.

Core post meta. Adding 'custom-fields' to supports turns on the built-in meta box, and you read and write values with get_post_meta() and update_post_meta(). No plugin needed, but the only UI is a basic key-value box, so it suits simple data.

ACF, Meta Box or Pods. Each gives you a visual field group builder and richer field types (image, relationship, repeater, gallery, map). ACF is the most widely used, Meta Box has a strong developer API, and Pods includes its own post type registration. The ACF Pro vs Metabox vs Pods guide linked above compares all three in detail.

Custom database tables. For data that’s truly relational (many-to-many links, large datasets, complex queries), the postmeta table doesn’t scale well. Registering your own table and handling reads and writes yourself is covered in the guide to creating a custom database table in WordPress.

If you store fields as core post meta and want them in the REST API, register each one with register_post_meta() on init. Fields without this registration don’t appear in REST responses even when the post type itself is exposed:

function mysite_register_portfolio_meta() {
    register_post_meta( 'portfolio', 'client_name', array(
        'type'              => 'string',
        'single'            => true,
        'show_in_rest'      => true,
        'sanitize_callback' => 'sanitize_text_field',
    ) );
}
add_action( 'init', 'mysite_register_portfolio_meta' );

ACF and Meta Box have their own REST settings for the fields they manage. For exposing custom post types through custom routes, the guide to WordPress custom REST API endpoints goes further.

How to display and query custom post types

WordPress displays a custom post type with single-{post_type}.php for individual entries and archive-{post_type}.php for the archive, falling back to single.php and archive.php. For lists anywhere else, such as a homepage section, query the post type explicitly with WP_Query, because the main blog query only returns regular posts.

Template files for your custom post type

  • single-portfolio.php renders an individual portfolio item.
  • archive-portfolio.php renders the /portfolio/ archive.

If neither file exists, WordPress works down the template hierarchy to single.php, archive.php and finally index.php. In a block theme the equivalents are single-portfolio.html and archive-portfolio.html in the theme’s templates/ folder. The guide to building a custom WordPress theme from scratch explains the hierarchy and when to use each file.

Querying custom post types with WP_Query

Pass the post type key to WP_Query, and combine it with taxonomy and meta filters as needed:

$portfolio_query = new WP_Query( array(
    'post_type'      => 'portfolio',
    'posts_per_page' => 12,
    'tax_query'      => array(
        array(
            'taxonomy' => 'project_type',
            'field'    => 'slug',
            'terms'    => 'web-design',
        ),
    ),
    'meta_query'     => array(
        array(
            'key'     => '_featured_project',
            'value'   => '1',
            'compare' => '=',
        ),
    ),
) );

if ( $portfolio_query->have_posts() ) {
    while ( $portfolio_query->have_posts() ) {
        $portfolio_query->the_post();
        // template output
    }
    wp_reset_postdata();
}

wp_reset_postdata() restores the global post after your loop so the rest of the page isn’t affected. For heavier queries that mix custom fields and taxonomy terms, including the performance cost of meta_query on large datasets, see advanced WP_Query with custom fields and taxonomies.

CPT UI plugin vs code: which should you use?

Use CPT UI when a non-developer needs to manage post types, or when you’re prototyping and want to change settings quickly. Use code in a site-specific plugin when the post type is a permanent part of the site, since it removes a plugin dependency and can live in version control. CPT UI’s Get Code tool bridges the two.

Factor CPT UI plugin Code (register_post_type)
Skill needed None beyond wp-admin Basic PHP and FTP or file access
Who can change it Any administrator, from a settings screen Whoever can edit the plugin file
Plugin dependency Yes, CPT UI must stay active and updated No, only your own site plugin
Version control Settings live in the database Lives in a file you can track in Git
Custom fields Needs ACF or Pods alongside Core post meta, or ACF, Meta Box or Pods
Best for Prototypes, client-managed sites, restricted hosts Production sites where the post type is permanent

The pattern I use most: build the post type in CPT UI to get the settings right, open CPT UI > Tools > Get Code, paste the generated register_post_type() call into a site plugin, then delete the post type from CPT UI so it isn’t registered twice. Your content stays put, because it’s stored against the post type key.

Common custom post type mistakes

Most custom post type problems trace back to a short list of mistakes: skipping the rewrite flush, registering the post type in the theme, leaving show_in_rest off, forgetting has_archive, choosing a generic key, and mixing field plugins. Each has a clear symptom and a quick fix once you know what to look for.

Forgetting to flush rewrite rules. After registering a new post type, single and archive URLs return 404 until the rules are rebuilt. Visit Settings > Permalinks and click Save, or call flush_rewrite_rules() on plugin activation. Never call it on every request.

Registering the post type in functions.php. Switch themes and the post type vanishes from the admin, taking access to its content with it. Register it in a site-specific plugin (even a single-file mu-plugin) so it’s independent of the theme.

Omitting 'show_in_rest' => true. The block editor won’t load for the post type, and anything REST-based (headless frontends, third-party integrations) can’t read it.

Not setting has_archive when you want an archive. If you expect /portfolio/ to be a browsable page, you need has_archive. Without it, that URL returns 404.

Using a generic or over-long key. Keys over 20 characters fail to register, and a generic key like product collides with WooCommerce. Prefix your keys.

Running Pods and ACF on the same post type. Two field systems editing the same data leads to conflicts. Pick one per post type.

When You’ll Need a Developer Anyway

The no-code path covers a real majority of use cases, but it has a ceiling: custom relationships between two post types (a Book linked to its Author post type, with data flowing both directions), custom REST API endpoints exposing the post type to a separate app, admin column customization, or performance tuning once a post type holds tens of thousands of entries. None of that is a settings-screen problem. That’s the point where custom plugin development is worth bringing in, and if the post types need matching templates in your theme, that’s part of custom theme development.

Frequently asked questions

A custom post type is a content type registered in WordPress alongside the built-in posts and pages, such as portfolio items, events, properties or job listings. Each one gets its own admin menu, its own URLs (for example /portfolio/project-name/), an optional archive page and its own templates. You can extend it with custom taxonomies for grouping and custom fields for extra data.

Yes. Custom Post Type UI registers the post type through a settings screen with no PHP required. Pair it with Pods or ACF for custom fields, and you can build a working content type with its own fields and taxonomies without ever opening functions.php.

Write a function that calls register_post_type() and hook it to init, ideally in a small site-specific plugin so the post type survives a theme change. The minimum useful registration is a post type key, labels, 'public' => true and 'show_in_rest' => true. Then visit Settings > Permalinks and click Save, or call flush_rewrite_rules() on plugin activation, so the new URLs don't return 404.

It includes the post type in the WordPress REST API at /wp-json/wp/v2/{post_type} and makes it available in the block editor. It defaults to false, so without it the post type uses the classic editor and REST-based tools can't see it. Custom taxonomies need 'show_in_rest' => true set separately in register_taxonomy().

Use CPT UI when non-developers need to manage post types or when you're prototyping. Use code in a site-specific plugin for production sites where the post type is permanent, since it removes a plugin dependency and can be version-controlled. A practical middle path is to build it in CPT UI, copy the output from Tools > Get Code into a plugin, then remove it from CPT UI.

Almost always because the rewrite rules weren't flushed after the post type was registered. Go to Settings > Permalinks and click Save Changes, and the single and archive URLs should start working. If only the archive URL 404s, check that has_archive is set to true.

Photo of Ajay Khandal

Written by Ajay Khandal

I'm a freelance WordPress developer with 14+ years of experience building, fixing, and speeding up sites for businesses, agencies, and store owners across the US, UK, Europe, and Australia. I specialize in custom themes, WooCommerce, and performance, the kind of work that shows up as faster load times and fewer support tickets. You work directly with me, and replies typically arrive within 5 hours.

Work with me →