frontend
Bovenstad Menu
A self-refreshing digital menu board for a Dutch coffee shop, served from a cached CMS proxy.
- Role
- Software Engineer
- Date
- January 2026
Technologies
- vue
- vite
- typescript
- contentful
- sanity
- aws amplify
- aws lambda
- dynamodb
- vuetify
- unocss
- vue-router
Problem
A coffee shop’s menu changes with the season, but the menu on the wall should not. The screens in the store run all day on hardware nobody wants to maintain, and the one thing they must never do is show last month’s prices. Pulling live content from a CMS on every refresh is the obvious approach, and the obvious way to hammer the CMS.
The menu content was also being moved from Contentful to a new CMS, and the schema that had been
drafted for the move was written for the code rather than for the people who would have to use it.
Product, kind and menu were generic types chained by references: product pointed at kind, kind
pointed at menu, and the filter meant to keep a kind inside its menu was passed the kind’s own
identifier where the parent menu’s belonged, so it never filtered anything at all. Naming was
inconsistent: one export was called adType while its schema was named menuAd, and the menu type
carried no prefix to separate it from anything else. Almost no field had a title, so editors met raw
field names like strength in the Studio sidebar, and nothing had a preview, so every document in
the list looked the same. The migration itself was half-finished: the frontend still read
Contentful while the generated types still described the old schema, which is exactly the kind of
gap that becomes a runtime bug later.
Solution
The menu is a static Vue single-page app on AWS Amplify: no server to keep running, nothing to patch. Content lives in Contentful. Between the two sits a small Lambda behind API Gateway that proxies Contentful and caches the response in DynamoDB, so repeated screen refreshes are served from the cache instead of the CMS.
Freshness is handled with a deliberately blunt instrument: <meta http-equiv="refresh" content="300">, so every screen reloads itself every five minutes. For a menu board that is enough:
no kiosk software, no websocket, no operator. The layout is sized for the screens it actually runs
on rather than for a browser window, and the carousel for rotating promotions is the one place a
component library earns its keep.
The schema was then rebuilt around what it is actually for. Four document types replaced the generic
three, each carrying its domain in the name (menuProduct, menuProductBlock, menuAd and
menuColumn): a product, a block holding an array of product references, an ad, and a column that
holds blocks or ads. Because the nesting now runs downwards as
arrays of references (a block’s products, a column’s blocks and ads), the broken parent filter has
nowhere left to live; what a filter could never express is expressed by structure instead. Every
field gained a human title and explicit validation, with blocks required to hold at least one
product, and every type gained a preview showing a price, how many products it contains or a marker
for a new item, so the document list reads like a menu rather than a file listing. One export was
renamed so that the type name finally matches the schema name it belongs to, and the file formatting
was normalised.
What remains is honest work rather than a loose end nobody noticed. Renaming the types orphans the existing documents in the production dataset, so their content needs a migration that does not exist yet. The generated types have not been regenerated, so they still describe the old schema, and the frontend still reads Contentful: today the migration is true of the schema and nothing else. One fragile detail is also worth replacing before anything depends on it: the column type decides which fields to hide from a title the editor types by hand, so a stray “Ads” or “AD” would quietly change behaviour. That belongs in a dedicated field with fixed choices.
Contact
Interested in working together?
Tell me what you are building and what you need. A short note is enough.