TopSyde
Start Free Trial

WordPress Custom Post Types and Taxonomies Explained

Complete guide to WordPress custom post types: registration, taxonomies, ACF integration, REST API exposure, and query optimization for developers.

Colton Joseph

Colton Joseph

Founder & Lead Developer

··9 min read

Last updated: April 26, 2026

WordPress custom post types architecture diagram showing CPTs, taxonomies, and meta fields relationships

WordPress custom post types (CPTs) extend the default post and page functionality to create structured content for specific purposes like portfolios, testimonials, or products. They provide dedicated content management interfaces, custom taxonomies, and specialized query capabilities beyond WordPress's built-in content types.

What Are WordPress Custom Post Types?

Custom post types are content structures that extend WordPress beyond its default posts and pages. They function as independent content containers with their own admin interfaces, archive pages, and query methods. Unlike posts, which are chronological content, CPTs organize data by purpose and function.

WordPress ships with five built-in post types: post, page, attachment, revision, and nav_menu_item. Custom post types follow the same WordPress architecture but allow developers to define specific fields, taxonomies, and capabilities for each content type.

According to WordPress.org usage statistics, over 60% of WordPress sites use at least one custom post type, with WooCommerce's product CPT being the most common implementation (2024).

The WordPress post type system uses a hierarchical structure where each post type can have:

  • Custom fields (meta data)
  • Taxonomies (classification systems)
  • Archive templates
  • Single post templates
  • REST API endpoints
  • Admin UI configurations

When to Use Custom Post Types vs Pages and Categories

Choose custom post types when content requires structured data fields, specialized templates, or dedicated management interfaces. Use pages for static hierarchical content and categories for organizing chronological posts.

Content TypeUse CPT WhenUse Pages/Categories When
Portfolio ItemsNeed image galleries, client details, project datesSimple about/work sections
Team MembersRequire bio, position, contact info fieldsBasic staff listing
ProductsNeed pricing, SKU, inventory dataService descriptions only
TestimonialsInclude rating, client company, project typeSimple quote collection
EventsRequire date, location, ticket pricingOccasional announcements

Custom post types excel for content that benefits from:

  • Structured data fields: Product specifications, event dates, portfolio categories
  • Specialized queries: "Show upcoming events" or "Display featured portfolio items"
  • Custom admin interfaces: Tailored editing experiences for different content types
  • REST API endpoints: Dedicated API access for mobile apps or external systems

Categories and tags work better for:

  • Content classification: Organizing blog posts by topic
  • SEO organization: Creating topic clusters and content hierarchies
  • Simple filtering: Basic content sorting without complex data requirements

How to Register Custom Post Types Programmatically

Register custom post types using the register_post_type() function in your theme's functions.php file or a custom plugin. This approach provides complete control over CPT configuration and ensures proper version control integration.

function register_portfolio_post_type() {
    $labels = array(
        'name'               => 'Portfolio',
        'singular_name'      => 'Portfolio Item',
        'menu_name'          => 'Portfolio',
        'add_new'            => 'Add New Item',
        'add_new_item'       => 'Add New Portfolio Item',
        'edit_item'          => 'Edit Portfolio Item',
        'new_item'           => 'New Portfolio Item',
        'view_item'          => 'View Portfolio Item',
        'search_items'       => 'Search Portfolio',
        'not_found'          => 'No portfolio items found',
        'not_found_in_trash' => 'No portfolio items found in Trash',
    );

    $args = array(
        'labels'              => $labels,
        'public'              => true,
        'publicly_queryable'  => true,
        'show_ui'             => true,
        'show_in_menu'        => true,
        'show_in_nav_menus'   => true,
        'show_in_admin_bar'   => true,
        'show_in_rest'        => true,
        'rest_base'           => 'portfolio',
        'capability_type'     => 'post',
        'hierarchical'        => false,
        'menu_position'       => 20,
        'menu_icon'           => 'dashicons-portfolio',
        'supports'            => array('title', 'editor', 'thumbnail', 'excerpt'),
        'has_archive'         => 'portfolio',
        'rewrite'             => array('slug' => 'portfolio'),
        'query_var'           => true,
    );

    register_post_type('portfolio', $args);
}
add_action('init', 'register_portfolio_post_type');

Key registration parameters:

public: Controls front-end visibility and admin interface access show_in_rest: Enables Gutenberg editor and REST API endpoints
supports: Defines available meta boxes (title, editor, thumbnail, etc.) has_archive: Creates archive pages at /portfolio/ URL rewrite: Customizes permalink structure hierarchical: Enables parent-child relationships like pages

