Stop Stuffing Everything Into Posts: A Practical Case for WordPress Custom Post Types
Categories are not a content model. Here's how register_post_type() gives you structured content, clean URLs, template overrides, and a REST endpoint in about twenty lines—and when to reach for custom tables instead.
29 May 2026, 08:53 UTC

If you've ever built a WordPress site with a portfolio, a product catalog, or a book library, you've probably seen the temptation: just use regular posts, add a category called "Books," and move on. It works—until it doesn't. Your editorial content gets mixed with structured content, your permalink structure becomes a lie, and querying "all books by publisher" turns into a category-and-tag contortion act.
The better engineering decision is a custom post type registered with register_post_type(). It takes about twenty lines of code, gives you a dedicated admin UI, clean URLs, template overrides with zero routing code, and—if you want it—a REST API endpoint for free.
The thesis: data structures belong in plugins, not themes
WordPress's core abstraction is the post. A page is a post. An attachment is a post. A menu item is a post. The post_type column in wp_posts is what separates them. Registering your own type is not a hack—it's using the platform the way it was designed.
The one rule that matters: register post types in a plugin, never in functions.php. Themes own presentation; plugins own data structures. If you register a post type in your theme and later switch themes, the admin UI and URLs for that content vanish (the data stays in the database, but it's orphaned). A small site-specific plugin avoids that entirely.
A worked example: a "book" post type
Drop this into a file like wp-content/plugins/site-books/site-books.php with a standard plugin header:
add_action('init', function () {
register_post_type('book', [
'label' => 'Books',
'public' => true,
'show_in_rest' => true, // block editor + REST API
'supports' => ['title', 'editor', 'thumbnail', 'excerpt'],
'has_archive' => true,
'rewrite' => ['slug' => 'books'],
'menu_icon' => 'dashicons-book',
]);
register_taxonomy('publisher', 'book', [
'label' => 'Publishers',
'show_in_rest' => true,
'hierarchical' => false,
]);
});Run this on a staging site first. After activating the plugin, go to Settings > Permalinks and click Save—this flushes rewrite rules. Skip this and your new archive will 404, which is the single most common "custom post types are broken" report. If you flush programmatically, call flush_rewrite_rules() only on plugin activation—it's an expensive operation and must never run on every init.
Once active, you get:
- A "Books" menu in wp-admin with its own list, editor, and featured image support.
- An archive at
/books/and singles at/books/dune/. - A REST endpoint at
/wp-json/wp/v2/book(the defaultrest_baseis the post type key), becauseshow_in_restis true. This also enables the block editor instead of the classic editor. - A filterable "Publishers" taxonomy, so you never needed a custom table.
Theming with zero routing code
The template hierarchy resolves custom types automatically. Create single-book.php in your theme and it overrides single.php for books only; archive-book.php does the same for the archive. No rewrite rules, no query parsing, no conditional routing—just conventionally named files.
To verify: add a temporary HTML comment like <!-- single-book.php --> at the top of the template, load a book's URL, and view source. If the comment appears, the hierarchy picked your file. Then switch themes briefly on staging and confirm the Books menu and content survive—that validates the plugin-not-theme decision.
The trade-off you should actually weigh
Custom post types store everything in wp_posts and wp_postmeta. That's fine for thousands of items, but meta queries—filtering by custom field values—don't hit indexed columns and degrade noticeably at scale. If you're modeling millions of rows with complex relational joins (think inventory with stock movements, not a book library), a custom table with proper indexes is the honest answer. For the vast majority of structured editorial content, post types plus taxonomies are the right tool.
One more gotcha: choose a rewrite slug that can't collide with an existing page slug. If you have a page at /books and a post type archive at /books, one of them loses.
Where to start
Pick one thing on your current site that's pretending to be a blog post but isn't—a portfolio piece, a team member, a product. Register it as a post type in a small plugin on staging, flush permalinks, and check three things: the archive loads, a single loads with your custom template, and /wp-json/wp/v2/yourtype returns JSON. If all three pass, you've replaced a category hack with a real content model in under an hour.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.