Register a custom post type in WordPress the right way

WordPress ships with two built-in content types — posts and pages — that cover the standard blogging and static content use cases the platform was originally designed for. As WordPress evolved into a general-purpose content management system and application framework, developers began building sites that needed to manage fundamentally different types of structured content: real estate listings with bedrooms and price fields, event calendars with dates and venues, employee directories with job titles and departments, recipe collections with ingredients and cooking times. Shoehorning all of these into the blog post model and using custom fields for the extra attributes works at small scale but becomes unmaintainable quickly — your posts list mixes actual blog articles with team member profiles, the editor interface shows irrelevant blocks and meta boxes, and URL structures do not reflect the actual content type. Custom Post Types solve this by letting you register entirely new content entities that behave like first-class WordPress citizens. A registered CPT gets its own admin menu section, its own post list screen with custom columns, its own edit screen, its own archive URL, its own template hierarchy, and full integration with widgets, menus, REST API, and any plugin that respects WordPress content standards. The registration function, register_post_type(), accepts a slug and an array of configuration arguments that control every aspect of the CPT’s behavior. The labels array defines all the strings that appear in the admin interface for this type. The supports array specifies which default meta boxes the edit screen shows. Setting public to true makes the CPT appear on the front end and in admin. Setting has_archive to true creates a listing page at yourdomain.com/your-cpt-slug/. Setting show_in_rest to true enables Gutenberg support and REST API access, which is essential for any site using the block editor. After registering the CPT, visit Settings → Permalinks and click Save to flush the rewrite rules — without this step your new CPT URLs return 404 errors. If your CPT needs its own classification system, pair it with a custom taxonomy using register_taxonomy(), which works exactly like registering a CPT but produces a category or tag-style classification hierarchy. You can then use the taxonomy-based query technique described in our post on querying posts by custom taxonomy term to filter CPT entries by classification.

Problem: The built-in posts and pages content types do not fit structured content like portfolios, events, or team members.

Solution: Add the following code to your functions.php file:

<?php
add_action( 'init', 'ha_register_portfolio_cpt' );

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

    $args = array(
        'labels'        => $labels,
        'public'        => true,
        'has_archive'   => true,
        'rewrite'       => array( 'slug' => 'portfolio' ),
        'supports'      => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
        'show_in_rest'  => true,  // enables Gutenberg and REST API
        'menu_icon'     => 'dashicons-portfolio',
        'menu_position' => 5,
    );

    register_post_type( 'portfolio', $args );
}

// Optional: register a custom taxonomy for the CPT
add_action( 'init', 'ha_register_portfolio_category' );

function ha_register_portfolio_category() {
    register_taxonomy( 'portfolio_category', 'portfolio', array(
        'label'        => 'Portfolio Categories',
        'rewrite'      => array( 'slug' => 'portfolio-category' ),
        'hierarchical' => true,
        'show_in_rest' => true,
    ) );
}

NOTE: After adding this code, go to Settings → Permalinks and click Save — this flushes the rewrite rules and makes the new CPT URLs work. Without this step, all single and archive URLs for the new post type return 404. Replace portfolio with your own CPT slug throughout the code. If you plan to show CPT entries in search results, also add 'exclude_from_search' => false to the $args array, and consider limiting search to specific post types to keep results relevant.