Always hook registration to the init action to ensure WordPress core functions are available. Register CPTs in plugins rather than themes to preserve content when switching themes.

Custom Taxonomies for Content Organization

Custom taxonomies provide classification systems for custom post types, similar to how categories and tags organize posts. They create hierarchical or flat organizational structures depending on your content needs.

function register_portfolio_taxonomies() {
    // Hierarchical taxonomy (like categories)
    register_taxonomy('portfolio_category', 'portfolio', array(
        'labels' => array(
            'name'              => 'Portfolio Categories',
            'singular_name'     => 'Portfolio Category',
            'search_items'      => 'Search Categories',
            'all_items'         => 'All Categories',
            'edit_item'         => 'Edit Category',
            'update_item'       => 'Update Category',
            'add_new_item'      => 'Add New Category',
            'new_item_name'     => 'New Category Name',
            'menu_name'         => 'Categories',
        ),
        'hierarchical'      => true,
        'public'            => true,
        'show_ui'           => true,
        'show_admin_column' => true,
        'show_in_nav_menus' => true,
        'show_tagcloud'     => false,
        'show_in_rest'      => true,
        'rewrite'           => array('slug' => 'portfolio-category'),
    ));

    // Non-hierarchical taxonomy (like tags)
    register_taxonomy('portfolio_skill', 'portfolio', array(
        'labels' => array(
            'name'          => 'Skills',
            'singular_name' => 'Skill',
            'add_new_item'  => 'Add New Skill',
            'new_item_name' => 'New Skill Name',
            'menu_name'     => 'Skills',
        ),
        'hierarchical'      => false,
        'public'            => true,
        'show_ui'           => true,
        'show_admin_column' => true,
        'show_in_rest'      => true,
        'rewrite'           => array('slug' => 'skill'),
    ));
}
add_action('init', 'register_portfolio_taxonomies');

Taxonomy registration creates:

  • Admin interface for term management
  • Meta boxes in post edit screens
  • Archive pages for taxonomy terms
  • REST API endpoints for term queries

Use hierarchical taxonomies for nested classifications (Web Design > E-commerce > Shopify). Use non-hierarchical taxonomies for flexible tagging systems (PHP, JavaScript, React).

Advanced Custom Fields Integration

ACF provides a powerful interface for managing custom post type meta fields. It creates field groups that attach to specific CPTs, offering rich field types and template integration options.

// ACF field group registration (via PHP or JSON)
function register_portfolio_acf_fields() {
    if (function_exists('acf_add_local_field_group')) {
        acf_add_local_field_group(array(
            'key' => 'group_portfolio_fields',
            'title' => 'Portfolio Details',
            'fields' => array(
                array(
                    'key' => 'field_client_name',
                    'label' => 'Client Name',
                    'name' => 'client_name',
                    'type' => 'text',
                    'required' => 1,
                ),
                array(
                    'key' => 'field_project_date',
                    'label' => 'Project Date',
                    'name' => 'project_date',
                    'type' => 'date_picker',
                    'display_format' => 'F j, Y',
                    'return_format' => 'Y-m-d',
                ),
                array(
                    'key' => 'field_project_url',
                    'label' => 'Project URL',
                    'name' => 'project_url',
                    'type' => 'url',
                ),
                array(
                    'key' => 'field_gallery',
                    'label' => 'Project Gallery',
                    'name' => 'project_gallery',
                    'type' => 'gallery',
                    'return_format' => 'array',
                ),
            ),
            'location' => array(
                array(
                    array(
                        'param' => 'post_type',
                        'operator' => '==',
                        'value' => 'portfolio',
                    ),
                ),
            ),
        ));
    }
}
add_action('acf/init', 'register_portfolio_acf_fields');

Template integration retrieves ACF data using get_field() functions:

// In single-portfolio.php template
$client_name = get_field('client_name');
$project_date = get_field('project_date');
$project_url = get_field('project_url');
$gallery = get_field('project_gallery');

if ($gallery): ?>
    <div class="portfolio-gallery">
        <?php foreach($gallery as $image): ?>
            <img src="<?php echo $image['sizes']['medium']; ?>" 
                 alt="<?php echo $image['alt']; ?>">
        <?php endforeach; ?>
    </div>
<?php endif;

ACF provides REST API integration when show_in_rest is enabled, exposing custom fields in JSON responses for headless WordPress implementations.

REST API Exposure and Headless Integration

WordPress REST API automatically creates endpoints for custom post types when show_in_rest is enabled during registration. This enables headless WordPress architectures and external application integrations.

