Type something to search...
How to create custom post types in WordPress?

How to create custom post types in WordPress?

You can create a custom post type in WordPress in two ways: with a plugin such as Custom Post Type UI, Advanced Custom Fields (ACF), or Pods, which gives you a point-and-click interface, or with code by calling register_post_type() on the init hook in a custom plugin. Either way, you get a new content type, such as "Portfolio", "Events", or "Testimonials", with its own menu in the admin, its own archive, and its own templates.

Out of the box, WordPress gives you posts and pages. That's enough for a simple blog, but once your site includes products, projects, recipes, or team members, cramming everything into posts gets messy. Custom post types (CPTs) let you model your content properly. This guide explains what custom post types are, when to use them, how to create them with a plugin and in code, how to add custom taxonomies, how to display CPTs on the front end, and how to avoid common mistakes.

What Is a Custom Post Type?

In WordPress, every piece of content is stored in the wp_posts database table and has a post type that tells WordPress what kind of content it is. WordPress includes several built-in post types:

  • post: Blog posts
  • page: Static pages
  • attachment: Media Library items
  • revision: Saved revisions of posts and pages
  • nav_menu_item: Items in classic navigation menus
  • wp_block, wp_template, wp_template_part, wp_navigation: Patterns and Site Editor content

A custom post type is any additional type you register. It behaves like posts or pages but is kept separate, with its own labels, settings, and URLs.

Examples of Custom Post Types

  • Portfolio projects for a designer or agency
  • Events with dates and venues
  • Testimonials from clients
  • Team members with photos and job titles
  • Recipes, properties, courses, or job listings

Plugins use CPTs too. WooCommerce stores products as a product post type, and many form and LMS plugins register their own.

When Should You Use a Custom Post Type?

A custom post type is a good fit when:

  • The content has a different structure from blog posts (for example, events need dates and locations).
  • You want it separate from your blog feed and archive.
  • It needs its own templates or URL structure, like /portfolio/project-name/.
  • It will have many entries that editors need to manage independently.

If you just need to group blog posts by topic, use categories and tags instead. A CPT is about a different kind of content, not a different subject.

Method 1: Create a Custom Post Type With a Plugin

Plugins are the fastest way to get started and are great for non-developers or for prototyping.

Using Custom Post Type UI

Custom Post Type UI (CPT UI) is a free, long-standing plugin focused solely on registering post types and taxonomies.

  1. Install the plugin: Go to Plugins > Add New Plugin, search for "Custom Post Type UI", click Install Now, then Activate.
  2. Add a post type: Go to CPT UI > Add/Edit Post Types.
  3. Set the slug: Enter a lowercase slug with no spaces, such as portfolio. This becomes part of the URL and is stored in the database, so choose carefully.
  4. Add labels: Enter the Plural Label ("Projects") and Singular Label ("Project"). Click Populate additional labels based on chosen labels to fill in the rest automatically.
  5. Adjust settings: Scroll through the settings. Important ones include Has Archive (set to true for a listing page), Show in REST API (must be true for the block editor), and Supports (title, editor, thumbnail, excerpt, and so on).
  6. Save: Click Add Post Type.

A new "Projects" menu appears in your dashboard. CPT UI also offers a Tools > Get Code screen that shows the equivalent PHP, which is handy if you later want to move the registration into code.

Using ACF or Pods

  • Advanced Custom Fields (ACF): Since version 6.1, ACF can register post types and taxonomies directly from ACF > Post Types, alongside its custom fields.
  • Pods: A free framework for creating post types, taxonomies, and custom fields, with relationship fields and templating built in.

These are good choices if you'll also need custom fields for your content type, which is very common.

A Note About Plugin-Based CPTs

If you deactivate the plugin that registers a post type, the content stays in the database, but WordPress no longer knows about the post type, so it disappears from the admin and front end. Reactivating the plugin (or registering the same slug in code) brings it back.

Method 2: Create a Custom Post Type With Code

Registering a CPT in code gives you full control, keeps your site lean, and makes the setup version-controllable.

Where Should the Code Go?

Put CPT registration in a custom plugin (or a must-use plugin), not in your theme's functions.php. Content types are about your data, not your design. If the code lives in the theme and you switch themes, your content types vanish from the admin.

Create a folder wp-content/plugins/sajjad-portfolio/ with a file named sajjad-portfolio.php.

A Complete Example

<?php
/**
 * Plugin Name: Sajjad Portfolio
 * Description: Registers a Portfolio custom post type and Project Type taxonomy.
 * Version: 1.0.0
 * Author: Sajjad
 * Text Domain: sajjad-portfolio
 */

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

