Contact Us

Custom post types are the backbone of any serious WordPress development project. They transform WordPress from a simple blogging platform into a full-featured content management system.

Want to build a portfolio? You need a “Portfolio” post type.
Building a real estate site? You need a “Properties” post type.
Creating an event calendar? You need an “Events” post type.

If you haven’t created your main plugin file yet, make sure to read my step-by-step guide on how to create a WordPress plugin main file and understand the plugin header properly before continuing.

Here’s the truth: If you can’t create custom post types, you can’t call yourself a WordPress developer. It’s that fundamental.

In this tutorial, you’ll master register_post_type()—the WordPress function that makes all of this possible. You’ll build a complete “Portfolio” post type with featured images, custom URLs, and Gutenberg support.

And you’ll do it inside your plugin, following WordPress coding standards and best practices.

What We’re Building Today

Remember our WP Creative Portfolio Pro plugin? Today we’re adding the core data structure:

text
wp-creative-portfolio-pro/
├── wp-creative-portfolio.php (already done)
├── includes/
│   ├── class-wp-creative-portfolio.php (← YOU ARE HERE)
│   └── class-wp-creative-portfolio-settings.php (future)
├── assets/
└── languages/

By the end of this tutorial, you’ll have:
✅ A fully functional “Portfolio” custom post type
✅ Featured image support for portfolio items
✅ Custom URL structure (yoursite.com/portfolio/project-name/)
✅ Gutenberg block editor compatibility
✅ Admin menu with portfolio icon

[IMAGE PLACEHOLDER: WordPress admin showing Portfolio menu item and Add New screen]
Figure 1: Your finished Portfolio custom post type in WordPress admin

What Exactly is a Custom Post Type?

Let’s get technical for a moment.

In WordPress, everything is a “post” under the hood. Regular blog posts are stored in the database as post_type = 'post'. Pages are post_type = 'page'. Attachments are post_type = 'attachment'.

A custom post type is simply a new post_type value that you define. Instead of ‘post’ or ‘page’, your portfolio items will have post_type = 'wp_portfolio'.

This separation gives you:

  • Dedicated admin screens – Portfolio items don’t mix with blog posts

  • Custom URLs – Like /portfolio/web-design-project/

  • Separate templates – Theme developers can create single-wp_portfolio.php

  • Different capabilities – Control who can edit portfolios vs blog posts

The Complete Custom Post Type Code

Create includes/class-wp-creative-portfolio.php and add this complete code:

php
<?php
/**
 * Main portfolio functionality class
 *
 * @package WP_Creative_Portfolio
 */

// Prevent direct access
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class WP_Creative_Portfolio {

    /**
     * Constructor - hooks into WordPress
     */
    public function __construct() {
        // Register Custom Post Type on WordPress init
        add_action( 'init', array( $this, 'register_portfolio_cpt' ) );
        
        // We'll add taxonomy registration here in Video/Post 4
        // add_action( 'init', array( $this, 'register_portfolio_taxonomy' ) );
        
        // We'll add asset registration here in Video/Post 5
        // add_action( 'wp_enqueue_scripts', array( $this, 'register_assets' ) );
        
        // We'll add shortcode here in Video/Post 6
        // add_shortcode( 'wp_creative_portfolio', array( $this, 'render_portfolio' ) );
    }

    /**
     * Register Portfolio Custom Post Type
     */
    public function register_portfolio_cpt() {

        // Get settings from database (or use defaults)
        $options  = get_option( 'wp_creative_portfolio_options' );
        $plural   = ! empty( $options['plural_name'] ) ? $options['plural_name'] : 'Portfolios';
        $singular = ! empty( $options['singular_name'] ) ? $options['singular_name'] : 'Portfolio';
        $slug     = ! empty( $options['slug'] ) ? $options['slug'] : 'portfolio';

        // Labels control how the post type appears in admin
        $labels = array(
            'name'               => $plural,
            'singular_name'      => $singular,
            'add_new'            => 'Add New',
            'add_new_item'       => 'Add New ' . $singular,
            'edit_item'          => 'Edit ' . $singular,
            'new_item'           => 'New ' . $singular,
            'view_item'          => 'View ' . $singular,
            'search_items'       => 'Search ' . $plural,
            'not_found'          => 'No ' . $plural . ' found',
            'not_found_in_trash' => 'No ' . $plural . ' found in Trash',
            'all_items'          => 'All ' . $plural,
            'menu_name'          => $plural,
            'name_admin_bar'     => $singular,
        );

        // Arguments define how the post type behaves
        $args = array(
            'labels'              => $labels,
            'public'              => true,
            'publicly_queryable'  => true,
            'show_ui'             => true,
            'show_in_menu'        => true,
            'query_var'           => true,
            'rewrite'             => array(
                'slug'       => $slug,
                'with_front' => true,
                'pages'      => true,
                'feeds'      => true,
            ),
            'capability_type'     => 'post',
            'has_archive'         => true,
            'hierarchical'        => false,
            'menu_position'       => 20,
            'menu_icon'           => 'dashicons-portfolio',
            'supports'            => array(
                'title',
                'editor',
                'thumbnail',
                'excerpt',
                'custom-fields',
                'revisions',
            ),
            'show_in_rest'        => true, // Enables Gutenberg
            'rest_base'           => 'portfolio',
            'rest_controller_class' => 'WP_REST_Posts_Controller',
        );

        // Register the post type
        register_post_type( 'wp_portfolio', $args );

        // Optional: Flush rewrite rules on next page load
        // (Handled by activation hook in main file)
    }
}
Your portfolio class file with all the register_post_type parameters
Your portfolio class file with all the register_post_type parameters