Default CPT REST endpoints:

  • GET /wp-json/wp/v2/portfolio - List all portfolio items
  • GET /wp-json/wp/v2/portfolio/{id} - Get specific portfolio item
  • POST /wp-json/wp/v2/portfolio - Create new portfolio item (authenticated)
  • PUT /wp-json/wp/v2/portfolio/{id} - Update portfolio item (authenticated)

Custom REST fields registration:

function add_portfolio_rest_fields() {
    register_rest_field('portfolio', 'client_name', array(
        'get_callback' => function($post) {
            return get_field('client_name', $post['id']);
        },
        'schema' => array(
            'description' => 'Client name for portfolio item',
            'type' => 'string',
        ),
    ));
    
    register_rest_field('portfolio', 'project_gallery', array(
        'get_callback' => function($post) {
            $gallery = get_field('project_gallery', $post['id']);
            if (!$gallery) return null;
            
            return array_map(function($image) {
                return array(
                    'id' => $image['ID'],
                    'url' => $image['url'],
                    'alt' => $image['alt'],
                    'sizes' => $image['sizes'],
                );
            }, $gallery);
        },
        'schema' => array(
            'description' => 'Project gallery images',
            'type' => 'array',
        ),
    ));
}
add_action('rest_api_init', 'add_portfolio_rest_fields');

For headless WordPress implementations, configure proper CORS headers and authentication. According to State of JS 2024, 34% of WordPress developers use headless architectures for performance-critical applications.

Archive Templates and Custom Queries

Custom post types require specific template files for archive and single post displays. WordPress follows a template hierarchy that checks for CPT-specific templates before falling back to generic ones.

Template hierarchy for portfolio CPT:

  1. archive-portfolio.php (CPT archive)
  2. archive.php (generic archive)
  3. index.php (fallback)

Single post templates:

  1. single-portfolio-{slug}.php (specific post)
  2. single-portfolio.php (CPT single)
  3. single.php (generic single)
  4. singular.php (fallback)

Archive template example:

// archive-portfolio.php
get_header(); ?>

<div class="portfolio-archive">
    <header class="archive-header">
        <h1>Our Portfolio</h1>
        
        <?php
        // Portfolio category filter
        $portfolio_categories = get_terms(array(
            'taxonomy' => 'portfolio_category',
            'hide_empty' => true,
        ));
        
        if ($portfolio_categories): ?>
            <div class="portfolio-filters">
                <button data-filter="*" class="active">All</button>
                <?php foreach($portfolio_categories as $category): ?>
                    <button data-filter=".<?php echo $category->slug; ?>">
                        <?php echo $category->name; ?>
                    </button>
                <?php endforeach; ?>
            </div>
        <?php endif; ?>
    </header>

    <div class="portfolio-grid">
        <?php
        $portfolio_query = new WP_Query(array(
            'post_type' => 'portfolio',
            'posts_per_page' => 12,
            'meta_key' => 'project_date',
            'orderby' => 'meta_value',
            'order' => 'DESC',
        ));
        
        if ($portfolio_query->have_posts()): 
            while($portfolio_query->have_posts()): $portfolio_query->the_post();
                $categories = get_the_terms(get_the_ID(), 'portfolio_category');
                $category_classes = $categories ? implode(' ', wp_list_pluck($categories, 'slug')) : '';
                ?>
                
                <article class="portfolio-item <?php echo $category_classes; ?>">
                    <a href="<?php the_permalink(); ?>">
                        <?php if (has_post_thumbnail()): ?>
                            <?php the_post_thumbnail('medium'); ?>
                        <?php endif; ?>
                        <h3><?php the_title(); ?></h3>
                        <p><?php echo get_field('client_name'); ?></p>
                    </a>
                </article>
                
            <?php endwhile;
            wp_reset_postdata();
        endif; ?>
    </div>
</div>

<?php get_footer();

Query Optimization for Custom Post Types

Custom post type queries require optimization for performance, especially when dealing with complex meta queries, taxonomy filtering, or large datasets.

Colton Joseph
Colton Joseph

Founder & Lead Developer

20+ years full-stack development, WordPress, AI tools & agents

Colton is the founder of TopSyde with 20+ years of full-stack development experience spanning WordPress, cloud infrastructure, and AI-powered tooling. He specializes in performance optimization, server architecture, and building AI agents for automated site management.

Related Articles

View all →

Stop managing your WordPress site

Let our team handle hosting, speed, security, and updates — so you can focus on what matters.

Get Started Free