Create Custom Post Type in WordPress Without a Plugin
WordPress ships with two content types — posts and pages — but you don’t need a plugin to add more. Here’s how to register a custom post type (“Portfolio,” in this example) with plain PHP, plus a couple of things the quick-tutorial version of this usually gets wrong.
Step 1: Put the registration code in a small site plugin, not functions.php
It’s tempting to paste this into the theme’s functions.php via Appearance → Theme Editor, but that ties your content type’s existence to whichever theme happens to be active — switch themes (or child themes) and your custom post type, and everything registered under it, disappears from the admin. Create a tiny single-purpose plugin instead (a single PHP file with a plugin header comment dropped in wp-content/plugins/) so the post type survives independently of theme changes.
Step 2: Register the post type
Hook into init and call register_post_type():
function custom_portfolio_post_type() {
$labels = array(
'name' => _x( 'Portfolio', 'Post Type General Name', 'text_domain' ),
'singular_name' => _x( 'Portfolio Item', 'Post Type Singular Name', 'text_domain' ),
'menu_name' => __( 'Portfolio', 'text_domain' ),
'add_new_item' => __( 'Add New Portfolio Item', 'text_domain' ),
'edit_item' => __( 'Edit Portfolio Item', 'text_domain' ),
'search_items' => __( 'Search Portfolio Items', 'text_domain' ),
'not_found' => __( 'Not found', 'text_domain' ),
'not_found_in_trash' => __( 'Not found in Trash', 'text_domain' ),
);
$args = array(
'label' => __( 'Portfolio', 'text_domain' ),
'description' => __( 'Portfolio Items', 'text_domain' ),
'labels' => $labels,
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
'hierarchical' => false,
'public' => true,
'show_ui' => true,
'show_in_menu' => true,
'show_in_nav_menus' => true,
'show_in_admin_bar' => true,
'show_in_rest' => true, // required for the block editor and the REST API
'menu_position' => 5,
'menu_icon' => 'dashicons-portfolio',
'can_export' => true,
'has_archive' => true,
'exclude_from_search' => false,
'publicly_queryable' => true,
'capability_type' => 'post',
);
register_post_type( 'portfolio', $args );
}
add_action( 'init', 'custom_portfolio_post_type', 0 );
The one arg worth calling out is show_in_rest. Without it — and it’s missing from most copy-paste versions of this snippet floating around — the post type won’t get the block editor; WordPress falls back to the classic editor for it, and it won’t be reachable through the REST API either, which breaks anything expecting to query it that way (Gutenberg blocks, headless setups, most modern plugins).
Step 3: Adding actual custom fields
One correction worth making here: the supports array above controls which built-in editor metaboxes show up (title, editor, featured image, excerpt) — it does not create a custom field. If you want something like a “Project Date” or “Client Name” field on your Portfolio items, you need one of two things:
- Advanced Custom Fields (ACF) — the practical choice for most projects; create a field group, assign it to the “Portfolio” post type, done.
register_post_meta()— if you want a field without another plugin dependency, register it explicitly withshow_in_rest => trueso it’s editable from the block editor’s custom fields panel or your own block.
register_post_meta( 'portfolio', 'project_date', array(
'type' => 'string',
'single' => true,
'show_in_rest' => true,
) );
Step 4: Activate and test
Activate the plugin (or save the theme file, if you went that route anyway). You’ll see a new “Portfolio” item in the admin menu. One thing that trips people up: rewrite rules don’t refresh automatically when you register a new post type, so /portfolio/some-item/ URLs can 404 until rules are flushed — visiting Settings → Permalinks once (just load the page, no need to change anything) forces a flush. Don’t call flush_rewrite_rules() on every page load in your plugin code; it’s expensive and should only run once, ideally on plugin activation.
Before this goes live
Test the archive URL and single-item URLs, check permissions for non-administrator roles who’ll be managing this content, confirm it’s included (or correctly excluded) from your sitemap, and decide deliberately whether has_archive and exclude_from_search should actually be on — not every custom post type belongs in public search results. If you need dated archive URLs for this post type, see our custom post type date archive guide.



[…] Create a Custom Post Type in WordPress Without a Plugin […]
[…] Create a Custom Post Type in WordPress Without a Plugin […]