function sajjad_register_portfolio_post_type() {
    $labels = array(
        'name'                  => _x( 'Projects', 'post type general name', 'sajjad-portfolio' ),
        'singular_name'         => _x( 'Project', 'post type singular name', 'sajjad-portfolio' ),
        'menu_name'             => _x( 'Portfolio', 'admin menu', 'sajjad-portfolio' ),
        'add_new'               => __( 'Add New', 'sajjad-portfolio' ),
        'add_new_item'          => __( 'Add New Project', 'sajjad-portfolio' ),
        'edit_item'             => __( 'Edit Project', 'sajjad-portfolio' ),
        'new_item'              => __( 'New Project', 'sajjad-portfolio' ),
        'view_item'             => __( 'View Project', 'sajjad-portfolio' ),
        'view_items'            => __( 'View Projects', 'sajjad-portfolio' ),
        'search_items'          => __( 'Search Projects', 'sajjad-portfolio' ),
        'not_found'             => __( 'No projects found.', 'sajjad-portfolio' ),
        'not_found_in_trash'    => __( 'No projects found in Trash.', 'sajjad-portfolio' ),
        'all_items'             => __( 'All Projects', 'sajjad-portfolio' ),
        'archives'              => __( 'Project Archives', 'sajjad-portfolio' ),
        'featured_image'        => __( 'Project Image', 'sajjad-portfolio' ),
        'set_featured_image'    => __( 'Set project image', 'sajjad-portfolio' ),
        'remove_featured_image' => __( 'Remove project image', 'sajjad-portfolio' ),
        'use_featured_image'    => __( 'Use as project image', 'sajjad-portfolio' ),
    );

    $args = array(
        'labels'             => $labels,
        'public'             => true,
        'show_in_rest'       => true,
        'menu_icon'          => 'dashicons-portfolio',
        'menu_position'      => 5,
        'has_archive'        => true,
        'rewrite'            => array(
            'slug'       => 'portfolio',
            'with_front' => false,
        ),
        'supports'           => array( 'title', 'editor', 'thumbnail', 'excerpt', 'revisions', 'custom-fields' ),
        'taxonomies'         => array(),
        'capability_type'    => 'post',
        'hierarchical'       => false,
    );

    register_post_type( 'sajjad_portfolio', $args );
}
add_action( 'init', 'sajjad_register_portfolio_post_type' );

Activate it under Plugins, and you'll see a new Portfolio menu in the dashboard.

Understanding the Key Arguments

  • Post type key (sajjad_portfolio): Must be 20 characters or fewer, lowercase, with only letters, numbers, dashes, and underscores. Prefix it to avoid clashes with other plugins. Avoid reserved names like post, page, action, author, or order.
  • public: Makes the post type visible on the front end and in the admin. It sets sensible defaults for several other visibility arguments.
  • show_in_rest: Required for the block editor and for REST API access. Without it, the post type falls back to the classic editor.
  • has_archive: Creates an archive page at /portfolio/. You can also pass a string to use a different archive slug.
  • rewrite: Controls the URL. 'slug' => 'portfolio' gives you /portfolio/project-name/. Setting with_front to false prevents your permalink base (like /blog/) from being prepended.
  • supports: Which editor features are available. Common options are title, editor, author, thumbnail, excerpt, comments, revisions, page-attributes, and custom-fields.
  • hierarchical: Set to true if entries should have parents and children like pages.
  • menu_icon: Any Dashicons class or a URL to an SVG icon.
  • capability_type: Defines which permissions control the post type. post means it uses the same capabilities as regular posts.

Flush Rewrite Rules

After registering a new CPT, visit Settings > Permalinks and click Save Changes. This flushes WordPress's rewrite rules so your new URLs work. Without it, you'll often see 404 errors on your new post type.

In a plugin, you can do this automatically on activation:

function sajjad_portfolio_activate() {
    sajjad_register_portfolio_post_type();
    flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'sajjad_portfolio_activate' );

function sajjad_portfolio_deactivate() {
    unregister_post_type( 'sajjad_portfolio' );
    flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'sajjad_portfolio_deactivate' );

Never call flush_rewrite_rules() on every page load; it's an expensive operation meant for activation and deactivation only.

Adding a Custom Taxonomy

Custom post types often need their own way of grouping content. For a portfolio, that might be a "Project Type" taxonomy with terms like "Branding" and "Web Design". Add this to the same plugin:

