The trouble with menu plugins is they all think they know what a menu is. Starters, mains, desserts, maybe a wine list if they’re feeling ambitious. Bolt on an allergen field that’s a free-text box, ship a couple of pre-made layouts, call it a menu solution. Fine for the case they’re built for. Useless for a patisserie.
Beurre’s menu doesn’t have starters. It has flavour families. It has techniques. It has the same lemon and yuzu number existing as a slice, a whole gateau, and the occasional celebration tier. The closest off-the-shelf plugin would have got us about 70% of the way there and the remaining 30% would have been a permanent compromise built into the site forever. So I built one.
Custom post type, ACF Pro for the structured fields, a category taxonomy that maps to how Raf actually thinks about the menu rather than the way a generic plugin assumes. Flavour profile, technique, seasonal availability, pricing variants for the size formats. Each item gets a consistent template across the site. Front-end cards that look like Beurre, not like a Yelp listing.
The visible bits are the cards. The work is mostly in the bits nobody sees – validation, the way category changes propagate, the fact that an item marked out-of-season hides from the public menu without being deleted. None of that is glamorous engineering. Most of it is the unsexy discipline of treating menu data as actual data instead of a wall of formatted text.
The allergen handling is the bit I took most seriously. Most menu plugins treat allergens as free text – a ‘may contain’ field that gets copy-pasted between items and slowly drifts out of sync with reality. That’s how people get hurt. The Beurre plugin treats every allergen as a known entity from a controlled vocabulary. Multi-select against a fixed list, not a notes field. The display logic warns clearly on the front-end. Connor still has to confirm in conversation for anything serious, because the website is never the final word on whether a cake is safe for someone, but the plugin makes sure the site isn’t telling lies in the meantime. The difference between ‘we did the thing because the platform asked us to’ and ‘we actually thought about this’ is small in code and large in consequence.
The clone function arrived later, as v1.1 features tend to. After a few weeks of watching Raf use the plugin, the pattern was obvious. New menu items weren’t really new. A seasonal variant of the opera. A different flavour of the drizzle. The structure stayed the same, two or three fields changed, and Raf was rebuilding the whole thing from scratch each time. A clone button. That was the entire feature. Copy the source item with all its fields populated, set the status to draft, let Raf edit the bits that differ. It should have been in v1. It wasn’t, because you don’t know you need it until you’re three weeks in and watching the same dance happen for the fifteenth time.
The argument for custom code is almost never that it’s cheaper. It isn’t. Building a bespoke plugin took longer than installing a generic one and clicking through the setup wizard. The argument is that the up-front cost buys you affordances that match the work, forever. A generic plugin asks the patisserie to think like a restaurant. The custom plugin asks the patisserie to think like itself. Over five years that compounds.
Most custom plugins fail because they reinvent generic plugins poorly. Someone needs a menu system, can’t be bothered to evaluate the existing options, writes a worse version from scratch, ships it. That’s not engineering, that’s vanity. This one earns its place because the off-the-shelf options were genuinely wrong – not ‘wrong by my taste’, wrong by the structure of the business they were being asked to model.
The other test is Connor. He’s not a developer. He publishes menu items. If the admin interface is wrong for him, the plugin has failed regardless of how cleanly the post type is wired up. The back-end could be a museum piece and it wouldn’t matter. So the bits that matter aren’t the schema; they’re the field labels, the order they appear in, the things the plugin makes hard to get wrong. Whether Connor uses it without swearing is the only metric that counts.

There’s a quiet satisfaction to bespoke software that does exactly the job it was built to do. Not over-featured. Not under-featured. Doesn’t try to be everyone’s plugin. Doesn’t have a pricing tier or a settings page with forty toggles. Just a custom post type, a sensible vocabulary, a clone button I should have shipped sooner, and an allergen field that’s actually a field. The kind of thing that’s never going on a product page. Never going to be downloaded by anyone. Goes nowhere, does nothing – except, in this very specific case, exactly what was needed.