Deep Dive: Understanding register_post_type()

The register_post_type() function takes two parameters:

  1. Post type name – 'wp_portfolio' (must be unique, lowercase, underscores)

  2. Arguments array – Configuration options

Let’s examine every argument because this is where 90% of developers make mistakes.

The Labels Array (Lines 32-49)

Labels control what users see in the WordPress admin:

php
$labels = array(
    'name'               => $plural,           // "Portfolios"
    'singular_name'      => $singular,         // "Portfolio"
    'add_new'            => 'Add New',         // Button text
    'add_new_item'       => 'Add New Portfolio', // Page title
    'edit_item'          => 'Edit Portfolio',  // Edit screen title
    'new_item'           => 'New Portfolio',   // New item title
    'view_item'          => 'View Portfolio',  // View link text
    'search_items'       => 'Search Portfolios', // Search button
    'not_found'          => 'No portfolios found', // Empty state
    'not_found_in_trash' => 'No portfolios found in Trash',
    'all_items'          => 'All Portfolios',  // Menu item
    'menu_name'          => $plural,           // Menu display name
    'name_admin_bar'     => $singular,         // Admin bar name
);

Why this matters: Clear labels make your plugin user-friendly. Imagine if “Add New” said “Add New Portfolio” instead of just “Add New”—much clearer!

Pro tip: Notice we’re using $plural and $singular from settings. This lets users customize the names without touching code. We’ll build that settings page in Video/Post 10.

Critical Arguments Explained

public (Line 52)

php
'public' => true,

Setting this to true automatically enables several features:

  • Visible in admin UI

  • Queryable on frontend

  • Has individual permalinks

  • Included in site searches

Setting it to false hides everything. Useful for internal data.

rewrite (Lines 55-60)

php
'rewrite' => array(
    'slug'       => $slug,       // URL slug: /portfolio/
    'with_front' => true,        // Respect permalink structure
    'pages'      => true,        // Enable pagination: /portfolio/page/2/
    'feeds'      => true,        // Enable RSS feeds
),

This controls your URL structure. With slug => 'portfolio':

  • Archive: yoursite.com/portfolio/

  • Single item: yoursite.com/portfolio/project-name/

  • Category: yoursite.com/portfolio-category/design/ (we’ll add this in Video 4)

has_archive (Line 63)

php
'has_archive' => true,

Creates an archive page listing all portfolio items. If you set this to a string like 'work', the archive becomes yoursite.com/work/.

menu_icon (Line 67)

php
'menu_icon' => 'dashicons-portfolio',

Uses WordPress Dashicons. Find all available icons at developer.wordpress.org/resource/dashicons.

Common portfolio icons:

  • dashicons-portfolio

  • dashicons-images-alt2

  • dashicons-layout

  • dashicons-schedule

supports (Lines 68-75)

php
'supports' => array(
    'title',        // Project title
    'editor',       // Description/content
    'thumbnail',    // Featured image
    'excerpt',      // Short description
    'custom-fields', // Meta boxes
    'revisions',    // Version history
),

Each support option adds a feature to your post editor:

  • title: Required. Without it, how would you name projects?

  • editor: The main content area. Great for project descriptions.

  • thumbnail: Featured images. Essential for portfolios!

  • excerpt: Short summaries for grid views.

  • custom-fields: For adding metadata like client name, project date.

  • revisions: Tracks changes. Users expect this.

show_in_rest (Line 76)

php
'show_in_rest' => true,

