How to Add Custom Post Types to WordPress Without Plugins

Custom post types extend WordPress beyond blogs and pages, enabling portfolio items, products, testimonials, and any content structure imaginable. While plugins simplify creation, coding custom post types provides complete control, better performance, and deeper WordPress understanding. This comprehensive guide teaches custom post type registration, taxonomy association, template creation, and advanced customization without plugins.

Understanding Custom Post Types

WordPress includes five default post types:

  • post (Blog posts)
  • page (Static pages)
  • attachment (Media)
  • revision (Post revisions)
  • nav_menu_item (Menu items)

Custom post types add structured content beyond these defaults.

Common Use Cases:

  • Portfolio projects
  • Products/services
  • Team members
  • Testimonials
  • Events
  • Case studies
  • Properties/listings
  • Courses/lessons

Basic Custom Post Type Registration

Simple Portfolio CPT:

function dprt_register_portfolio() {
    register_post_type('portfolio', array(
        'labels' => array(
            'name' => 'Portfolio',
            'singular_name' => 'Portfolio Item',
            'add_new' => 'Add New Project',
            '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'
        ),
        'public' => true,
        'has_archive' => true,
        'rewrite' => array('slug' => 'portfolio'),
        'supports' => array('title', 'editor', 'thumbnail', 'excerpt'),
        'menu_icon' => 'dashicons-portfolio',
        'show_in_rest' => true, // Gutenberg support
    ));
}
add_action('init', 'dprt_register_portfolio');

Flush Rewrite Rules (run once after registration):

// Visit Settings → Permalinks to flush, or run this code once:
flush_rewrite_rules();

Complete Parameter Reference

Essential Parameters:

public (boolean): Makes CPT publicly queryable.

  • true: Visible on front-end and admin
  • false: Hidden from public

publicly_queryable (boolean): Whether post type can be queried from front-end.

show_ui (boolean): Generate admin UI.

show_in_menu (boolean): Show in admin menu.

  • true: Top-level menu
  • string: ‘tools.php’ makes submenu under Tools

menu_position (integer): Admin menu order.

  • 5: Below Posts
  • 10: Below Media
  • 15: Below Links
  • 20: Below Pages
  • 25: Below Comments

menu_icon (string): Dashicon class or image URL.

  • ‘dashicons-portfolio’
  • ‘dashicons-products’
  • ‘dashicons-groups’
  • get_template_directory_uri() . ‘/images/icon.png’

supports (array): Features CPT supports.

  • ‘title’: Title field
  • ‘editor’: Content editor
  • ‘thumbnail’: Featured image
  • ‘excerpt’: Excerpt field
  • ‘custom-fields’: Custom fields metabox
  • ‘comments’: Comment discussion
  • ‘revisions’: Revision tracking
  • ‘page-attributes’: Order/parent
  • ‘post-formats’: Post format selection

taxonomies (array): Associated taxonomies.

  • array(‘category’, ‘post_tag’)

has_archive (boolean|string): Archive page support.

  • true: Uses CPT slug
  • ‘projects’: Custom archive slug

rewrite (array|boolean): Permalink structure.

array(
    'slug' => 'portfolio',
    'with_front' => false,
    'hierarchical' => false
)

capability_type (string): Capability structure.

  • ‘post’: Uses post capabilities
  • ‘page’: Uses page capabilities
  • array(‘book’, ‘books’): Custom capabilities

show_in_rest (boolean): REST API and Gutenberg support.

  • true: Enables block editor

Advanced Custom Post Type Example

Complete Product CPT:

