Custom Post Types
Custom Post Types
Custom post types (CPTs) extend Minnow beyond the built-in post and page types, letting you create structured content like portfolios, products, events, or testimonials. There are three ways to register them, each suited to different use cases.
1. Theme YAML (Recommended for Theme-Specific Content)
Define post types and their associated taxonomies directly in your theme's theme.yaml. This is the best approach when the content type is inherent to the theme's design (e.g. a portfolio theme needs a "Work" post type).
Example: data/themes/my-theme/theme.yaml
name: My Theme
version: 1.0.0
post_types:
work:
label: Work
singular: Work
plural: Work
public: true
hierarchical: true
has_archive: true
supports: [title, editor, thumbnail, excerpt]
menu_icon: briefcase
menu_position: 6
rewrite:
slug: work
taxonomies:
project-type:
label: Project Type
singular: Project Type
plural: Project Types
object_types: [work]
hierarchical: true
public: true
show_in_menu: true
show_admin_column: true
rewrite:
slug: project-type
The post type will appear in the admin sidebar and the frontend will resolve archive/single URLs automatically.
Templates
Create matching templates in your theme to display the custom post type:
templates:
single-work: templates/single-work.html # Individual item
archive-work: templates/archive-work.html # Archive/listing page
Template naming convention: single-{post_type} for individual items, archive-{post_type} for listings.
2. Plugin YAML (For Portable/Reusable Content Types)
Define post types in a plugin's plugin.yaml when the content type should be available regardless of which theme is active.
Example: data/plugins/my-plugin/plugin.yaml
name: Real Estate Listings
namespace: RealEstate
slug: real-estate
version: 1.0.0
post_types:
listing:
label: Listing
singular: Listing
plural: Listings
public: true
hierarchical: false
has_archive: true
supports: [title, editor, thumbnail, excerpt, custom-fields]
menu_icon: home
menu_position: 8
capability_type: post
rewrite:
slug: listings
taxonomies:
listing-type:
label: Listing Type
singular: Listing Type
plural: Listing Types
object_types: [listing]
hierarchical: true
public: true
show_in_menu: true
show_admin_column: true
The plugin must be listed in data/plugins.json to be active.
3. Admin UI (For Non-Developer Admins)
Navigate to Settings > Post Types in the admin panel to create, edit, and delete post types without writing any YAML. Admin-created CPTs are stored in the database and persist across theme changes.
- Click Add New to create a post type
- Fill in labels, choose supported features, assign taxonomies
- The post type key is auto-generated from the plural label (editable on creation, locked after)
- Admin-created post types appear with an "Admin" source badge; plugin/theme types show as "Plugin"
- Only admin-created post types can be edited or deleted from the UI
Post Type Configuration Reference
All three methods accept the same configuration options:
| Option | Type | Default | Description |
|---|---|---|---|
label |
string | ucfirst(name) | Display label |
singular |
string | label | Singular form (e.g. "Movie") |
plural |
string | singular + "s" | Plural form (e.g. "Movies") |
public |
bool | true |
Whether publicly queryable and visible |
hierarchical |
bool | false |
Support parent/child relationships (like pages) |
has_archive |
bool | false |
Enable archive listing pages |
show_in_menu |
bool | same as public | Show in admin sidebar |
supports |
array | [title, editor] |
Enabled features (see below) |
menu_icon |
string | file-text |
Lucide icon name for admin menu |
menu_position |
int | 25 |
Order in admin sidebar |
capability_type |
string | post |
Permission base |
rewrite |
object/string | {slug: name} |
URL rewrite rules |
Supported Features
Use these values in the supports array:
title— Post title fieldeditor— Content editorauthor— Author selectorthumbnail— Featured imageexcerpt— Excerpt fieldcomments— Enable commentsrevisions— Revision historycustom-fields— Custom field metaboxespage-attributes— Menu order and parent (requireshierarchical: true)
Rewrite Options
# Simple slug
rewrite:
slug: my-items
# Or shorthand string (plugin YAML only)
rewrite: my-items
# Disable rewrites
rewrite: false
Taxonomy Configuration Reference
| Option | Type | Default | Description |
|---|---|---|---|
label |
string | ucfirst(name) | Display label |
singular |
string | label | Singular form |
plural |
string | singular + "s" | Plural form |
object_types |
array | [] |
Post types this taxonomy applies to |
public |
bool | true |
Whether publicly queryable |
hierarchical |
bool | false |
Tree structure (categories) vs flat (tags) |
show_in_menu |
bool | same as public | Show in admin sidebar |
show_admin_column |
bool | false |
Show column in post list table |
show_tagcloud |
bool | same as public | Include in tag clouds |
rewrite |
object/bool | true |
URL rewrite rules |
Registration Priority
When the same post type name is defined in multiple places, the first registration wins:
- Built-in types —
post,page,attachment,revision,nav_menu_item(always registered first) - Plugins — Loaded via
PluginLoader::load() - Theme — Loaded via
ThemeEngine::registerContentTypes() - Admin UI — Loaded via
CustomPostTypeManager::loadFromDatabase()
If a plugin and theme both define a post type with the same key, the plugin's definition takes precedence. Admin UI entries skip any name already registered.
Migrating from WordPress
When converting a WordPress theme that registers custom post types in functions.php:
WordPress:
register_post_type('projects', [
'labels' => ['name' => 'Projects', 'singular_name' => 'Project'],
'public' => true,
'hierarchical' => true,
'supports' => ['title', 'editor', 'thumbnail', 'excerpt'],
'has_archive' => true,
'menu_icon' => 'dashicons-format-gallery',
'menu_position' => 20,
'rewrite' => true,
]);
Minnow (theme.yaml):
post_types:
projects:
label: Project
singular: Project
plural: Projects
public: true
hierarchical: true
has_archive: true
supports: [title, editor, thumbnail, excerpt]
menu_icon: image # Use Lucide icon name, not dashicons
menu_position: 20
rewrite:
slug: projects
Key differences:
- Labels use
singular/pluralshorthand instead of the full WordPress labels array - Menu icons use Lucide names instead of Dashicons
- Configuration is declarative YAML instead of PHP function calls
register_taxonomy()calls become ataxonomies:section withobject_typeslinking to post types