function sajjad_register_project_type_taxonomy() {
    $labels = array(
        'name'          => _x( 'Project Types', 'taxonomy general name', 'sajjad-portfolio' ),
        'singular_name' => _x( 'Project Type', 'taxonomy singular name', 'sajjad-portfolio' ),
        'search_items'  => __( 'Search Project Types', 'sajjad-portfolio' ),
        'all_items'     => __( 'All Project Types', 'sajjad-portfolio' ),
        'parent_item'   => __( 'Parent Project Type', 'sajjad-portfolio' ),
        'edit_item'     => __( 'Edit Project Type', 'sajjad-portfolio' ),
        'update_item'   => __( 'Update Project Type', 'sajjad-portfolio' ),
        'add_new_item'  => __( 'Add New Project Type', 'sajjad-portfolio' ),
        'new_item_name' => __( 'New Project Type Name', 'sajjad-portfolio' ),
        'menu_name'     => __( 'Project Types', 'sajjad-portfolio' ),
    );

    register_taxonomy(
        'sajjad_project_type',
        array( 'sajjad_portfolio' ),
        array(
            'labels'            => $labels,
            'hierarchical'      => true,
            'public'            => true,
            'show_in_rest'      => true,
            'show_admin_column' => true,
            'rewrite'           => array( 'slug' => 'project-type' ),
        )
    );
}
add_action( 'init', 'sajjad_register_project_type_taxonomy' );

Setting hierarchical to true makes it behave like categories; false makes it behave like tags. show_admin_column adds a column to the projects list screen. If you add the taxonomy after the plugin is already active, remember to flush rewrite rules again by saving permalinks.

You can also attach the built-in categories and tags to your CPT by listing 'category' and 'post_tag' in the post type's taxonomies argument.

Displaying Custom Post Types on Your Site

Once registered, your CPT has URLs out of the box:

  • Single entries: /portfolio/project-name/
  • Archive (if has_archive is true): /portfolio/
  • Taxonomy archives: /project-type/branding/

Templates in Block Themes

In a block theme, go to Appearance > Editor > Templates and click Add New Template. WordPress offers options such as Single item: Project and Archive: Projects. Choose one and design it with blocks like Post Title, Post Featured Image, Post Content, and Query Loop.

You can also ship templates in your theme's templates folder using the template hierarchy names, such as single-sajjad_portfolio.html and archive-sajjad_portfolio.html.

Templates in Classic Themes

In a classic theme, WordPress follows the template hierarchy:

  • single-sajjad_portfolio.php for single entries, falling back to single.php
  • archive-sajjad_portfolio.php for the archive, falling back to archive.php
  • taxonomy-sajjad_project_type.php for the taxonomy archive

A minimal archive-sajjad_portfolio.php might look like this:

<?php get_header(); ?>

<main id="primary" class="site-main">
    <h1><?php post_type_archive_title(); ?></h1>

    <?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' ); ?>>
                    <a href="<?php the_permalink(); ?>">
                        <?php the_post_thumbnail( 'medium_large' ); ?>
                        <h2><?php the_title(); ?></h2>
                    </a>
                    <?php the_excerpt(); ?>
                </article>
            <?php endwhile; ?>
        </div>
        <?php the_posts_pagination(); ?>
    <?php else : ?>
        <p><?php esc_html_e( 'No projects yet.', 'sajjad-portfolio' ); ?></p>
    <?php endif; ?>
</main>

<?php get_footer(); ?>

Put theme templates in a child theme if you're customizing someone else's theme, so updates don't overwrite them.

Displaying CPT Entries Anywhere

With blocks, add a Query Loop block to any page, open its settings, and change Post type to your CPT. You can filter by taxonomy, set the number of items, and choose a layout.

In PHP, use WP_Query:

<?php
$sajjad_projects = new WP_Query(
    array(
        'post_type'      => 'sajjad_portfolio',
        'posts_per_page' => 6,
        'tax_query'      => array(
            array(
                'taxonomy' => 'sajjad_project_type',
                'field'    => 'slug',
                'terms'    => 'web-design',
            ),
        ),
    )
);

if ( $sajjad_projects->have_posts() ) {
    echo '<ul class="latest-projects">';
    while ( $sajjad_projects->have_posts() ) {
        $sajjad_projects->the_post();
        printf(
            '<li><a href="%s">%s</a></li>',
            esc_url( get_permalink() ),
            esc_html( get_the_title() )
        );
    }
    echo '</ul>';
    wp_reset_postdata();
}
?>

Always call wp_reset_postdata() after a custom loop so the main query's post data is restored.

