
WordPress ships with two content types: posts and pages. That covers a blog and a handful of static pages just fine. But the moment your site needs to model something with its own identity, testimonials, products, team members, case studies, events, you’re better off with a custom post type (CPT) than a growing pile of pages with inconsistent templates and no shared structure.
When a CPT beats a page tree
A page tree works when content is genuinely hierarchical and one-off: an About page, a few sub-pages under it, done. It stops working when you have many similar items that should share a template, be queried together, be filtered by taxonomy, or be pulled into a REST API response as a distinct object.
Ask yourself three questions:
- Will there be more than a handful of these, added over time?
- Do they share a consistent shape (same fields, same layout)?
- Do you ever need to list, filter, or query them as a group (by category, by date, by custom taxonomy)?
If you answered yes to any of those, stop making pages and register a post type. A page named “Testimonial – Sarah K.” with no relationship to the other nineteen testimonial pages is a maintenance problem waiting to happen. A testimonial post type with an archive, a taxonomy for “industry,” and a consistent single-testimonial.php template is a content model.
Registering a CPT properly
The clean way to register a post type is in a small, focused plugin or in your theme’s functions.php, hooked to init. Here’s a solid baseline for a “Project” post type:
function serverborn_register_project_cpt() {
$labels = array(
'name' => 'Projects',
'singular_name' => 'Project',
'add_new_item' => 'Add New Project',
'edit_item' => 'Edit Project',
'all_items' => 'All Projects',
);
register_post_type( 'project', array(
'labels' => $labels,
'public' => true,
'show_in_rest' => true,
'has_archive' => true,
'menu_icon' => 'dashicons-portfolio',
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
'rewrite' => array( 'slug' => 'projects' ),
'capability_type' => 'post',
) );
}
add_action( 'init', 'serverborn_register_project_cpt' );A few of these arguments matter more than they look:
- show_in_rest makes the post type available to the block editor and the REST API. Skip it and you get the old classic editor with no warning why.
- has_archive gives you an automatic archive template at
/projects/. Leave it false if you plan to query the post type manually and never expose a listing page. - rewrite slug controls the URL structure. Decide it up front. Changing it later means every existing link and every search engine’s index needs to catch up.
- supports should list only what you actually need. If projects never need an excerpt, don’t clutter the admin screen with an unused metabox.
If you’d rather not touch code at all, a plugin like Custom Post Type UI does the same job through a settings screen and is a perfectly reasonable choice, especially for site owners who want the post type to survive a theme change untouched. The tradeoff is one more active plugin and a small UI layer between you and the actual register_post_type call. Either approach is fine; what matters is that the registration lives somewhere that isn’t your theme’s core template files, so switching themes doesn’t wipe out your content model.
The rewrite-flush gotcha everyone hits
You’ll register the post type, save your first project, click the link on the frontend, and get a 404. This is the single most common CPT support ticket, and it has one cause: WordPress builds its rewrite rules (the mapping between pretty URLs and internal queries) and caches them. Registering a new post type doesn’t automatically rebuild that cache. Until it’s rebuilt, WordPress has no idea that /projects/my-first-project/ should route anywhere.
The fix is to flush rewrite rules once, after the post type is registered. There are two correct ways to do this, and one very tempting wrong way.
Wrong way: calling flush_rewrite_rules() inside the same function that runs on every init. This looks like it works (the 404 goes away) but it silently costs you performance on every single page load, because rebuilding rewrite rules is not free. Never call it unconditionally on init.
Right way, if you’re in a plugin: hook the flush to activation, once:
register_activation_hook( __FILE__, function() {
serverborn_register_project_cpt();
flush_rewrite_rules();
});Right way, if you’re in a theme or you just need a one-time fix: use wp-cli, which does the same job without touching your code at all:
wp rewrite flushOr, in the dashboard, simply visit Settings, Permalinks, and click Save (with no changes). That triggers the same rebuild. Either method is safe to run as many times as you like; it’s a maintenance action, not something that should run automatically on every request.
Taxonomies, templates, and caching
Most CPTs want a companion taxonomy: register_taxonomy() alongside register_post_type(), tied together with the taxonomy => array of post types) argument. This is how you filter projects by client, testimonials by industry, or events by category, without inventing a second post type just to hold a label.
Templating follows WordPress’s normal hierarchy: create archive-project.php and single-project.php in your theme, and WordPress will pick them up automatically once the post type exists. No extra registration needed.
On the performance side, CPT archive and single pages are cached exactly like any other page when you’re running a full-page cache like LiteSpeed Cache. The only wrinkle is cache invalidation: make sure your cache plugin’s purge rules include your custom post type’s archive URL, not just posts and pages, otherwise editors will see stale project listings after publishing. If you’re on managed hosting like ServerBorn, this kind of purge-rule mapping across custom post types is handled at the server level, so it’s one less thing to configure by hand.
Takeaway
Reach for a custom post type whenever you have more than a few similar items that share a shape and need to be queried as a group. Register it with show_in_rest and a deliberate rewrite slug from day one, and flush rewrite rules exactly once, on activation or via wp rewrite flush, never on every page load. Get those two things right and your content model will scale a lot further than another folder of pages ever could.