“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.
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
| Argument | Meaning | Common values |
|---|---|---|
label / labels | The name shown in the admin screen | 'Products' (use the labels array for finer control) |
public | Whether it appears on the site | true |
has_archive | Whether it has an archive page | true (a string also changes the archive URL) |
menu_icon | The menu icon | 'dashicons-cart' and the like |
menu_position | Where it sits in the menu | 5 (below Posts) to 25 |
supports | Which editing features to use | ['title','editor','thumbnail','excerpt'] |
show_in_rest | Block editor support | true |
hierarchical | Whether it can have parents and children | false (true makes it page-like) |
rewrite | The URL shape | ['slug' => 'products', 'with_front' => false] |
taxonomies | Use the standard taxonomies | ['category', 'post_tag'] |
exclude_from_search | Exclude from search results | false |
show_in_nav_menus | Whether it can be added to menus | true |
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 name | Page it handles |
|---|---|
archive-shohin.php | The product archive (/shohin/) |
single-shohin.php | A product’s single page |
taxonomy-season.php | An 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.
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_archivesets the archive URL to that name rewrite.slugis the single page’s URL (/products/product-name/)with_front => falsedrops 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,themeand 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 templatesIf you’re using core custom fields, add 'custom-fields' to supports.
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 classificationsOutputting 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 mattersfunctions.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
- Just run
register_post_type()on theinithook infunctions.php has_archivesets the archive,supportssets the editing panels,show_in_restenables the block editor- Templates are
archive-<name>.php/single-<name>.php. Without them, existing files stand in - 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.”