
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 postspage: Static pagesattachment: Media Library itemsrevision: Saved revisions of posts and pagesnav_menu_item: Items in classic navigation menuswp_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.
- Install the plugin: Go to Plugins > Add New Plugin, search for "Custom Post Type UI", click Install Now, then Activate.
- Add a post type: Go to CPT UI > Add/Edit Post Types.
- 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. - 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.
- 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).
- 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 likepost,page,action,author, ororder. 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/. Settingwith_fronttofalseprevents your permalink base (like/blog/) from being prepended.supports: Which editor features are available. Common options aretitle,editor,author,thumbnail,excerpt,comments,revisions,page-attributes, andcustom-fields.hierarchical: Set totrueif 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.postmeans 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_archiveis 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.phpfor single entries, falling back tosingle.phparchive-sajjad_portfolio.phpfor the archive, falling back toarchive.phptaxonomy-sajjad_project_type.phpfor 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
eventcan 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-replaceor 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.


