A lot of WordPress sites shoehorn content that isn't really a "post" — products, team members, case studies, events — into the default Post type with categories doing double duty as a content model. It works until it doesn't: content gets hard to query separately, templates get full of conditional logic, and editors get confused about what a "post" even means on that site. Custom post types (CPTs) and custom taxonomies exist to fix exactly this.
When you actually need a custom post type
Ask: is this a distinct kind of content with its own fields, its own template, and its own place in the site's structure — separate from blog posts? If a "Team Member" needs a photo, title, and bio and appears on a dedicated /team/ archive with its own layout, it's a CPT candidate, not a blog category.
Common legitimate cases: products (if not using WooCommerce, which registers its own CPT), portfolio/case studies, events, testimonials, team members, locations. The test isn't "is there a lot of it" — it's "does it need its own identity, template, and query behavior separate from Posts."
Registering one correctly
Register custom post types in a plugin (or a theme's functions.php for theme-coupled content), not ad hoc — and always on the init hook. Key arguments worth being deliberate about:
publicandshow_in_rest— the latter is required for the block editor and REST API access; easy to forget and a common cause of "why doesn't Gutenberg work on this post type" bugs.supports— only enable what the content type actually uses (title, editor, thumbnail, custom-fields); don't leave defaults that expose UI editors don't need.rewrite— control the URL slug deliberately; changing it later requires a redirect strategy, so get it right up front.capability_type— if this content type needs different permissions than default posts (e.g., only certain roles can publish), define custom capabilities rather than reusingedit_posts.
Taxonomies: categorization that isn't tags-by-another-name
A custom taxonomy groups a custom post type the way categories group posts — "Event Type" for an Events CPT, "Department" for a Team CPT. Two decisions matter:
- Hierarchical or not. Hierarchical (category-like, with parent/child) suits structured classification; non-hierarchical (tag-like, free-form) suits flexible labeling.
- Which post types it applies to. A taxonomy can be shared across multiple post types deliberately — useful when, say, both "Articles" and "Case Studies" should share the same "Industry" taxonomy for cross-content filtering.
Custom fields: don't reinvent what core already gives you
For structured data beyond title/content (a product's price, an event's date and location), use a proper custom fields solution rather than cramming everything into post content. Whether you build with the native block editor's custom fields support, a fields plugin, or block bindings depends on your team's workflow — but the fields should be registered with show_in_rest enabled if the block editor or REST API needs to read them, which is easy to overlook and causes silent "field is empty in the editor" confusion.
Querying custom content types correctly
Query with WP_Query and post_type explicitly set — don't rely on defaults, which only include post and page. For taxonomy-filtered queries, use tax_query rather than trying to filter post-processing in PHP, which throws away pagination and is far slower on any real dataset.
A common mistake: over-fragmenting into too many post types
Not every distinct "kind" of content needs its own CPT. If two content types share nearly identical fields, templates, and behavior and differ only by a label, a shared post type with a taxonomy to distinguish them is often simpler to maintain than two nearly-identical CPTs with duplicated registration and template code.
The takeaway
Custom post types and taxonomies exist so your content model matches what the content actually is, instead of overloading Posts and categories to mean several different things at once. Reach for them when content has its own identity and structure — and register them deliberately (REST support, correct capabilities, thoughtful rewrite rules) rather than copy-pasted boilerplate.
