What Is a Custom Post Type
A WordPress custom post type is a content type you define yourself, separate from the built in posts and pages, so you can manage a specific kind of content on its own terms. Out of the box WordPress ships with a handful of content types you already use without thinking about them: posts for your blog, pages for static content like About and Contact, plus a few behind the scenes types for attachments, navigation menus, and revisions. A custom post type adds a brand new kind of content to that list, with its own admin menu, its own editing screen, and its own place in your database.
The reason this matters is organisation and control. Imagine you run a design studio and you want a portfolio. You could publish each project as a normal blog post, but then your projects and your articles get mixed together in the same list, share the same categories, and use the same template. That gets messy fast. A Portfolio custom post type gives projects their own menu item in the dashboard, their own set of categories, their own URL structure like /portfolio/riverside-cafe/, and their own template so a project page can look completely different from a blog post. The content is cleanly separated, easier to manage, and easier for visitors to browse.
Think of the WordPress dashboard as a filing cabinet. Posts are one drawer, pages are another. A custom post type is you adding a new labelled drawer for a specific kind of paperwork, so it does not end up jammed into a drawer that was never meant to hold it. This guide walks through what custom post types are for, both ways to create one, how to show them on your site, and the traps that catch people. If you would rather have a developer set this up properly, you can get a free quote and we will build it for you.
When to Use a Custom Post Type
Not every piece of content needs its own post type. The test is simple: if the content is meaningfully different from a blog post or a page, has its own set of fields, and you want to manage or display it as a group, a custom post type earns its keep. If it is just another article, a normal post is fine. Reaching for a custom post type when a category would do only adds complexity.
Here are the situations where a custom post type is usually the right call.
- Portfolio or projects. A studio, agency, photographer, or freelancer showing work. Each project has an image gallery, a client name, a year, and a description, and you want a dedicated portfolio archive.
- Testimonials or reviews. Short pieces of content with a person's name, company, and quote, displayed in sliders or grids across the site rather than as blog posts.
- Team members or staff. Each person has a photo, a role, a bio, and social links. A Team post type keeps them tidy and lets you output a staff page automatically.
- Products or a catalogue. If you are not running a full store but want to list products, services, or menu items with their own fields and archive.
- Events. Each event has a date, a location, and a ticket link, and you want an events calendar or upcoming events list.
- Properties, listings, or vehicles. Real estate, rentals, or dealerships where each item has structured details like price band, bedrooms, or mileage.
- Case studies, recipes, courses, FAQs, or knowledge base articles. Any repeating, structured content that deserves its own home.
The common thread is repeating content with its own shape. When you find yourself thinking "these all have the same three or four fields and I want them listed together, but they are not blog posts", that is a custom post type asking to exist. If your project is large or the content type sits at the centre of the site, it is worth planning this properly, which is where a custom WordPress development approach pays off.
Plugin or Code: Which Method
There are two ways to create a custom post type: with a plugin that gives you a form to fill in, or with a few lines of code. Both end up in the same place, a registered post type that behaves the same way. The difference is who does the work and where the definition lives. Neither is wrong, and the right choice depends on who is building the site and how much control you want.
The plugin route is faster to start with and needs no code, which suits site owners and non technical users. You click through a screen, type in some labels, tick a few boxes, and your post type appears. The trade off is that the definition lives inside the plugin's settings, so the post type depends on that plugin staying installed and active.
The code route puts the definition in your theme or a small plugin of your own, which is how most developers do it on client sites. It is more portable, has no ongoing dependency on a third party plugin, and gives you full control over every option. The trade off is that you need to be comfortable editing a PHP file and you should never edit the parent theme directly.
| Consideration | Plugin method | Code method |
|---|---|---|
| Skill needed | None, point and click | Comfortable editing PHP |
| Speed to set up | Very quick | Quick once you have a snippet |
| Dependency | Relies on the plugin staying active | Lives in your code, no third party dependency |
| Control | Common options exposed in a form | Every argument available |
| Portability | Tied to the plugin's data | Move the file, move the post type |
| Best for | Site owners, quick builds, prototyping | Developers, client sites, long term projects |
A practical middle path many people use: prototype with a plugin to get the shape right, then have a developer move the definition into code before launch so there is no lingering dependency. We will cover both methods in full so you can pick.
Method 1: Create a CPT With a Plugin
The most widely used plugin for this is Custom Post Type UI, a free plugin that gives you a form for creating post types and taxonomies without writing code. Here is how the process works, described step by step.
Step 1: Install the plugin
In your dashboard go to Plugins, then Add New, search for Custom Post Type UI, install it, and activate it. A new menu item labelled CPT UI appears in the left hand admin menu. This is where all your post type and taxonomy settings will live.
Step 2: Add a new post type
Open CPT UI, then Add / Edit Post Types. You will see a form. The first field is the post type slug, the internal name WordPress uses, which should be lowercase, short, and use no spaces, for example portfolio. Below that are the plural and singular labels, such as Portfolios and Portfolio, which are the words that appear throughout the admin. There is a handy button that auto fills all the label text for you from those two words, which saves a lot of typing.
Step 3: Set the important options
Scroll down to the Settings section. The options that matter most for a normal, publicly visible post type are these. Set Public to true so the post type is visible on the site and in the admin. Set Has Archive to true if you want an archive page that lists all items, for example a page at /portfolio/. Under Supports, tick the editing features you want the post type to have, such as Title, Editor, Featured Image (called Thumbnail), and Excerpt. Choose a menu icon and a menu position if you like, so your new type gets a recognisable icon in the admin sidebar. Make sure Show in REST is on, which is required for the block editor to work and for the post type to appear in the REST API.
Step 4: Save and check
Click Add Post Type at the bottom. Your new post type appears in the admin menu straight away, with Add New and All Portfolios items, exactly like Posts. Add one to test it. Because the plugin flushes the permalink rules for you when you save, the archive and single URLs usually work immediately, which avoids the 404 trap we cover later.
That is the whole process. In a couple of minutes you have a working custom post type with no code. The one thing to keep in mind is the dependency: if you deactivate Custom Post Type UI, the post type registration disappears and your items, while still safe in the database, stop showing up until it is reactivated or the type is registered again in code. That is the main reason developers often prefer the code method for sites that need to last.
Method 2: Register a CPT With Code
The code method uses a single WordPress function, register_post_type(), called on the init hook. You can place this code in a small custom plugin (the cleaner choice) or in your theme's functions.php file. If you use a theme file, always use a child theme so your changes are not wiped out when the parent theme updates. Better still, put it in a tiny plugin so the post type survives a theme change as well, which is exactly what a small custom WordPress plugin is for.
Here is a complete, working example that registers a Portfolio post type with sensible labels and options.
<?php
function wpd_register_portfolio_cpt() {
$labels = array(
'name' => 'Portfolios',
'singular_name' => 'Portfolio',
'menu_name' => 'Portfolio',
'add_new' => 'Add New',
'add_new_item' => 'Add New Project',
'edit_item' => 'Edit Project',
'new_item' => 'New Project',
'view_item' => 'View Project',
'search_items' => 'Search Projects',
'not_found' => 'No projects found',
'all_items' => 'All Projects',
);
$args = array(
'labels' => $labels,
'public' => true,
'has_archive' => true,
'rewrite' => array( 'slug' => 'portfolio' ),
'menu_position' => 20,
'menu_icon' => 'dashicons-portfolio',
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
'show_in_rest' => true,
);
register_post_type( 'portfolio', $args );
}
add_action( 'init', 'wpd_register_portfolio_cpt' );
Save the file, reload the admin, and a Portfolio menu appears. A few things about this code are worth understanding so you can adapt it. The function is hooked to init, which is the correct time to register post types. The first argument to register_post_type() is the post type key, here portfolio, which must be twenty characters or fewer and should not clash with an existing type. The $args array holds every setting, and the next section explains the ones that trip people up.
Notice the wpd_ prefix on the function name. Prefixing your function names with something unique to you avoids collisions with other code on the site, which is a small habit that prevents mysterious fatal errors down the line.
Understanding the Key Arguments
The $args array is where a custom post type gets its personality. You do not need to set every possible option, because WordPress fills in sensible defaults, but a handful of arguments decide how the type behaves. These are the ones to know.
- public: The master switch. Setting this to true is a shortcut that turns on visibility on the front end, in the admin, and in search. For most content people want to display, true is correct. Set it to false only for internal, hidden data.
- has_archive: When true, WordPress creates an archive page that lists all items, reachable at the post type slug, for example
/portfolio/. Turn this off for types that should only ever appear as single items or inside other pages, like testimonials embedded in a slider. - supports: An array listing which editing features the post type has. Common values are
title,editor(the main content area),thumbnail(the featured image),excerpt,author, andcustom-fields. If your featured image box is missing from the editor, it is almost always becausethumbnailwas left out of this array. - rewrite: Controls the URL structure. Passing
array( 'slug' => 'portfolio' )sets the URL base. This is also where the permalink flushing pitfall lives, covered below. - show_in_rest: Must be true for the block editor (Gutenberg) to load for this post type and for the type to appear in the WordPress REST API. If your custom post type opens in the old classic editor unexpectedly, this is usually why. It also matters for headless WordPress setups that read content over the API.
- menu_position and menu_icon: Cosmetic but useful. Position is a number that decides where the menu item sits in the admin sidebar (5 is under Posts, 20 is under Pages), and the icon accepts a Dashicons class like
dashicons-portfolioso your type is easy to spot. - hierarchical: When true, the post type behaves like pages, with parent and child relationships and a page style editor. When false (the default), it behaves like posts. Most custom types are non hierarchical.
The full list of arguments is long, and the official register_post_type reference documents every one. You rarely need more than the handful above. Get those right and the post type behaves exactly as you expect.
A few more arguments are worth knowing once the basics click. Setting show_in_menu lets you tuck a post type under another menu rather than giving it a top level item, which keeps the admin sidebar tidy when you have several related types. The capability_type argument controls which user roles can edit the content, defaulting to the same permissions as normal posts, and you can point it at a custom set of capabilities when a post type should only be editable by specific roles. The exclude_from_search argument decides whether items appear in your site's search results, which you might turn on for internal content like testimonials that should show in a slider but not clutter search. And menu_icon accepts not just a Dashicons class but also a base64 encoded SVG or a URL to an image file, so a client's brand icon can sit in the admin menu if you want it there.
One habit that saves time later: decide up front whether a post type is public facing content or internal data, because that single decision drives most of the other arguments. Public content wants public true, an archive, search visibility, and templates. Internal data, the kind you read in code but never show directly, often wants public false, no archive, and show_in_rest only if you need it in the editor or API. Being clear about that intent from the start stops you fighting the defaults.
Adding Custom Taxonomies
A taxonomy is a way of grouping content. Categories and tags are the two taxonomies that come with posts. A custom taxonomy does the same job for your custom post type, letting you file and filter items into groups. For a Portfolio type you might add a Project Type taxonomy with terms like Branding, Web, and Print, so visitors can browse projects by kind.
Taxonomies come in two flavours, and the difference matters. A hierarchical taxonomy works like categories, with parent and child terms and tick box selection. A non hierarchical taxonomy works like tags, a flat list you type into. Choose hierarchical for structured groupings and non hierarchical for free form labels.
You register a taxonomy with register_taxonomy(), also on the init hook. Here is a Project Type taxonomy attached to the portfolio post type.
<?php
function wpd_register_project_type_taxonomy() {
$labels = array(
'name' => 'Project Types',
'singular_name' => 'Project Type',
'menu_name' => 'Project Types',
'all_items' => 'All Project Types',
'edit_item' => 'Edit Project Type',
'add_new_item' => 'Add New Project Type',
'search_items' => 'Search Project Types',
);
$args = array(
'labels' => $labels,
'public' => true,
'hierarchical' => true,
'show_admin_column' => true,
'show_in_rest' => true,
'rewrite' => array( 'slug' => 'project-type' ),
);
register_taxonomy( 'project_type', array( 'portfolio' ), $args );
}
add_action( 'init', 'wpd_register_project_type_taxonomy' );
The first argument is the taxonomy key, the second is the post type or types it applies to (you can attach one taxonomy to several post types), and the third is the settings array. Setting show_admin_column to true adds a handy column in the admin list showing each item's terms. As with the post type, show_in_rest true keeps the block editor and the API happy, and the rewrite slug sets the URL base for term archives like /project-type/branding/. If you created your post type with Custom Post Type UI, that same plugin has an Add / Edit Taxonomies screen that does all of this through a form.
Adding Custom Fields
A custom post type gives you the container, but most structured content also needs extra fields. A portfolio project wants a client name, a year, and a project URL. A team member wants a role and social links. An event wants a date and a venue. These extra pieces of data are called custom fields, and the friendly way to add them is a fields plugin.
The most popular is Advanced Custom Fields, usually shortened to ACF. It lets you define a group of fields, choose their types (text, image, date picker, true or false, repeater, and many more), and attach that group to your custom post type. Once set up, the editing screen for each item shows clean, labelled boxes for exactly the data you want, so the person filling it in cannot get it wrong. This is far tidier than the raw custom fields box WordPress includes by default, which is unlabelled and error prone.
The workflow is: install ACF, create a new field group, add your fields, then set the location rule to "Post Type is equal to Portfolio". Now every portfolio item has those fields. To show a field on the front end you call it in your template, for example the_field('client_name') to print a value or get_field('project_url') to fetch one for use in code. Registering the fields to show in REST also makes them available to the API, which matters if you display content with blocks or build a headless front end.
You can register fields in code too, which keeps them version controlled alongside the post type, and on client projects that is often the better long term choice. Whether you use the plugin's interface or code, the goal is the same: give each item the specific fields its content needs, so the content is structured data rather than one undifferentiated blob of text.
It helps to plan the fields before you build them. Sit down with whoever will actually enter the content and list every piece of information a single item carries. For an event that might be a start date, an end date, a venue name, an address, a ticket link, and a short summary. Writing that list first stops you from adding fields piecemeal later, which leaves older entries with gaps. It also reveals which fields should be required, which should have a fixed set of choices, and which need a specific input type like a date picker or an image uploader so the person entering data cannot make a mistake.
Custom fields also open the door to filtering and sorting. Because the values are stored as real data rather than buried in the body text, you can build an archive that sorts events by date, or a properties page that filters by bedrooms, using a query that reads those fields. That is the payoff of structuring content properly: once the data is clean and separated, the ways you can display and organise it multiply. A blog post approach, where the same details are typed into a paragraph, can never do that because the information is not stored as data the site can read.
Displaying Your Custom Post Type
Registering a post type creates the content and the admin screens, but WordPress needs to know how to display it on the front end. By default it falls back to your theme's generic archive and single templates, which often do not suit the new content. To control the look, you add template files named after your post type, and WordPress picks them up automatically through its template hierarchy.
The two templates you will most often create live in your theme (again, use a child theme so updates do not erase them):
| Template file | What it controls | Falls back to |
|---|---|---|
| archive-portfolio.php | The listing page at /portfolio/ showing all projects | archive.php, then index.php |
| single-portfolio.php | An individual project page | single.php, then index.php |
| taxonomy-project_type.php | A term archive, for example all Branding projects | archive.php, then index.php |
The naming pattern is the key idea. WordPress looks for single-{post_type}.php for a single item and archive-{post_type}.php for the archive, where you swap in your post type key. Because the file exists, WordPress uses it instead of the generic template, and you get full control of the layout. Inside these files you write normal template code: the loop, template tags like the_title() and the_content(), and your ACF field calls like the_field('client_name').
If you are on a block theme or a page builder, the approach differs slightly. Block themes let you edit templates for a custom post type in the Site Editor, and popular builders and many themes offer template builders where you design the single and archive layouts without writing PHP. The underlying idea is identical: you are telling WordPress how this specific content type should look. To pull a few items into any page, for example a "recent projects" strip on the home page, you can use a query loop block or a WP_Query in code that asks for posts of your type.
Flushing Permalinks and the 404 Trap
This is the single most common frustration with custom post types, so it gets its own section. You register a post type with code, add an item, click to view it, and get a 404 Not Found page. The post type is fine, the content is saved, but the URL does not resolve. What went wrong?
WordPress caches its URL routing rules, called rewrite rules, for performance. When you add a new post type with an archive and a rewrite slug, those new rules are not active until the cache is refreshed, a step called flushing the permalinks. Until you flush, WordPress does not know that /portfolio/riverside-cafe/ should map to your new type, so it returns a 404.
The fix is simple and you do not need code for it. Go to Settings, then Permalinks, and click Save Changes. You do not have to change anything, because the act of saving that screen flushes the rewrite rules and your custom post type URLs start working immediately. Do this every time you add a new post type or taxonomy in code, or change a rewrite slug.
A word of caution developers should know: it is tempting to call flush_rewrite_rules() inside your init function so it happens automatically, but that is the wrong place, because it runs on every single page load and slows the site down. If you want to flush in code, do it only on plugin activation with register_activation_hook(), not on init. For most people, visiting the Permalinks screen once after registering the type is the easiest and safest fix. Plugins like Custom Post Type UI flush for you when you save, which is why the 404 rarely appears with the plugin method.
Common Pitfalls and How to Avoid Them
Beyond the permalink trap, a handful of mistakes catch people again and again. Knowing them in advance saves a lot of head scratching.
Choosing a reserved or clashing post type key
Your post type key must be unique and must not use one of WordPress's reserved names such as post, page, attachment, revision, or nav_menu_item. It also must be twenty characters or fewer. Using a generic word risks clashing with a plugin that registers the same key. Prefix your keys with something short and specific to the site, and keep them lowercase with underscores or hyphens, no spaces.
Losing your post type when a plugin is deactivated
If you created the type with a plugin and then deactivate that plugin, the registration disappears and your items vanish from the admin. The content is still safe in the database, but nothing displays until the type is registered again. This is the strongest argument for registering post types in code, ideally in a small standalone plugin, so the definition does not depend on a third party staying installed.
Missing features because supports was incomplete
If the featured image box, the excerpt field, or the main editor is missing from your post type's edit screen, the cause is almost always the supports array. Add the feature you need, such as thumbnail for featured images, and it reappears. This one confuses a lot of people because nothing is broken, the feature was simply never switched on.
The block editor will not load
If your custom post type opens in the old classic editor instead of the block editor, set show_in_rest to true. The block editor is built on the REST API and refuses to load for a post type that is not exposed to it.
Editing the parent theme instead of a child theme
If you put your registration code or your templates in the parent theme's files, the next theme update overwrites them and your work is gone. Always use a child theme for template files, or a small plugin for the registration code, so your changes survive updates.
Forgetting the archive is a real page you can link to
With has_archive true, the archive lives at your rewrite slug, but it is not a page in the Pages list, so it will not appear automatically in menus. To add it to navigation, use a custom link in the menu editor pointing at /portfolio/, or your theme may offer the archive as a menu option.
Custom Post Type Checklist
Here is the whole process as a quick checklist you can work through when adding a new custom post type, whichever method you use.
| Step | What to do | Why it matters |
|---|---|---|
| Pick a key | Lowercase, unique, 20 chars or fewer, prefixed | Avoids clashes and reserved names |
| Set labels | Singular and plural, filled through the admin | Clear, correct wording everywhere |
| Choose method | Plugin for speed, code for portability | Decides your long term dependency |
| Set public and has_archive | True for most visible content | Controls front end visibility |
| Set supports | Title, editor, thumbnail, excerpt as needed | Turns on the editing features you want |
| Turn on show_in_rest | Set to true | Enables the block editor and the API |
| Add taxonomies | Register any groupings you need | Lets visitors filter and browse |
| Add custom fields | ACF or coded fields for extra data | Makes the content structured |
| Flush permalinks | Save the Permalinks screen once | Stops the 404 on new URLs |
| Add templates | single and archive files in a child theme | Controls how the content looks |
Work through that list and your custom post type will be registered, grouped, structured, and displayed the way you intended, with none of the common traps left open.
When to Get Help
Creating a basic custom post type is well within reach for a confident site owner, especially with a plugin, and this guide gives you everything you need to try it. But custom post types are also where a simple site starts to become a real application, and past a certain point it pays to have a developer plan the structure properly. A post type that sits at the centre of your site, with several taxonomies, many custom fields, custom templates, and relationships between content types, is worth getting right the first time, because reworking it later once you have hundreds of entries is far more work.
We design and build custom post types, taxonomies, and fields as part of everyday WordPress work, and we register them in clean, portable code that does not depend on a plugin staying installed. We can build the admin experience so your team enters content without mistakes, create the templates that display it, and connect it to search, filtering, or an API if you need a headless front end. If your content model is the heart of the project, our WordPress development services cover it end to end. You can also read more about how we approach custom builds on our services page.
If you want your content types built the reliable way, you can get a free quote and we will map out the structure your project needs. Or book a free consultation and we will talk through your content and how best to model it, with no obligation. Getting the structure right early is one of the highest value decisions in a WordPress build, and the first conversation is free.