Short answer: a custom post type is a content type beyond the built-in Posts and Pages – think Portfolio, Product, Event, or Testimonial. You create one either with a plugin like Custom Post Type UI (no code, point and click) or with
register_post_type()
in PHP, hooked to the
init
action. After registering one, visit Settings > Permalinks and click Save once, or the archive URL returns a 404.
This guide covers both routes, explains the
register_post_type()
arguments that actually matter, shows how to attach categories of your own with custom taxonomies, and walks through the gotchas – the permalink flush, where to put the code, and the difference between a custom post type and a plugin.
What a custom post type actually is#
WordPress ships with a handful of post types. Two are visible in the admin – Posts and Pages – and a few work behind the scenes (attachments, revisions, navigation menu items). A post type is just a label WordPress attaches to a row in its content table so it knows how to list, edit, and display that content.
A custom post type (CPT) is one you define yourself. Once registered, WordPress treats it as a first-class citizen: it gets its own menu item in the admin, its own editor screen, its own archive page, and its own slot in the template hierarchy. A photographer registers a
portfolio
type; a real estate site registers a
property
type; a restaurant registers a
menu_item
type. Each keeps that content cleanly separated from blog posts and pages.
The reason this matters: without a CPT, every non-page piece of content is forced into the Posts bucket, mixed in with the blog, sharing the same categories and the same archive. A CPT gives the content its own home, its own admin workflow, and its own URL space.
Plugin or code: which route to take#
There are two ways to register a custom post type, and the right choice depends on who maintains the site.
| Plugin (Custom Post Type UI) | Code (
register_post_type()
) | |
|---|---|---|
| Setup | Point-and-click admin screen | A PHP function in a file |
| Best for | Non-developers, quick prototypes | Developers, production sites |
| Survives theme switch | Yes (it is a plugin) | Only if the code is in a plugin |
| Version control | No – settings live in the database | Yes – the code is a file |
| Risk | None – no code to break | A typo can white-screen the site |
The plugin route (covered next) is the fastest and safest if you do not write PHP. The code route is the better long-term answer for a developer-maintained site, because the definition lives in a file you can review, version, and deploy – but only if you put it in the right place, which is the single most common mistake people make.
Creating a custom post type with a plugin#
The most popular no-code option is Custom Post Type UI (often shortened to CPT UI). It is a free plugin that gives you a form for everything
register_post_type()
would otherwise do in code.
- Install and activate Custom Post Type UI – the standard process in how to install plugins in WordPress.
- Go to CPT UI > Add/Edit Post Types.
- Fill in the Post Type Slug (lowercase, no spaces – for example
portfolio), the Plural Label, and the Singular Label. - Open Settings and review the options – the important ones are Public (yes), Has Archive (yes, if you want a
/portfolio/listing page), and Show in REST (yes, so the block editor works). - Under Supports, tick the editing features the type should have – Title, Editor, Featured Image, Excerpt.
- Click Add Post Type.
The new type appears in the admin menu immediately. CPT UI even has a Get Code button that prints the equivalent
register_post_type()
call, which is handy if you later want to move the definition into a plugin and drop CPT UI.
The one trade-off to know: a plugin stores its post type settings in the database, not in a file. That is fine for most sites, but it means the definition is not version-controlled and does not travel with a code deploy. For a site managed by a developer with a deployment pipeline, the code route below is cleaner.
Creating a custom post type with code#
In code, a custom post type is one function call –
register_post_type()
– hooked to the
init
action. Here is a complete, working registration for a
portfolio
type:
add_action( 'init', function() {
register_post_type( 'portfolio', array(
'labels' => array(
'name' => 'Portfolio',
'singular_name' => 'Portfolio item',
'add_new_item' => 'Add new portfolio item',
'edit_item' => 'Edit portfolio item',
),
'public' => true,
'has_archive' => true,
'menu_icon' => 'dashicons-portfolio',
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
'rewrite' => array( 'slug' => 'portfolio' ),
'show_in_rest' => true,
) );
} );
The first argument,
'portfolio'
, is the post type key. Keep it short, lowercase, under 20 characters, and avoid the reserved names WordPress uses internally (
post
,
page
,
attachment
,
revision
,
nav_menu_item
, and anything starting with
wp_
). Prefix it on a client site –
acme_portfolio
– so it cannot collide with a plugin that registers the same key.
The arguments that matter#
The second argument is an array of options. Most have sensible defaults; these are the ones worth setting deliberately:
-
public–truemakes the type visible in the admin and on the front end. This one flag flips several others to reasonable defaults, so set it first. -
has_archive–truecreates an archive listing page at/portfolio/. Leave itfalsefor a type that only ever appears as single items (a testimonial shown in a slider, say). -
supports– the array of editor features the type gets.titleandeditorare the basics; addthumbnailfor a featured image,excerptfor a summary,custom-fieldsfor metadata,page-attributesfor ordering. Anything you leave out simply does not appear on the edit screen. -
show_in_rest–trueis required for the block editor. Omit it and the type silently falls back to the old classic editor, which is a confusing bug to debug. -
menu_icon– a Dashicon name (dashicons-portfolio,dashicons-cart) or an image URL, for the admin menu. -
rewrite– controls the URL slug.array( 'slug' => 'portfolio' )gives/portfolio/item-name/. -
hierarchical–false(the default) makes the type behave like Posts;truemakes it behave like Pages, with parent/child relationships.
Where to put the code - and where not to#
This is the decision that trips people up. The
register_post_type()
call can live in two places:
- A small plugin – the correct choice for almost every CPT. Create a single PHP file in
wp-content/plugins/, add a plugin header comment, paste the registration code, and activate it. - The theme’s
functions.php– works, but ties the content type to the theme.
The problem with the theme route is concrete: a custom post type is content structure, not design. If you register
portfolio
in a theme and later switch themes, the registration stops running. The portfolio entries are still in the database – the data is safe – but WordPress no longer knows the
portfolio
type exists, so the admin menu vanishes and the archive 404s. The content is stranded.
A plugin keeps the post type alive regardless of the active theme. The rule: design belongs in the theme, content types belong in a plugin. This is the same principle that applies to shortcodes that content depends on – anything authors build content around should survive a theme switch. If you genuinely are building a theme where the type is inseparable from the design, register it in
functions.php
the same way the custom theme guide shows – but treat that as the exception, not the default.
The permalink flush gotcha#
After registering a custom post type with
has_archive
or a custom
rewrite
slug, the archive and single URLs return a 404 until you flush WordPress’s rewrite rules.
WordPress builds its URL routing table once and caches it. A brand-new post type’s URLs are not in that cached table yet. The fix is a one-time action:
Go to Settings > Permalinks and click Save Changes. You do not need to change anything – the act of saving rebuilds the rewrite rules and registers your new URLs.
This catches almost everyone the first time. If a freshly created custom post type’s archive page shows a 404, this is the cause – not a broken registration. Do the permalink save once after adding or changing a CPT, and the URLs resolve.
Adding custom taxonomies#
A custom post type often needs its own way to be organized. Blog posts have Categories and Tags; a
portfolio
type might need a “Project type” grouping that is separate from the blog’s categories. That is a custom taxonomy, registered with
register_taxonomy()
:
add_action( 'init', function() {
register_taxonomy( 'project_type', 'portfolio', array(
'labels' => array(
'name' => 'Project types',
'singular_name' => 'Project type',
),
'public' => true,
'hierarchical' => true, // true = behaves like categories, false = like tags
'show_in_rest' => true,
'rewrite' => array( 'slug' => 'project-type' ),
) );
} );
The first argument is the taxonomy key, the second is the post type (or array of post types) it attaches to. Set
hierarchical
to
true
for a category-style taxonomy with parent/child terms, or
false
for a flat, tag-style one. Like a CPT, a new taxonomy needs a permalink flush before its archive URLs work.
A custom taxonomy keeps the new content type’s organization separate from the blog – the
portfolio
items get grouped by Project type without polluting the blog’s categories.
How custom post types display on the front end#
Once content exists in a custom post type, WordPress uses the template hierarchy to display it. For a
portfolio
type:
- A single portfolio item is rendered by
single-portfolio.phpif the theme has that file, falling back tosingle.php, thenindex.php. - The archive page at
/portfolio/is rendered byarchive-portfolio.php, falling back toarchive.php, thenindex.php.
This means you can give a custom post type a completely different layout from blog posts just by creating the right template file – no conditional logic needed. A block theme handles the same idea through the Site Editor, where you add a template for the custom post type visually. If you are building the templates yourself, the custom theme guide covers the template hierarchy in full.
Common mistakes to avoid#
- Forgetting the permalink flush. A new CPT’s archive 404s until you save Settings > Permalinks once. This is the number-one CPT support question.
- Registering the CPT in the theme. Switch themes and the type disappears, stranding its content. Use a plugin unless the type is genuinely theme-specific.
- Omitting
show_in_rest. Without'show_in_rest' => true, the type uses the classic editor instead of the block editor – an easy bug to miss. - Using a reserved or unprefixed key. Avoid
post,page,wp_*, and keys longer than 20 characters. Prefix client-site keys to dodge plugin collisions. - Registering on the wrong hook.
register_post_type()must run oninit. Calling it directly, or too early, fails silently. - Expecting old content to migrate itself. Changing a post’s type is not something the admin offers natively – moving existing posts into a new CPT needs a plugin like Post Type Switcher or a WP-CLI command.
How Hostney handles this#
Registering a custom post type is WordPress-side work, but the code route means editing PHP, and that is where the hosting environment matters.
Every Hostney site runs in its own isolated container, so a syntax error in a CPT plugin – a missing brace, a stray semicolon – affects only the site you are editing, never a neighbor. You can reach the files three ways: SSH/SFTP, FTPS, or the built-in file manager. The file manager’s code editor flags PHP syntax errors before you save, which catches the most common cause of a white screen right when you would otherwise be debugging a blank page.
Because the PHP environment is kept current,
register_post_type()
and
register_taxonomy()
behave exactly as the current WordPress documentation describes, including the
show_in_rest
block editor integration. And since each account is fully isolated, dropping a small CPT plugin into
wp-content/plugins/
to test a new content type is genuinely safe to do on a live site – a botched registration cannot reach anything beyond that one install.
Summary#
A custom post type gives content like portfolios, products, or events its own home in WordPress – its own admin menu, editor, archive, and URL space – instead of forcing everything into Posts and Pages. Create one with the Custom Post Type UI plugin if you do not write code, or with
register_post_type()
hooked to
init
if you do. Put the code in a small plugin, not the theme, so the content type survives a theme switch. Set
public
,
has_archive
,
supports
, and
show_in_rest
deliberately, pair the type with a custom taxonomy when it needs its own grouping, and – the step everyone forgets – visit Settings > Permalinks and click Save once so the new URLs resolve instead of 404ing.