This single line enables:

  • Gutenberg block editor support

  • REST API access (/wp-json/wp/v2/portfolio)

  • Future block editor features

Without this, your users are stuck with the classic editor.

Full post register code looks like this at the moment
Full post register code looks like this at the moment

Why We’re Using a Class

You might wonder why we wrapped everything in a class. Three reasons:

1. Namespace Protection

php
class WP_Creative_Portfolio {
    public function register_portfolio_cpt() { }
}

Without a class, register_portfolio_cpt() would be a global function. If another plugin uses the same function name, your site crashes.

2. Organization
All portfolio-related methods stay in one place. When you need to fix something, you know exactly where to look.

3. Reusability
You can create multiple instances if needed. Not relevant here, but good practice.

Connecting to the Main Plugin File

Remember our main plugin file includes this class:

php
require_once WPCP_PLUGIN_DIR . 'includes/class-wp-creative-portfolio.php';

And runs it:

php
function run_wp_creative_portfolio_pro() {
    new WP_Creative_Portfolio(); // This calls __construct()
}

The constructor hooks register_portfolio_cpt() to init, so WordPress runs it at the right time.

Testing Your Custom Post Type

Follow these steps to verify everything works:

Step 1: Create the Class File

Create includes/class-wp-creative-portfolio.php and add the complete code above.

Step 2: Check for Errors

  • Go to Plugins → Installed Plugins

  • Your plugin should still be active

  • No error messages? Good!

Step 3: Look for the Menu

[IMAGE PLACEHOLDER: WordPress admin menu highlighting the new Portfolio item]
Figure 3: Your Portfolio menu appears in the left admin sidebar

Look in your WordPress admin sidebar. You should see “Portfolios” with a portfolio icon.

Step 4: Create a Portfolio Item

  1. Click “Portfolios” → “Add New”

  2. Enter a title: “My First Project”

  3. Add some description text

  4. Set a featured image (Featured Image box in right sidebar)

  5. Click “Publish”

Portfolio post showing in the backend WordPress menu
Portfolio post showing in the backend WordPress menu

Step 5: View on Frontend

  • Visit yoursite.com/portfolio/my-first-project/

  • You should see your portfolio item

  • Visit yoursite.com/portfolio/

  • You should see an archive listing

Step 6: Check Gutenberg Compatibility

Edit your portfolio item. If you see the block editor, show_in_rest => true is working!

Common Problems and Solutions

Problem: 404 Error When Viewing Portfolio

Solution: Go to Settings → Permalinks and click “Save Changes”. This flushes rewrite rules manually. Our activation hook should handle this, but manual flush confirms the issue.

Problem: Portfolio Menu Not Showing

Solution: Check these in order:

  1. Is the class file included? (Check main plugin file)

  2. Is the constructor hooking to init?

  3. Any PHP errors? Enable WP_DEBUG.

Problem: Featured Image Not Showing

Solution: Did you add ‘thumbnail’ to supports array? And did you set a featured image in the post?

Problem: Gutenberg Not Working

Solution: Verify 'show_in_rest' => true is set. Some hosts disable the REST API—check with your hosting provider.

Advanced: Custom Post Type Parameters You Might Need Later

Here are additional arguments you might find useful:

php
$args = array(
    // ... other args ...
    
    // Custom capabilities (for user role management)
    'capability_type' => 'portfolio',
    'map_meta_cap'    => true,
    
    // Exclude from search
    'exclude_from_search' => false,
    
    // Custom template hierarchy
    'template' => array(
        array( 'core/paragraph', array(
            'placeholder' => 'Add project description...',
        ) ),
    ),
    
    // Available in navigation menus
    'show_in_nav_menus' => true,
    
    // Delete with post (for attachments, etc.)
    'delete_with_user' => false,
);

Performance Considerations

Custom post types don’t slow down WordPress. They’re just database entries. However:

  • Register early: Hook to init (priority 10) is fine

  • Don’t query unnecessarily: We’ll use proper WP_Query in Video 7

  • Use transients for complex queries: Future optimization

The Settings Connection

Notice we used:

php
$options = get_option( 'wp_creative_portfolio_options' );
$plural = ! empty( $options['plural_name'] ) ? $options['plural_name'] : 'Portfolios';

This reads from our settings (which don’t exist yet). In Video 10, we’ll build the settings page that lets users change:

  • “Portfolios” to “Projects” or “Work”

  • “Portfolio” to “Project” or “Case Study”

  • URLs from /portfolio/ to /work/

This makes your plugin flexible without coding!

What We’ve Accomplished

Today you:

✅ Created your first WordPress class file
✅ Registered a custom post type with proper labels
✅ Configured URL structure with custom slug
✅ Added featured image support
✅ Enabled Gutenberg compatibility
✅ Connected everything to your main plugin

Your plugin now has a place to store portfolio data. In Video 4, we’ll add categories to organize that data.

Frequently Asked Questions

Can I change the post type name after launching?

Technically yes, but it’s dangerous. Changing ‘wp_portfolio’ to something else loses all existing data. Choose carefully and stick with it.

Why use ‘wp_portfolio’ instead of just ‘portfolio’?

Prefixing with ‘wp_’ reduces chance of conflicts. If another plugin uses ‘portfolio’, your site breaks. Always prefix custom post type names.

How many custom post types can I have?

No technical limit. But each adds admin menu items. Keep it reasonable for user experience.

Will this work with any theme?

Yes! The post type exists in database regardless of theme. Display depends on theme templates, but your shortcode (Video 6) will handle display regardless of theme.

Can I add custom fields to portfolio items?

Yes! With ‘custom-fields’ in supports array, users can add custom fields manually. For better UX, you’d use Advanced Custom Fields or build custom meta boxes (advanced topic).

Complete Code Reference

Here’s your complete class-wp-creative-portfolio.php file:

php
<?php
/**
 * Main portfolio functionality class
 *
 * @package WP_Creative_Portfolio
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class WP_Creative_Portfolio {

    /**
     * Constructor
     */
    public function __construct() {
        add_action( 'init', array( $this, 'register_portfolio_cpt' ) );
    }

    /**
     * Register Portfolio Custom Post Type
     */
    public function register_portfolio_cpt() {

        $options  = get_option( 'wp_creative_portfolio_options' );
        $plural   = ! empty( $options['plural_name'] ) ? $options['plural_name'] : 'Portfolios';
        $singular = ! empty( $options['singular_name'] ) ? $options['singular_name'] : 'Portfolio';
        $slug     = ! empty( $options['slug'] ) ? $options['slug'] : 'portfolio';

        $labels = array(
            'name'               => $plural,
            'singular_name'      => $singular,
            'add_new'            => 'Add New',
            'add_new_item'       => 'Add New ' . $singular,
            'edit_item'          => 'Edit ' . $singular,
            'new_item'           => 'New ' . $singular,
            'view_item'          => 'View ' . $singular,
            'search_items'       => 'Search ' . $plural,
            'not_found'          => 'No ' . $plural . ' found',
            'not_found_in_trash' => 'No ' . $plural . ' found in Trash',
            'all_items'          => 'All ' . $plural,
            'menu_name'          => $plural,
            'name_admin_bar'     => $singular,
        );

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

        register_post_type( 'wp_portfolio', $args );
    }
}

Next Steps

Your portfolio plugin now has a home for data. In the next tutorial, you’ll learn:

Video/Post 4: How to Create Custom Taxonomy in WordPress Plugin

  • Adding categories to organize portfolio items

  • Understanding hierarchical vs non-hierarchical taxonomies

  • Connecting taxonomies to post types

  • URL structure for taxonomy archives

How to Create Custom Taxonomy (Coming soon)

Troubleshooting Checklist

If something isn’t working, go through this checklist:

  • Class file exists in correct location

  • Main plugin file includes the class

  • Constructor hooks to ‘init’ action

  • No PHP errors (enable WP_DEBUG)

  • Flushed rewrite rules (Settings → Permalinks)

  • Created at least one portfolio item

  • Checked frontend URLs

Share Your Progress!

Built your first custom post type? Share your success in the comments! Having issues? Drop your question below with:

  • What you expected to happen

  • What actually happened

  • Any error messages

I personally read and respond to every comment.

Coming up next (Video/Post 4): How to Create a Custom Taxonomy in a WordPress Plugin — where you’ll learn how to structure and organize your custom post types the right way. A full in-depth video walkthrough is also coming to give you complete clarity.

WordPress Core Contributor | Plugin Developer | Educator

Akram Ul Haq is a WordPress core contributor, WordPress.org plugin author, and official translator with 10+ years of development experience. He has created premium plugins on CodeCanyon and professional themes for ThemeForest, along with custom WordPress solutions for businesses worldwide. At WPThrill, he teaches WordPress development, SEO structure, and performance optimization through practical, implementation-focused tutorial series.

Leave a Reply

Your email address will not be published. Required fields are marked *

Subscribe To Our Newsletter & Get Latest Updates.

Copyright @ 2025 WPThrill.com. All Rights Reserved.