Skip to content
WordPress

WordPress Custom Post Types: Set Them Up Properly

WordPress custom post types let you build any content structure — portfolios, recipes, properties, events. This guide covers code registration, CPT UI, templates, search, and REST API.

WordPress Custom Post Types: Set Them Up Properly

WordPress ships with two built-in content types — posts and pages — and they cover the needs of a simple blog. But most serious WordPress sites need more: a portfolio of projects, a product catalogue, a recipe collection, a property listing database, an events calendar. WordPress custom post types are the mechanism for creating any content structure imaginable, with their own admin menus, URL structures, template files, and capability sets. This guide covers creating, managing, and displaying custom post types correctly. If you want the full context, see our Complete Guide to WordPress How.

WordPress Custom Post Types — Understanding the Architecture

WordPress custom post types are content containers that WordPress treats with the same database and template infrastructure as the built-in “post” post type, but with independent configuration — their own admin menu labels, capability groups, URL slugs, and archive behaviours. The data itself is stored in the wp_posts table like everything else in WordPress, identified by the post_type column value. This shared database structure means custom post types benefit from all of WordPress’s built-in infrastructure: revisions, autosave, meta boxes, taxonomies, custom fields, user capability management, and REST API exposure.

When planning WordPress custom post types for a site, two key architectural decisions shape everything else: taxonomy assignment and template hierarchy. Taxonomies — the classification system — can be shared between post types (using the built-in “category” and “tag” taxonomies on both posts and a custom “recipe” post type) or custom-created specifically for the new post type (a “Cuisine” taxonomy only for recipes). Template hierarchy determines how WordPress displays the custom post type — WordPress looks for single-{post-type}.php for individual items and archive-{post-type}.php for the archive listing. Planning these before creating the post type prevents structural refactoring later when content has already been added.

The difference between using a plugin to manage WordPress custom post types and registering them in code matters for long-term site management. Plugin-based registration (Custom Post Type UI, Pods) provides a visual admin interface and stores configuration in the database — accessible to non-developers but dependent on the plugin remaining active. Code-based registration in a custom plugin or child theme’s functions.php is permanently tied to the codebase and survives plugin changes but requires developer access to modify. For professional WordPress sites with ongoing development, a custom plugin is the correct approach — it keeps the post type registration separate from both the theme and any third-party plugins, ensuring the content type persists regardless of which theme or other plugins are active.

Registering Custom Post Types With Code

register_post_type() is the API for creating WordPress custom post types via code. It must be called on the init hook, takes a post type name (the internal identifier) and an array of arguments, and is idempotent — calling it multiple times with the same name on each page load is the correct WordPress pattern.

A complete registration for one of many possible WordPress custom post types:

add_action('init', function() {
    register_post_type('portfolio', [
        'label'               => 'Portfolio',
        'public'              => true,
        'show_in_rest'        => true,
        'has_archive'         => true,
        'rewrite'             => ['slug' => 'portfolio'],
        'supports'            => ['title','editor','thumbnail','excerpt','custom-fields'],
        'menu_icon'           => 'dashicons-portfolio',
        'taxonomies'          => ['category','post_tag'],
        'labels'              => [
            'name'          => 'Portfolio Items',
            'singular_name' => 'Portfolio Item',
            'add_new_item'  => 'Add New Portfolio Item',
        ],
    ]);
});

The critical arguments: 'public' => true makes the post type visible in the admin and queryable on the front-end. 'show_in_rest' => true exposes it via the REST API — required for the block editor to work with this post type. 'has_archive' => true creates the archive page at /portfolio/. 'rewrite' => ['slug' => 'portfolio'] sets the URL prefix. After adding this code for WordPress custom post types, navigate to Settings → Permalinks → Save Changes to flush rewrite rules and register the new URL structure — without this flush, all WordPress custom post types URLs return 404 immediately after registration.

Using Custom Post Type UI Plugin

