Minnow

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 field
  • editor — Content editor
  • author — Author selector
  • thumbnail — Featured image
  • excerpt — Excerpt field
  • comments — Enable comments
  • revisions — Revision history
  • custom-fields — Custom field metaboxes
  • page-attributes — Menu order and parent (requires hierarchical: 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:

  1. Built-in typespost, page, attachment, revision, nav_menu_item (always registered first)
  2. Plugins — Loaded via PluginLoader::load()
  3. Theme — Loaded via ThemeEngine::registerContentTypes()
  4. 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/plural shorthand 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 a taxonomies: section with object_types linking to post types