Home › Blog › Article

Custom Post Types and Taxonomies: Design Mistakes

Custom Post Types and Taxonomies: Design Mistakes

Register custom post types with explicit rewrite slugs, set show_in_rest to true for block editor and API access, and keep taxonomies for grouping only. Verify by visiting the archive URL, checking /wp-json/wp/v2/, and profiling queries with Query Monitor. If permalinks resolve, REST returns data, and no duplicate slug 404s appear, the design is correct.

How it works

A custom post type (CPT) registers a new object type in the wp_posts table via register_post_type(), storing entries with a distinct post_type column value. Taxonomies registered with register_taxonomy() create terms in wp_terms and relationships in wp_term_relationships. WordPress builds rewrite rules in wp_rewrite and matches requests through WP_Query. Below is a minimal runnable example for a ‘book’ CPT and ‘genre’ taxonomy as of WordPress 6.7 (2026).

<?php
add_action( 'init', function() {
    register_post_type( 'book', [
        'public' => true,
        'label'  => 'Books',
        'show_in_rest' => true,
        'rewrite' => [ 'slug' => 'library/book' ],
        'has_archive' => true,
        'supports' => [ 'title', 'editor' ],
    ] );
    register_taxonomy( 'genre', 'book', [
        'public' => true,
        'show_in_rest' => true,
        'rewrite' => [ 'slug' => 'library/genre' ],
    ] );
} );

Place this in a plugin file such as wp-content/plugins/my-books/my-books.php. After activation, go to Settings > Permalinks and click Save to flush rewrite rules.

CPT vs taxonomy responsibilities

A CPT owns content entries; a taxonomy classifies them. Do not use a CPT as a category system. For example, store each movie as a CPT ‘movie’, and use taxonomy ‘genre’ to group them. If you need attributes like release year, use post meta on the CPT, not a taxonomy term. To verify: check that your archive at /library/book/ lists books and that /library/genre/scifi/ filters them. Rollback: if you accidentally made genres a CPT, export with WP-CLI wp export --post_type=genre, then re-register as taxonomy and reassign with wp term create. Common pitfall is duplicating data across both, causing sync bugs. When planning content models, our guide on WordPress API Development shows how REST exposure depends on clean separation.

Rewrite slug conflicts

Get a Quote

Email + requirement + budget range — we reply with a quote within 24 hours.

Rewrite slugs collide when a CPT slug matches a page slug or another taxonomy. WordPress resolves by rule order in wp_rewrite->rules, leading to 404s or wrong content. Always namespace slugs, e.g. library/book not book. After changing slugs, flush rules by visiting Settings > Permalinks or running wp rewrite flush. Verify with wp rewrite list | grep library. Pitfall: forgetting to flush after code change leaves stale rules. Rollback: revert the slug in code and flush again. For multilingual sites, see WordPress Multilingual Website Setup to avoid per-language slug clashes.

show_in_rest

Set show_in_rest to true to expose the CPT and taxonomy in the WordPress REST API at /wp-json/wp/v2/book and enable Gutenberg. Without it, the block editor falls back to classic and headless clients fail. In 2026, WordPress 6.7 defaults many core types to true, but custom ones need explicit flags. Verify by curling https://yoursite.com/wp-json/wp/v2/book and confirming JSON. Pitfall: setting true but forgetting rest_base gives ugly endpoints; add 'rest_base' => 'books'. Rollback: set false and flush. For API-heavy builds, our Headless WordPress Development guide covers consumption patterns.

Query cost as data grows

As rows in wp_posts pass 50k, WP_Query on CPTs without indexes slows. Always add post_type to queries and avoid meta_query on large sets. Use tax queries instead of meta for filtering. Verify with Query Monitor: check query time on /library/book/. If slow, add MySQL index via wp db query "ALTER TABLE wp_posts ADD INDEX idx_type_status (post_type, post_status)". Pitfall is using posts_per_page => -1. Rollback: drop index with ALTER TABLE wp_posts DROP INDEX idx_type_status. Monitor with New Relic or Query Monitor plugin.

Step-by-step process to audit your CPT design

Step-by-step process to audit your CPT design
  1. List all registered types with wp post-type list and taxonomies with wp taxonomy list.
  2. Check each rewrite slug for conflicts against page slugs.
  3. Confirm show_in_rest is true and test the endpoint.
  4. Profile archive queries with Query Monitor under load.
  5. Flush permalinks and re-verify URLs resolve 200 not 404.

Frequently asked questions

How do I flush rewrite rules safely?

Use wp rewrite flush via WP-CLI after code deploy, never on every init in production.

Can I use one taxonomy for multiple CPTs?

Yes, pass an array to register_taxonomy() second arg, e.g. ['book','movie'].

What breaks if show_in_rest is false?

Gutenberg won’t load for that CPT and REST clients get 404 on the endpoint.

Action checklist

  • Namespace all rewrite slugs and flush after change.
  • Enable show_in_rest and set rest_base.
  • Keep taxonomies for grouping, meta for attributes.
  • Index post_type and avoid unbounded queries.
  • Verify with WP-CLI and Query Monitor.

Need expert help? Our plugin team builds scalable CPT architectures. See all services or the tech column.

References & further reading

Need WordPress Development?

Free proposal and quote within 24 hours — we serve clients worldwide.

QQ