Tips・Adding post types and taxonomies in WordPress

How to create custom post types (register_post_type)

How to add content types like "Products" and "Case studies" alongside posts and pages. Includes copy-and-paste register_post_type code, an argument cheat sheet, and how to fix 404s.

“Products,” “Case studies,” “Staff profiles”—a custom post type is how you create a third kind of content alongside posts and pages. Write register_post_type() in functions.php and a dedicated menu appears in the admin screen.

Here’s the minimum that works, ready to copy.

function my_register_post_types() {
  register_post_type('shohin', [
    'label'        => 'Products',
    'public'       => true,
    'has_archive'  => true,
    'menu_icon'    => 'dashicons-cart',
    'supports'     => ['title', 'editor', 'thumbnail', 'excerpt'],
    'show_in_rest' => true,   // edit it in the block editor
  ]);
}
add_action('init', 'my_register_post_types');

That alone adds a “Products” menu to the admin screen and makes /shohin/ the product archive URL.

Theme developmentCreating "products" with a custom post typeWant to learn hands-on using the Shimaenagado example? Head to this lesson of the theme development course

Why not just mix it into “Posts”?

You could create a “Products” category and put them in Posts, but putting data of different natures in the same box makes management fall apart.

  • You want different sort orders and per-page counts for each list
  • You want price and stock fields on products only
  • You don’t want products mixed into the blog’s RSS feed and archives

Categories sort things within one box; post types add another box entirely—a different scale altogether. If any of the above applies, splitting the post type is the right call.

Argument cheat sheet

ArgumentMeaningCommon values
label / labelsThe name shown in the admin screen'Products' (use the labels array for finer control)
publicWhether it appears on the sitetrue
has_archiveWhether it has an archive pagetrue (a string also changes the archive URL)
menu_iconThe menu icon'dashicons-cart' and the like
menu_positionWhere it sits in the menu5 (below Posts) to 25
supportsWhich editing features to use['title','editor','thumbnail','excerpt']
show_in_restBlock editor supporttrue
hierarchicalWhether it can have parents and childrenfalse (true makes it page-like)
rewriteThe URL shape['slug' => 'products', 'with_front' => false]
taxonomiesUse the standard taxonomies['category', 'post_tag']
exclude_from_searchExclude from search resultsfalse
show_in_nav_menusWhether it can be added to menustrue

Fine-tuning the display names (labels)

label alone gets you most of the way, but when you want to tailor wording like “Add New,” use labels.

'labels' => [
  'name'               => 'Products',
  'singular_name'      => 'Product',
  'add_new_item'       => 'Add product',
  'edit_item'          => 'Edit product',
  'all_items'          => 'All products',
  'search_items'       => 'Search products',
  'not_found'          => 'No products found',
],

Preparing templates

Registering a post type makes dedicated template files available. There are naming rules.

File namePage it handles
archive-shohin.phpThe product archive (/shohin/)
single-shohin.phpA product’s single page
taxonomy-season.phpAn archive for a taxonomy applied to products

Without them, archive.php then index.php stand in. Get it working on the substitutes first and create dedicated files when you need them—that’s the low-effort path.

TipsTemplate hierarchy cheat sheet (which file gets used?)The cheat sheet of which file is used for which URL

Changing the archive URL

Keeping the post type name shohin while the URL is /products/ is a common requirement.

'has_archive' => 'products',
'rewrite'     => ['slug' => 'products', 'with_front' => false],
  • Passing a string to has_archive sets the archive URL to that name
  • rewrite.slug is the single page’s URL (/products/product-name/)
  • with_front => false drops the prefix from your permalink settings (/blog/ and the like)

Choosing a post type name

  • Lowercase alphanumerics and underscores, up to 20 characters
  • post, page, attachment, revision, nav_menu_item, action, order, theme and others are reserved and can’t be used
  • Change it later and all those posts vanish from view (the data remains, but the post type is treated as nonexistent), so decide up front

When in doubt, pick a short site-specific name like shohin. Generic names like item or data risk colliding with plugins, so they’re best avoided.

Adding input fields

Products need fields like price and stock. With ACF (SCF), you can add input fields without code. Set the field group’s “Location” to “Post Type = Products” and the fields appear only in the product editing screen.

TipsACF field types: a cheat sheet for displaying each oneHow to output those fields in your templates

If you’re using core custom fields, add 'custom-fields' to supports.

TipsWorking with custom fields using get_post_metaHow to handle them with get_post_meta, without a plugin

Adding taxonomies too

To classify products by “season” or “brand,” add a custom taxonomy. Reusing the standard categories and tags takes one line.

'taxonomies' => ['category', 'post_tag'],
TipsHow to create custom taxonomies (register_taxonomy)How to write register_taxonomy for your own classifications

Outputting the list in a template

To line products up on the front page, fetch them with WP_Query. The key is specifying post_type.

<?php
$items = new WP_Query([
  'post_type'      => 'shohin',
  'posts_per_page' => 6,
]);
?>
<?php while ($items->have_posts()) : $items->the_post(); ?>
  <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
<?php endwhile; ?>
<?php wp_reset_postdata(); ?>
TipsHow to fetch posts outside the loop (WP_Query)How to write WP_Query, and why cleanup matters

functions.php or a plugin?

  • The theme’s functions.php—easy. But change themes and the post type disappears (the data remains, it just becomes invisible in the admin screen)
  • A dedicated plugin (one file in wp-content/plugins/my-post-types/)—survives theme changes. Suited to long-running sites

There are plugins that let you create these from the admin screen, but writing code means you can carry the same setup to another site. As a learning exercise too, writing it in code first is what I’d recommend.

Summary

  1. Just run register_post_type() on the init hook in functions.php
  2. has_archive sets the archive, supports sets the editing panels, show_in_rest enables the block editor
  3. Templates are archive-<name>.php / single-<name>.php. Without them, existing files stand in
  4. On a 404, re-save the permalink settings. Decide the post type name up front and don’t change it

Once you can split content by type, you can design “one WordPress managing many kinds of content.”

FAQ

Where do I write a custom post type?
Write register_post_type() in your theme's functions.php (or a plugin) and run it on the init hook. Writing it in the theme means the post type disappears when you change themes, so on long-running sites it's also common to put it in a dedicated plugin.
The archive page for my custom post type 404s.
Open "Settings" → "Permalinks" in the admin screen and press "Save Changes." That regenerates the URL rewrite rules and fixes it. Also confirm 'has_archive' => true.
What characters can a post type name use?
Lowercase alphanumerics and underscores, up to 20 characters. Names WordPress reserves—post, page, attachment, revision, nav_menu_item, action, order, theme and others—can't be used.
I can't edit it in the block editor.
Add 'show_in_rest' => true to register_post_type's arguments. Without it you get the old (classic) editor screen.