Including CPTs in the Main Blog Feed

By default, custom post types don't appear on your blog homepage. If you want them mixed in:

function sajjad_add_portfolio_to_home( $query ) {
    if ( ! is_admin() && $query->is_main_query() && $query->is_home() ) {
        $query->set( 'post_type', array( 'post', 'sajjad_portfolio' ) );
    }
}
add_action( 'pre_get_posts', 'sajjad_add_portfolio_to_home' );

Using Custom Post Types With the REST API

Because show_in_rest is true, your CPT is available at /wp-json/wp/v2/sajjad_portfolio. You can customize the endpoint name with the rest_base argument, for example 'rest_base' => 'projects' to get /wp-json/wp/v2/projects. This makes CPTs a solid foundation for headless WordPress setups and JavaScript front ends.

Adding Custom Fields to Custom Post Types

Most CPTs need extra data, such as a client name, project URL, or event date. You can add this with custom fields, either through a plugin like ACF or with register_post_meta() in code. Our guide to custom fields in WordPress covers this in detail.

A quick example that registers a "client name" field and exposes it to the block editor and REST API:

function sajjad_register_portfolio_meta() {
    register_post_meta(
        'sajjad_portfolio',
        'sajjad_client_name',
        array(
            'type'              => 'string',
            'single'            => true,
            'show_in_rest'      => true,
            'sanitize_callback' => 'sanitize_text_field',
            'auth_callback'     => function () {
                return current_user_can( 'edit_posts' );
            },
        )
    );
}
add_action( 'init', 'sajjad_register_portfolio_meta' );

Common Mistakes to Avoid

  • Registering CPTs in the theme: Put them in a plugin so they survive theme changes.
  • Forgetting to flush permalinks: This is the number one cause of 404 errors on new post types.
  • Unprefixed names: A generic key like event can clash with another plugin. Use a prefix.
  • Changing the post type key later: Existing content is stored with the old key. Renaming requires a database update, for example with WP-CLI's wp search-replace or a SQL query, so choose the key carefully from the start.
  • Slug conflicts: If a page and a CPT archive share the same slug (like a page called "Portfolio" at /portfolio/), one will override the other.
  • Leaving out show_in_rest: Without it, you won't get the block editor.
  • Overusing CPTs: If the content is really just a blog post on a certain topic, a category is simpler.

FAQ: Custom Post Types in WordPress

A custom post type is an additional content type, such as Portfolio or Events, that you register alongside the built-in posts and pages. It has its own admin menu, URLs, and templates.

Plugins like Custom Post Type UI or ACF are quick and beginner-friendly. Code gives more control and fewer dependencies. Both produce the same result, since plugins call register_post_type() behind the scenes.

Rewrite rules usually need to be flushed. Go to Settings > Permalinks and click Save Changes without changing anything.

The post type must be registered with show_in_rest set to true and must support editor. Without REST support, WordPress uses the classic editor.

No. The content stays in the database, but it becomes hidden until the same post type is registered again, either by reactivating the plugin or in code.

It's better to put them in a custom plugin. Code in functions.php stops running when you switch themes, which hides your content types.

Yes. Add category and post_tag to the taxonomies argument, or register your own custom taxonomy with register_taxonomy().


Conclusion

Custom post types let you shape WordPress around your content instead of forcing everything into posts and pages. For a quick setup, a plugin like Custom Post Type UI, ACF, or Pods does the job with no code. For full control, register_post_type() in a small custom plugin gives you a clean, portable, version-controlled content model, and register_taxonomy() adds the grouping your new content needs.

Whichever route you take, plan your post type key and URL slug carefully, enable show_in_rest for block editor support, and remember to flush permalinks after registering. Then build your single and archive templates in the Site Editor or your theme, and you'll have a structured, easy-to-manage section of your site that feels like a natural part of WordPress.

Tags :
Share :

Related Posts

What are the best WordPress security plugins?

What are the best WordPress security plugins?

The best WordPress security plugins for most sites are Wordfence, Sucuri Security, Solid Security, MalCare, All-In-One Security (AIOS), Patchstack, a

Dive Deeper
What is the difference between posts and pages in WordPress?

What is the difference between posts and pages in WordPress?

The main difference between posts and pages in WordPress is that posts are timely, dated entries that appear in your blog feed, archives, and RSS fee

Dive Deeper
How much does it cost to build a WordPress website?

How much does it cost to build a WordPress website?

Building a WordPress website can cost anywhere from roughly the price of a domain and a year of budget hosting, if you do it yourself with free tools

Dive Deeper