Skip to main content
Blog|
How-to guides

How to create custom post types in WordPress

|
May 21, 2026|10 min read
HOW-TO GUIDESHow to create custom posttypes in WordPressHOSTNEYhostney.comMay 21, 2026

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() )
SetupPoint-and-click admin screenA PHP function in a file
Best forNon-developers, quick prototypesDevelopers, production sites
Survives theme switchYes (it is a plugin)Only if the code is in a plugin
Version controlNo – settings live in the databaseYes – the code is a file
RiskNone – no code to breakA 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.

  1. Install and activate Custom Post Type UI – the standard process in how to install plugins in WordPress.
  2. Go to CPT UI > Add/Edit Post Types.
  3. Fill in the Post Type Slug (lowercase, no spaces – for example portfolio ), the Plural Label, and the Singular Label.
  4. 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).
  5. Under Supports, tick the editing features the type should have – Title, Editor, Featured Image, Excerpt.
  6. 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 true makes 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 true creates an archive listing page at /portfolio/ . Leave it false for 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. title and editor are the basics; add thumbnail for a featured image, excerpt for a summary, custom-fields for metadata, page-attributes for ordering. Anything you leave out simply does not appear on the edit screen.
  • show_in_rest true is 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; true makes 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.

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.php if the theme has that file, falling back to single.php , then index.php .
  • The archive page at /portfolio/ is rendered by archive-portfolio.php , falling back to archive.php , then index.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 on init . 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.