Custom Post Type UI (CPT UI) is the most widely used plugin for managing WordPress custom post types without code, with over 2 million active installations. It provides a complete visual registration interface and also generates the equivalent PHP code for migration to a code-based approach when needed.

Install CPT UI to manage WordPress custom post types: CPT UI → Add/Edit Post Types → fill in the slug (lowercase, no spaces, maximum 20 characters — this is the internal identifier), singular label, and plural label. Under Settings, configure the key options: Public (whether it appears on the front-end and in admin), Has Archive (whether a /slug/ archive URL exists), Show in REST (required for block editor compatibility), and Rewrite Slug (the URL prefix). Under Supports, check the features this post type should have in its edit screen — Title, Editor, Thumbnail, Excerpt, Author, and Custom Fields are the most common. Save — the post type appears immediately in the admin sidebar without any code changes or permalink flush.

CPT UI also registers custom taxonomies alongside WordPress custom post types: CPT UI → Add/Edit Taxonomies → configure the taxonomy name, labels, and which post types it applies to. The taxonomy can use a hierarchical structure (like categories, with parent-child relationships) or a flat structure (like tags). After creating the taxonomy, it appears as a meta box on the post type’s edit screen and as a filtering option in the admin list view. One important limitation of CPT UI: if the plugin is deactivated, all custom post types and taxonomies it registered are deregistered — the admin menu items disappear and the URLs return 404. The content in the database is not lost, but it is inaccessible until the plugin is reactivated or equivalent code registration is added. This dependency risk is why production sites eventually migrate CPT UI registrations to code in a custom plugin. According to the WordPress developer handbook, post type registration should always occur in a plugin rather than a theme, because themes can change while content must persist across theme switches.

Displaying Custom Post Types on the Front-End

Registered WordPress custom post types need template files to display correctly on the front-end. WordPress’s template hierarchy automatically looks for type-specific templates before falling back to general templates — creating these files in the child theme gives full control over the presentation without modifying the parent theme.

Create single-portfolio.php in the child theme for individual portfolio item display. WordPress uses this template for any single /portfolio/item-name/ URL. The template follows standard WordPress template structure — call get_header(), run the Loop with while (have_posts()) { the_post(); }, output the post data using template tags (the_title(), the_content(), the_post_thumbnail()), call get_footer(). Create archive-portfolio.php for the /portfolio/ archive listing. This template receives a WP_Query pre-populated with all published portfolio items, which you loop through to display the archive grid or list. Without these template files, WordPress falls back to single.php and archive.php — the generic templates — which may produce acceptable results but typically lack the type-specific layout and metadata display that custom post types need.

Querying WordPress custom post types anywhere on the site uses WP_Query with the post_type argument: $query = new WP_Query(['post_type' => 'portfolio', 'posts_per_page' => 6, 'post_status' => 'publish']);. This query can be placed in any template file, shortcode, or widget to display portfolio items in any context — a homepage section, a sidebar widget, or a custom page template. The standard Loop processes WP_Query results identically to the main query: while ($query->have_posts()) { $query->the_post(); } followed by template tags and wp_reset_postdata(); after the loop. Our guide on creating WordPress custom post types with CPT UI pairs naturally here — the plugin handles registration while the template files in the child theme handle display. Our guide on adding custom CSS to WordPress covers styling the new templates for custom post types without modifying parent theme files.

Custom Post Types in Search, REST API, and Gutenberg

WordPress custom post types registered with 'show_in_rest' => true are automatically available in the block editor for content editing and through the REST API for headless and decoupled applications. Post types registered without this flag use the classic editor interface and are not accessible via the REST API — intentional for internal data post types that should not be editable through the block editor or exposed externally.

Include custom post types in WordPress search results by ensuring 'exclude_from_search' => false is set in the registration arguments — this is the default when 'public' => true but can be overridden for post types that should exist publicly but not appear in search. For post types where the default search exclusion needs to be changed after registration (for example, a third-party plugin’s post type), the pre_get_posts hook approach described in our guide on fixing WordPress search not working adds the post type to the main search query without modifying the plugin’s registration code.

