How to Register a Custom Post Type in WordPress

Registering a custom post type with labels and REST support

WordPress ships with Posts and Pages out of the box, but real-world projects almost always need more. Custom post types let you organise different kinds of content — portfolio items, team members, events, testimonials — separately from standard blog posts, each with its own admin menu, labels, and permalink structure.

Problem: How do you register a custom post type in WordPress with proper labels, block editor support, and a working archive permalink?

Solution: The example below registers a portfolio post type with full labels, REST API support (added in WordPress 4.7), and a proper activation hook to flush rewrite rules — a step that's easy to miss but required for the archive permalink to work on the front end.

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

function register_portfolio_cpt() {
    $labels = [
        'name'               => __( 'Portfolio',             'textdomain' ),
        'singular_name'      => __( 'Project',               'textdomain' ),
        'add_new_item'       => __( 'Add New Project',       'textdomain' ),
        'edit_item'          => __( 'Edit Project',          'textdomain' ),
        'new_item'           => __( 'New Project',           'textdomain' ),
        'view_item'          => __( 'View Project',          'textdomain' ),
        'search_items'       => __( 'Search Portfolio',      'textdomain' ),
        'not_found'          => __( 'No projects found',     'textdomain' ),
        'not_found_in_trash' => __( 'No projects in trash',  'textdomain' ),
        'menu_name'          => __( 'Portfolio',             'textdomain' ),
    ];

    register_post_type( 'portfolio', [
        'labels'       => $labels,
        'public'       => true,
        'has_archive'  => true,
        'rewrite'      => [ 'slug' => 'portfolio' ],
        'supports'     => [ 'title', 'editor', 'thumbnail', 'excerpt' ],
        'show_in_rest' => true,   // enables the block editor (WP 4.7+)
        'menu_icon'    => 'dashicons-portfolio',
    ] );
}

// Flush rewrite rules on plugin activation — never on every request
register_activation_hook( __FILE__, function() {
    register_portfolio_cpt();
    flush_rewrite_rules();
} );

NOTE: Never call flush_rewrite_rules() on every page load — it is expensive and will noticeably slow your site. Call it only in an activation hook. If your post type archive returns a 404 after registration, go to Settings → Permalinks and click Save to rebuild the rules manually.