Register Custom Post Types and Taxonomies in WordPress with the init Hook

Custom post types and taxonomies are the foundation of structured content in WordPress — they let you model data as portfolios, events, team members, or products without forcing everything into posts or pages. Registering them directly in a theme’s functions.php or in a dedicated plugin file is a deliberate architectural choice: plugin registration persists if the theme changes, while theme registration is simpler for tightly coupled content. The register_post_type() function accepts a large options array, but the most important settings for SEO and usability are rewrite, has_archive, and show_in_rest. Setting show_in_rest: true is required for the post type to appear in the block editor — without it, editing these posts falls back to the classic editor. Custom labels for every UI string prevent the admin showing “Add New Post” on a portfolio entry screen, which confuses non-technical editors. The rewrite slug should be short, lowercase, and hyphenated to produce clean URLs such as /portfolio/my-project/ rather than /?post_type=portfolio&p=123. After registering a new post type, you must flush rewrite rules by visiting Settings → Permalinks in the admin, or by running wp rewrite flush in WP-CLI — otherwise the archive and single URLs return 404. Custom taxonomies registered with register_taxonomy() need the hierarchical flag to determine whether they behave like categories (checkboxes and parent-child) or tags (free-form comma input). The custom database table post covers the next step when custom post type meta volume becomes too high for wp_postmeta. The breadcrumb post explains how to extend breadcrumb logic to support custom post type archives and hierarchical custom taxonomy term pages. Capability mapping via capability_type allows you to assign dedicated read, edit, and delete capabilities for the custom post type so editor-level users can manage portfolio entries without accessing core post management.

Problem: WordPress sites that store portfolio items, events, or team members in regular posts lack a structured URL hierarchy, appropriate admin labels, and REST API exposure needed for block editor support.

Solution: Call register_post_type() and register_taxonomy() on the init hook with show_in_rest: true, a custom rewrite slug, and complete label arrays to create a fully integrated content type.

add_action('init', function() {
    // Custom post type: Portfolio
    register_post_type('portfolio', [
        'labels' => [
            'name'               => 'Portfolio',
            'singular_name'      => 'Project',
            'add_new_item'       => 'Add New Project',
            'edit_item'          => 'Edit Project',
            'view_item'          => 'View Project',
            'search_items'       => 'Search Projects',
            'not_found'          => 'No projects found.',
        ],
        'public'       => true,
        'has_archive'  => true,
        'show_in_rest' => true,
        'menu_icon'    => 'dashicons-portfolio',
        'supports'     => ['title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'],
        'rewrite'      => ['slug' => 'portfolio', 'with_front' => false],
    ]);

    // Custom taxonomy: Project Type (hierarchical, like categories)
    register_taxonomy('project_type', 'portfolio', [
        'labels' => [
            'name'          => 'Project Types',
            'singular_name' => 'Project Type',
            'add_new_item'  => 'Add New Project Type',
        ],
        'hierarchical' => true,
        'show_in_rest' => true,
        'rewrite'      => ['slug' => 'project-type'],
    ]);
});

NOTE: Visit Settings → Permalinks and click Save after registering a new post type — this flushes the rewrite rules cache and makes the archive URL /portfolio/ and single post URL /portfolio/my-project/ resolve correctly instead of returning 404.