function dprt_register_products() {
    $labels = array(
        'name' => _x('Products', 'Post type general name', 'textdomain'),
        'singular_name' => _x('Product', 'Post type singular name', 'textdomain'),
        'menu_name' => _x('Products', 'Admin Menu text', 'textdomain'),
        'name_admin_bar' => _x('Product', 'Add New on Toolbar', 'textdomain'),
        'add_new' => __('Add New', 'textdomain'),
        'add_new_item' => __('Add New Product', 'textdomain'),
        'new_item' => __('New Product', 'textdomain'),
        'edit_item' => __('Edit Product', 'textdomain'),
        'view_item' => __('View Product', 'textdomain'),
        'all_items' => __('All Products', 'textdomain'),
        'search_items' => __('Search Products', 'textdomain'),
        'parent_item_colon' => __('Parent Products:', 'textdomain'),
        'not_found' => __('No products found.', 'textdomain'),
        'not_found_in_trash' => __('No products found in Trash.', 'textdomain'),
        'featured_image' => _x('Product Image', 'Overrides the "Featured Image" phrase', 'textdomain'),
        'set_featured_image' => _x('Set product image', 'Overrides the "Set featured image" phrase', 'textdomain'),
        'remove_featured_image' => _x('Remove product image', 'Overrides the "Remove featured image" phrase', 'textdomain'),
        'use_featured_image' => _x('Use as product image', 'Overrides the "Use as featured image" phrase', 'textdomain'),
        'archives' => _x('Product archives', 'The post type archive label', 'textdomain'),
        'insert_into_item' => _x('Insert into product', 'Overrides the "Insert into post" phrase', 'textdomain'),
        'uploaded_to_this_item' => _x('Uploaded to this product', 'Overrides the "Uploaded to this post" phrase', 'textdomain'),
        'filter_items_list' => _x('Filter products list', 'Screen reader text', 'textdomain'),
        'items_list_navigation' => _x('Products list navigation', 'Screen reader text', 'textdomain'),
        'items_list' => _x('Products list', 'Screen reader text', 'textdomain'),
    );

    $args = array(
        'labels' => $labels,
        'public' => true,
        'publicly_queryable' => true,
        'show_ui' => true,
        'show_in_menu' => true,
        'query_var' => true,
        'rewrite' => array('slug' => 'product'),
        'capability_type' => 'post',
        'has_archive' => true,
        'hierarchical' => false,
        'menu_position' => 5,
        'menu_icon' => 'dashicons-products',
        'supports' => array('title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'),
        'show_in_rest' => true,
        'rest_base' => 'products',
        'rest_controller_class' => 'WP_REST_Posts_Controller',
    );

    register_post_type('product', $args);
}
add_action('init', 'dprt_register_products');

Custom Taxonomies for CPT

Category-Style Taxonomy (Hierarchical):