REST API exposure of WordPress custom post types allows headless WordPress setups — where Next.js, Nuxt, or Gatsby fetches content from WordPress via API and renders it externally — to access all content types. The REST API endpoint for a custom post type follows the pattern /wp-json/wp/v2/{post-type-plural-slug}/. Custom fields on WordPress custom post types exposed via REST require the additional show_in_rest argument on the field registration in register_post_meta(). Without explicitly enabling REST API exposure for custom fields, they are not included in the API response even when the post type itself is API-accessible. For WooCommerce sites, custom post types for product variations, orders, and subscriptions all use the same REST API infrastructure — understanding this architecture is foundational for any WooCommerce API integration or headless commerce implementation. Reviews from the WordPress developer community confirm that the combination of correct registration arguments, post-registration permalink flush, and type-specific template files provides the complete foundation for stable, maintainable custom post types across all WordPress versions.

Capabilities for WordPress custom post types control which user roles can create, edit, publish, and delete items of that type. By default, custom post types use the same capabilities as the “post” post type — anyone who can edit posts can edit custom post type items. For post types that should be managed only by specific roles (e.g., only Editors can manage an Events post type while Authors can only manage their own posts), set 'capability_type' => 'event' in the registration arguments. This creates a separate capability group (edit_events, publish_events, delete_others_events, etc.) that can be assigned selectively to roles using a capability management plugin like User Role Editor. This granular capability control is essential for client sites where different content teams manage different post types independently.

Performance considerations for WordPress custom post types become significant when post counts grow large. Each custom post type query hits the wp_posts table — a table that may contain millions of rows on busy sites. Ensure custom queries use 'no_found_rows' => true when pagination is not needed (avoids the expensive SQL_CALC_FOUND_ROWS query), 'update_post_meta_cache' => false when custom fields are not used in the template (avoids loading all post meta), and 'update_post_term_cache' => false when taxonomies are not used in the template output. These three optimisation flags combined can reduce custom post type query time by 40–60% on posts tables with large row counts, directly improving page load times for archive pages and custom loops.

Migrating content between post types — for example, converting existing Posts to a custom “News” post type — requires direct database updates. Use phpMyAdmin or a safe database tool: UPDATE wp_posts SET post_type = 'news' WHERE post_type = 'post' AND [condition to identify the posts to migrate];. After the update, flush rewrite rules (Settings → Permalinks → Save Changes) and clear all caches. The migrated posts now appear under the custom post type admin menu and are served at the custom post type’s URL structure. Before running the migration SQL, test it with a SELECT query first to confirm the WHERE condition selects exactly the intended posts. Back up the database before any direct SQL migration on WordPress custom post types — a flawed WHERE clause could migrate unintended content.

Gutenberg’s block editor provides full support for WordPress custom post types registered with 'show_in_rest' => true — including all core blocks, third-party blocks, and the Document panel with status, visibility, and publish date controls. Post types registered without REST API exposure fall back to the classic editor interface, which does not support blocks. For existing post types that were registered without 'show_in_rest' and need to be migrated to the block editor, adding 'show_in_rest' => true to the registration arguments enables block editor support immediately — existing content is preserved exactly and the block editor parses the HTML content into blocks on first edit. No content migration or database update is required to enable block editor support on existing post types.

Nikolas Lamprou

Nikolas Lamprou (MSc; GCFR, SC-200, Security+) has been working with computers professionally since 2009 — starting with web development and e-commerce, and moving into cybersecurity over the years. Based in Greece, he brings over 15 years of real-world IT experience to SolveTechToday, where he writes about Windows fixes, software reviews, security tools, and AI applications. His goal is straightforward: cut through the noise and give readers clear, honest guidance on the tech decisions that matter.

Stay Ahead

Fix your next problem before it starts

Get the week's best Windows fixes, software picks, and security guides delivered straight to your inbox. No noise, just solutions.

Press ESC to close · Try "Windows 11" or "Chrome"