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.