function dprt_register_product_category() {
    register_taxonomy('product_category', 'product', array(
        'labels' => array(
            'name' => 'Product Categories',
            'singular_name' => 'Product Category',
            'search_items' => 'Search Categories',
            'all_items' => 'All Categories',
            'parent_item' => 'Parent Category',
            'parent_item_colon' => 'Parent Category:',
            '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, // Like categories
        'show_ui' => true,
        'show_admin_column' => true, // Show in admin post list
        'query_var' => true,
        'rewrite' => array('slug' => 'product-category'),
        'show_in_rest' => true,
    ));
}
add_action('init', 'dprt_register_product_category');

Tag-Style Taxonomy (Non-Hierarchical):

function dprt_register_product_tag() {
    register_taxonomy('product_tag', 'product', array(
        'labels' => array(
            'name' => 'Product Tags',
            'singular_name' => 'Product Tag',
            'search_items' => 'Search Tags',
            'all_items' => 'All Tags',
            'edit_item' => 'Edit Tag',
            'update_item' => 'Update Tag',
            'add_new_item' => 'Add New Tag',
            'new_item_name' => 'New Tag Name',
            'menu_name' => 'Tags',
        ),
        'hierarchical' => false, // Like tags
        'show_ui' => true,
        'show_admin_column' => true,
        'query_var' => true,
        'rewrite' => array('slug' => 'product-tag'),
        'show_in_rest' => true,
    ));
}
add_action('init', 'dprt_register_product_tag');

Template Files for Custom Post Types

WordPress looks for these template files (in order):

Single Post:

  1. single-{post-type}.php (single-portfolio.php)
  2. single.php
  3. singular.php
  4. index.php

Archive:

  1. archive-{post-type}.php (archive-portfolio.php)
  2. archive.php
  3. index.php

Taxonomy:

  1. taxonomy-{taxonomy}-{term}.php
  2. taxonomy-{taxonomy}.php
  3. taxonomy.php
  4. archive.php

Single Portfolio Template (single-portfolio.php):

<?php get_header(); ?>

<main id="primary" class="site-main">
    <?php
    while (have_posts()) :
        the_post();
        ?>
        <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
            <header class="entry-header">
                <h1 class="entry-title"><?php the_title(); ?></h1>
            </header>

            <?php if (has_post_thumbnail()) : ?>
                <div class="post-thumbnail">
                    <?php the_post_thumbnail('large'); ?>
                </div>
            <?php endif; ?>

            <div class="entry-content">
                <?php the_content(); ?>
            </div>

            <?php
            // Display custom taxonomy terms
            $categories = get_the_terms(get_the_ID(), 'portfolio_category');
            if ($categories && !is_wp_error($categories)) :
                ?>
                <div class="portfolio-categories">
                    <strong>Categories:</strong>
                    <?php
                    foreach ($categories as $category) {
                        echo '<span>' . esc_html($category->name) . '</span> ';
                    }
                    ?>
                </div>
            <?php endif; ?>
        </article>
    <?php endwhile; ?>
</main>

<?php get_footer(); ?>

Portfolio Archive Template (archive-portfolio.php):

<?php get_header(); ?>

<main id="primary" class="site-main">
    <header class="page-header">
        <h1 class="page-title">Portfolio</h1>
    </header>

    <?php if (have_posts()) : ?>
        <div class="portfolio-grid">
            <?php
            while (have_posts()) :
                the_post();
                ?>
                <article id="post-<?php the_ID(); ?>" <?php post_class('portfolio-item'); ?>>
                    <?php if (has_post_thumbnail()) : ?>
                        <a href="<?php the_permalink(); ?>">
                            <?php the_post_thumbnail('medium'); ?>
                        </a>
                    <?php endif; ?>

                    <h2 class="entry-title">
                        <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
                    </h2>

                    <div class="entry-excerpt">
                        <?php the_excerpt(); ?>
                    </div>
                </article>
            <?php endwhile; ?>
        </div>

        <?php the_posts_pagination(); ?>

    <?php else : ?>
        <p>No portfolio items found.</p>
    <?php endif; ?>
</main>

<?php get_footer(); ?>

Querying Custom Post Types

WP_Query Examples:

// Get all portfolio items
$portfolio = new WP_Query(array(
    'post_type' => 'portfolio',
    'posts_per_page' => 10,
));

// Get portfolio by category
$featured_portfolio = new WP_Query(array(
    'post_type' => 'portfolio',
    'posts_per_page' => 6,
    'tax_query' => array(
        array(
            'taxonomy' => 'portfolio_category',
            'field' => 'slug',
            'terms' => 'featured',
        ),
    ),
));

// Multiple post types
$mixed = new WP_Query(array(
    'post_type' => array('post', 'portfolio', 'product'),
    'posts_per_page' => 10,
));

// Custom field query
$expensive_products = new WP_Query(array(
    'post_type' => 'product',
    'meta_query' => array(
        array(
            'key' => 'price',
            'value' => 100,
            'compare' => '>',
            'type' => 'NUMERIC',
        ),
    ),
));

Adding Custom Columns to Admin List

// Add custom columns
function dprt_portfolio_columns($columns) {
    $new_columns = array();
    $new_columns['cb'] = $columns['cb'];
    $new_columns['thumbnail'] = 'Image';
    $new_columns['title'] = $columns['title'];
    $new_columns['portfolio_category'] = 'Category';
    $new_columns['date'] = $columns['date'];
    return $new_columns;
}
add_filter('manage_portfolio_posts_columns', 'dprt_portfolio_columns');

// Populate custom columns
function dprt_portfolio_column_content($column, $post_id) {
    switch ($column) {
        case 'thumbnail':
            echo get_the_post_thumbnail($post_id, array(60, 60));
            break;

        case 'portfolio_category':
            $terms = get_the_terms($post_id, 'portfolio_category');
            if ($terms && !is_wp_error($terms)) {
                $term_names = wp_list_pluck($terms, 'name');
                echo implode(', ', $term_names);
            } else {
                echo '—';
            }
            break;
    }
}
add_action('manage_portfolio_posts_custom_column', 'dprt_portfolio_column_content', 10, 2);

// Make columns sortable
function dprt_sortable_columns($columns) {
    $columns['portfolio_category'] = 'portfolio_category';
    return $columns;
}
add_filter('manage_edit-portfolio_sortable_columns', 'dprt_sortable_columns');
function dprt_custom_permalinks($post_link, $post) {
    if ($post->post_type === 'portfolio') {
        $terms = get_the_terms($post->ID, 'portfolio_category');
        if ($terms && !is_wp_error($terms)) {
            $category = array_shift($terms);
            return str_replace('%portfolio_category%', $category->slug, $post_link);
        }
    }
    return $post_link;
}
add_filter('post_type_link', 'dprt_custom_permalinks', 10, 2);

// Register rewrite with taxonomy
register_post_type('portfolio', array(
    'rewrite' => array(
        'slug' => 'portfolio/%portfolio_category%',
        'with_front' => false
    ),
    // ... other args
));

Best Practices

Use Descriptive Slugs: ‘portfolio’ better than ‘pf’.

Translation Ready: Use _x() and __() for labels.

Namespace Functions: Prefix functions to avoid conflicts.

Flush Permalinks: After registration changes, visit Settings → Permalinks.

show_in_rest: Always true for Gutenberg support.

Separate Files: Keep CPT registration in dedicated file (inc/post-types.php).

Documentation: Comment complex registrations explaining purpose.

Conclusion

WordPress custom post types extend content structure beyond posts and pages without plugins. Register CPTs using register_post_type(), create custom taxonomies with register_taxonomy(), build template files following WordPress hierarchy, and query content with WP_Query. Manual CPT registration provides complete control, better performance, and deeper platform understanding compared to plugin-based approaches.

  1. WordPress register_post_type() Reference
  2. WordPress register_taxonomy() Reference
  3. Template Hierarchy
  4. GenerateWP Post Type Generator
  5. WordPress CPT Documentation

Call to Action

Custom post types need protection. Backup Copilot Pro safeguards your WordPress custom configurations and content. Protect your CPT implementations—start your free 30-day trial today!