Custom post types on a personal WordPress site
This site runs on WordPress, and until recently every post, articles, short notes, checkins, and 805 book posts, lived as the default `post` type. The homepage sorted everything by date and rendered each item with the same Blocksy card layout. Book posts showed up as cards with a title, an excerpt, and no cover image. Checkins had a map thumbnail but also a featured image slot that made no sense for a venue checkin. Notes had empty title slots because notes don’t have titles.
Separating them into custom post types fixed the rendering mismatch. One for `book`, one for `checkin`, one for `note`. Each gets its own archive URL. Blocksy stores card layout settings per CPT as `{cpt}_archive_archive_order` theme mods, so each type can be rendered differently. The homepage still shows everything mixed together, that’s intentional, but Blocksy now knows what it’s rendering.
Before CPTs: everything was a `post`. After: Blocksy renders each type with its own card layout.
## Registering the types
All three are registered in a `post-types.php` mu-plugin. The common args: `public => true`, `has_archive => true`, `show_in_rest => true` (Gutenberg won’t work without it), and a `rewrite` slug for the archive URL.
register_post_type( 'book', [
'public' => true,
'has_archive' => true,
'rewrite' => [ 'slug' => 'books' ],
'show_in_rest' => true,
'supports' => [ 'title', 'editor', 'thumbnail', 'excerpt', 'custom-fields' ],
'labels' => [ 'name' => 'Books', 'singular_name' => 'Book' ],
] );
register_post_type( 'checkin', [
'public' => true,
'has_archive' => true,
'rewrite' => [ 'slug' => 'checkins' ],
'show_in_rest' => true,
'supports' => [ 'title', 'editor', 'custom-fields' ],
'labels' => [ 'name' => 'Checkins', 'singular_name' => 'Checkin' ],
] );
register_post_type( 'note', [
'public' => true,
'has_archive' => true,
'rewrite' => [ 'slug' => 'notes' ],
'show_in_rest' => true,
'supports' => [ 'editor', 'custom-fields' ],
'labels' => [ 'name' => 'Notes', 'singular_name' => 'Note' ],
] );Code language: PHP (php)
Notes skip `title` from `supports`, they’re short, unheadlined posts, so the title field would just stay empty.
Blocksy also skips the `read_more` slot for titleless post types. The button never renders, even when enabled in the Customizer. The workaround is to inject it manually via the `blocksy:archive:render-card-layers` filter, checking that `$outputs['read_more']` is empty before setting it.
The /books/ archive after migration: each card shows cover, title, author, reading status, and date.
## Migrating 847 posts
Everything was already tagged with `book`, `checkin`, and `note`, so migration was a WP-CLI one-liner per type. Books first, since there were 805 of them:
wp post list --post_type=post --tag=book --field=ID
| xargs -I{} wp post update {} --post_type=bookCode language: PHP (php)
Same pattern for checkins and notes. The posts move; the tags, meta, and content stay intact.
After migration, the Blocksy theme mods needed patching. If the CPT-specific key, say, `note_archive_archive_order`, doesn’t exist in the database, Blocksy falls back to a hardcoded default with `read_more` disabled. The card layout you configured in the Customizer gets silently ignored. Copy `blog_archive_order` into each CPT-specific key via WP-CLI:
wp eval "set_theme_mod('note_archive_archive_order', get_theme_mod('blog_archive_order'));"
wp eval "set_theme_mod('book_archive_archive_order', get_theme_mod('blog_archive_order'));"
wp eval "set_theme_mod('checkin_archive_archive_order', get_theme_mod('blog_archive_order'));"Code language: JavaScript (javascript)
Notes archive: short, titleless cards. Blocksy skips the read_more slot for titleless types, with the filter injecting it manually.
## Routing Micropub posts to the right type
Checkins arrive via OwnYourSwarm and Micropub. Books arrive via Hardcover and n8n, also through Micropub. The Micropub plugin creates `post` type by default, which would immediately undo the migration.
The `pre_insert_micropub_post` filter runs before the post is inserted and lets you modify the post array. For a checkin payload, set `post_type` to `checkin`. For note-format posts, `note`. Checkin content gets generated from the venue name and locality at the same point.
add_filter( 'pre_insert_micropub_post', function( $post_data, $input ) {
if ( isset( $input['properties']['checkin'] ) ) {
$post_data['post_type'] = 'checkin';
// generate post content from venue name + locality
} elseif ( in_array( 'post-format-note', (array) ( $input['properties']['category'] ?? [] ), true ) ) {
$post_data['post_type'] = 'note';
}
return $post_data;
}, 10, 2 );Code language: PHP (php)
New checkins and notes now land in the right type without manual intervention. The homepage picks them up because `WP_Query` on `is_home()` includes all three CPTs alongside standard posts.
The `/books/`, `/notes/`, and `/checkins/` archives exist now. Each renders with its own card layout.
## What the /books/ archive grew into
Three features emerged on the archive page.
### Year headings
Books are sorted by finish date. The archive injects a year label whenever the year changes between cards via the `blocksy:archive:render-card-layers` filter, then hoisted out of the card wrapper by a small inline script.
### Status filter
`?status=finished`, `?status=reading`, `?status=want-to-read` filter via `pre_get_posts` and a `meta_query` on `mf2_read-status`. The filter bar is injected before the card grid via `wp_footer` JS. Server-side, so it works across pagination and is bookmarkable.
### Note badge
Notes on the homepage have no title. A small “Note” pill label distinguishes them from articles in the mixed stream.