WordPress Custom Post Types: Build Beyond Posts and Pages
WordPress Custom Post Types: Build Beyond Posts and Pages
Out of the box, WordPress gives you two content types: Posts and Pages. For a blog or a simple website, that is enough. But the moment you want to manage a portfolio, a product catalog, a job board, a recipe collection, or a real estate listing database, you need more structure than Posts can provide.
That is exactly what Custom Post Types (CPTs) are for. This guide covers what they are, how to register them, how to add custom fields with meta boxes, and the best ways to display them on your site.
What Is a Custom Post Type?
A Custom Post Type is a new content type you create with its own set of capabilities, admin menu entry, labels, and supported features. Structurally, CPTs are stored in the same wp_posts database table as regular posts and pages, but they have a different post_type value.
Examples of what CPTs are used for:
- Portfolio Items (for a creative agency)
- Properties (for a real estate site)
- Products (for e-commerce — WooCommerce itself uses CPTs)
- Team Members (for a company site)
- Testimonials
- Events
- FAQs
Registering a Custom Post Type
Custom Post Types are registered using the register_post_type() function, called on the init hook. Add this to your theme’s functions.php or, better yet, a site-specific plugin:
<?php
function register_portfolio_cpt() {
$labels = array(
'name' => 'Portfolio Items',
'singular_name' => 'Portfolio Item',
'add_new' => 'Add New',
'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 Items',
'not_found' => 'No portfolio items found',
'not_found_in_trash' => 'No portfolio items found in trash',
'menu_name' => 'Portfolio',
);
$args = array(
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => true,
'rewrite' => array('slug' => 'portfolio'),
'capability_type' => 'post',
'has_archive' => true,
'hierarchical' => false,
'menu_position' => 5,
'menu_icon' => 'dashicons-portfolio',
'supports' => array('title', 'editor', 'thumbnail', 'excerpt'),
'show_in_rest' => true, // Enables Gutenberg editor support
);
register_post_type('portfolio', $args);
}
add_action('init', 'register_portfolio_cpt');
After adding this code, go to Settings → Permalinks and save (without changes) to flush the rewrite rules. Your new Portfolio section will appear in the WordPress admin menu.
Key Arguments Explained
| Argument | What It Does |
|---|---|
| public | Makes the CPT visible on the frontend and in admin |
| has_archive | Creates an archive page at /portfolio/ (or your slug) |
| hierarchical | True for page-like structure, false for post-like structure |
| supports | Which WordPress features this CPT uses (title, editor, thumbnail, etc.) |
| show_in_rest | Enables REST API access and Gutenberg editor support |
| rewrite | Defines the URL slug for this CPT |
| menu_icon | Dashicons class or SVG URL for the admin menu icon |
Adding Custom Taxonomies
Just like Posts have Categories and Tags, you can create custom taxonomies for your CPT:
function register_portfolio_taxonomies() {
// Portfolio Category taxonomy
register_taxonomy(
'portfolio_category',
'portfolio',
array(
'label' => 'Portfolio Categories',
'hierarchical' => true, // True = category-like, False = tag-like
'rewrite' => array('slug' => 'portfolio-category'),
'show_in_rest' => true,
)
);
// Portfolio Tags taxonomy
register_taxonomy(
'portfolio_tag',
'portfolio',
array(
'label' => 'Portfolio Tags',
'hierarchical' => false,
'rewrite' => array('slug' => 'portfolio-tag'),
'show_in_rest' => true,
)
);
}
add_action('init', 'register_portfolio_taxonomies');
Adding Custom Meta Boxes
Custom meta boxes let you add structured fields to your CPT — the client name, project URL, completion date, and any other data specific to your content type:
// Register the meta box
function portfolio_meta_boxes() {
add_meta_box(
'portfolio_details',
'Project Details',
'portfolio_meta_box_html',
'portfolio',
'normal',
'high'
);
}
add_action('add_meta_boxes', 'portfolio_meta_boxes');
// Render the meta box fields
function portfolio_meta_box_html($post) {
$client_name = get_post_meta($post->ID, '_portfolio_client', true);
$project_url = get_post_meta($post->ID, '_portfolio_url', true);
$project_year = get_post_meta($post->ID, '_portfolio_year', true);
wp_nonce_field('portfolio_meta_save', 'portfolio_meta_nonce');
?>
<p>
<label for="portfolio_client">Client Name:</label><br>
<input type="text" id="portfolio_client" name="portfolio_client"
value="<?php echo esc_attr($client_name); ?>" style="width:100%">
</p>
<p>
<label for="portfolio_url">Project URL:</label><br>
<input type="url" id="portfolio_url" name="portfolio_url"
value="<?php echo esc_url($project_url); ?>" style="width:100%">
</p>
<p>
<label for="portfolio_year">Year Completed:</label><br>
<input type="number" id="portfolio_year" name="portfolio_year"
value="<?php echo esc_attr($project_year); ?>" style="width:100px">
</p>
<?php
}
// Save the meta box data
function portfolio_meta_save($post_id) {
if (!isset($_POST['portfolio_meta_nonce']) ||
!wp_verify_nonce($_POST['portfolio_meta_nonce'], 'portfolio_meta_save')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
if (!current_user_can('edit_post', $post_id)) return;
update_post_meta($post_id, '_portfolio_client', sanitize_text_field($_POST['portfolio_client']));
update_post_meta($post_id, '_portfolio_url', esc_url_raw($_POST['portfolio_url']));
update_post_meta($post_id, '_portfolio_year', absint($_POST['portfolio_year']));
}
add_action('save_post_portfolio', 'portfolio_meta_save');
Displaying Custom Post Types
To display portfolio items on a page, use WP_Query:
<?php
$portfolio_query = new WP_Query(array(
'post_type' => 'portfolio',
'posts_per_page' => 12,
'tax_query' => array(
array(
'taxonomy' => 'portfolio_category',
'field' => 'slug',
'terms' => 'web-design', // Optional filter
),
),
'orderby' => 'date',
'order' => 'DESC',
));
if ($portfolio_query->have_posts()) {
echo '<div class="portfolio-grid">';
while ($portfolio_query->have_posts()) {
$portfolio_query->the_post();
$client = get_post_meta(get_the_ID(), '_portfolio_client', true);
$url = get_post_meta(get_the_ID(), '_portfolio_url', true);
?>
<div class="portfolio-item">
<?php if (has_post_thumbnail()): ?>
<a href="<?php echo esc_url($url); ?>" target="_blank">
<?php the_post_thumbnail('medium'); ?>
</a>
<?php endif; ?>
<h3><?php the_title(); ?></h3>
<p>Client: <?php echo esc_html($client); ?></p>
</div>
<?php
}
echo '</div>';
wp_reset_postdata();
}
?>
Plugin vs functions.php: Where to Put CPT Code
Always register Custom Post Types in a plugin, not in your theme’s functions.php. Here is why:
If you register a CPT in functions.php and then switch themes, all your CPT content becomes inaccessible (the URLs break and the admin menu disappears). Content and functionality should always be theme-independent.
Create a simple site-specific plugin — a PHP file in wp-content/plugins/my-site-functions/ — and put all your CPT registrations there. It takes 5 minutes to set up and protects your content from theme changes.
Conclusion
Custom Post Types unlock WordPress’s potential as a true content management platform rather than just a blog engine. Whether you are building a portfolio, a product catalog, or a complex directory, CPTs give you the structure, the admin interface, and the query capabilities to manage any type of content cleanly and professionally.
Start with a simple CPT registration, add the taxonomies and meta fields your content needs, and build your display templates. The WordPress ecosystem of plugins and themes that support CPTs is vast — once you understand the fundamentals, you will find it easy to extend.