This is part of Build log, a running series on the WordPress plugins I maintain for venue and community sites.
Every venue divides its programme up differently. One client wants Gigs and Comedy Nights. The next wants Workshops, Markets and Private Hire. The one after that has a word for it I’ve never heard of before!
I didn’t want to be writing PHP every time that happened, so the plugin contains no event types at all. Not “Gig”, not “Show”, not “Event”. There’s an admin screen where someone types a singular and plural name, and then one loop that registers whatever they’ve asked for:
foreach ($this->settings->allEventTypes() as $eventType) {
register_post_type($eventType['post_type'], [
'labels' => $this->buildLabels($eventType['label_singular'], $eventType['label_plural']),
'public' => true,
'has_archive' => true,
'show_in_menu' => EventTypesMenu::PARENT_SLUG,
'menu_icon' => 'dashicons-calendar-alt',
'rewrite' => ['slug' => $eventType['slug']],
'supports' => ['title', 'editor', 'thumbnail', 'excerpt'],
]);
}
That’s the whole of it. The same lines run whether the site has one event type or the maximum of eight. buildLabels() takes the singular and plural and generates the fifteen label strings WordPress wants — “Add New Workshop”, “No workshops found in Trash”, “Search Workshops” etc.
show_in_menu points every type at a single shared parent, so eight event types produce eight entries under one “Event Types” menu rather than eight top-level items clutteringthe admin sidebar.
Taxonomies belong to one type, not all of them
Taxonomies work the same way, and this is the part that actually keeps a site tidy. Say a venue wants a Genre taxonomy on Gigs. It would make no sense at all on Workshops.
WordPress’s instinct here is one shared vocabulary that everything gets registered against, which gets muddled within a month — you end up with a Genre dropdown on a Private Hire listing, and eventually someone uses it. So each taxonomy is bound to exactly one event type at the moment it’s created. Genre stays on Gigs. Age Rating stays wherever it’s actually relevant.
The part that can never change
When a type is created, its post_type key is derived from the singular label and its URL slug from the plural — and after that moment, neither is ever touched again.
Relabelling is explicitly a display-only operation. Rename “Gig” to “Live music” a year later and every existing gig keeps its post type key, keeps its URL, keeps its position in Google’s index, keeps working with any theme code written against it. Only the words on the admin screens change.
The key is written into every row of wp_posts and the slug into every URL that’s ever been shared. Labels are for humans and can change hourly; keys are infrastructure and are effectively permanent. Conflating them is how a rename turns into four hundred 404s.
Creation is validated against the same thing. A singular name that can’t produce a valid key is rejected outright, and so is one whose key is already taken — including by a post type some entirely unrelated plugin registered, which post_type_exists() catches for us.
Deleting one, carefully
Types can be deleted from the same screen, and this is the one place the plugin is deliberately slower than it needs to be. Deleting an event type isn’t deleting one gig — it’s removing an entire category of content and potentially every post inside it.
A standard “are you sure?” felt far too casual for that, so the delete button stays disabled until you’ve typed the type’s plural label out in full. It’s the trick GitHub uses before it lets you destroy a repository, and it works for the same reason: it makes you read what you’re about to delete, rather than confirming a dialog you stopped reading four dialogs ago.

The flush that has to happen next time
One genuinely fiddly bug worth recording. Registering a new post type means WordPress’s rewrite rules need flushing, or the new type’s URLs 404 until something else happens to flush them.
The obvious place to do that is right after saving the new type. It doesn’t work, and the reason is ordering: init has already fired by the time the admin-post.php handler runs, so register_post_type() hasn’t been called for the type you just created.
So the save sets a transient instead, and the registration code — which runs on init, after the loop above — checks for it and flushes then, on the very next request. One flag, one condition, and the type’s URLs work from the first click.
There’s a cap of eight event types, which exists less for technical reasons than organisational ones: a venue that wants nine categories of event usually wants a taxonomy, not nine post types, and the cap is where that conversation starts. It’s a filter away from being any other number.
Labels are for humans and change whenever someone changes their mind. Keys are infrastructure. The plugin never lets an edit to the first become an edit to the second.