gaash.ai

Technical & Structure · 8 min read · September 24, 2026 · , gaash.ai

A café menu machines can read: milks, pastries and the seasonal board

Publish the menu on a page as text: drinks with their sizes and prices, the milks and extras, the bakery case and any food, each item with the dietary label you use, in your own words. Keep the seasonal board as a dated section and take an item off the day it goes. Then repeat the same menu in Menu, MenuSection and MenuItem markup, attached to the café with hasMenu.

Why a café menu has to be words on a page

A question such as "does [café name] have oat milk" can only be confirmed from a line that says so. A photo of the chalkboard, a menu image in the social feed and a PDF from opening day hold the same words as pixels or as a separate file, and the PDF trap sets out what a fetcher receives from each. The café version of the fix is a menu page in HTML that a person reads and a machine parses in the same way.

In our audit of 9,483 small business websites (40 trades, 90 metros, September 2026), 2.1% kept their real content in a PDF and 3.0% delivered their text only through JavaScript, which an AI crawler that runs no scripts receives as an empty page. Those shares are for the whole sample; the audit has no café cut. A menu that exists only as an image has the same problem.

Keep the designed version if customers like it. Link the PDF under the text, or show the photo of the board with alt text that says what it is. The words that answer questions go in the HTML. The rest of what a café's site states, beyond the menu, is on the café industry page.

Sections and items: how a café menu is laid out

Group the menu the way people order, with a heading for each section, and give every item a name, a price for each size and a short description where the name does not say what is in the cup. A section heading is somewhere a question can land: someone asking which teas you have should find a heading called Tea.

Sections for a café menu, what each item line states, and an example line with placeholders for the café to fill in.
SectionEach item statesExample line
Espresso drinksName, sizes, a price per size, the milk it comes withFlat white, [size] [price] or [size] [price], made with whole milk unless you ask
Brewed and coldMethod, origin where it changes, sizes and pricesBatch brew, [origin], [size] [price]; cold brew, [size] [price]
Tea and other drinksName, hot or iced, caffeine-free where true[Tea name], caffeine-free, [price]
Milks and extrasEvery milk you carry and its charge, or no chargeOat, soy or almond, [price] or no charge; extra shot, [price]
BakeryName, your dietary label, when it runs out[Pastry name], vegan, from [time] until sold out, [price]
FoodName, the hours it is served, your dietary label[Dish name], vegetarian, served until [time], [price]
SeasonalName, the dates it runs, a price[Drink name], [start date] to [end date], [price]

Write sizes the way the counter says them, and say once at the top what each one holds, in ounces or milliliters. A drink sold in two sizes has two prices on the page, and in the markup each size gets its own offer. A long menu reads well as a table with a caption naming it and the date it was last changed; a short one reads well as headed lists. Either survives being read as plain text, and an image does not.

Dietary labels in your own words, and the line about allergies

The dietary words on a menu are claims your café makes, so use your own and define each one once at the top: what you mean by vegan, what you mean by dairy-free, what "made without gluten ingredients" does and does not promise. Put the label beside the item in text. An icon with no word beside it is a picture, and a picture of a leaf states nothing.

Which of those words carry a legal meaning where you trade, and what they then require, is not something this article states for any country. Ask your local food authority before a regulated word goes on the board. What the page can always carry is the honest description: "made with oat milk", "no animal products in the recipe", "baked in a kitchen that also uses wheat, nuts and dairy".

End the menu with one line that sends allergy questions to people rather than to the page: "Our kitchen uses [list]. If you have an allergy, ask a member of staff before you order." It is a sentence an assistant can pass on to someone asking whether your pastries are safe for them, and it puts the answer with the people who can give it.

In the markup, MenuItem takes suitableForDiet, with values from schema.org's RestrictedDiet list such as VeganDiet, VegetarianDiet, GlutenFreeDiet and LowLactoseDiet. Use a value only where your own label claims exactly that, and leave it out where your label is more careful than the value. The text on the page is the café's statement; the markup repeats it and never says more.

Milks, sizes and extras in the markup

Milk questions come in many forms (oat, soy, almond, lactose-free, the charge for each), and the menu answers all of them in one place: a section listing every milk and its price or "no charge". MenuItem has a property for exactly this: menuAddOn, which takes a MenuItem or a whole MenuSection of add-ons, so every espresso drink can point at one Milks section instead of repeating it.

Give that section an @id and refer to it from each drink, as the template below does. When you add a milk or drop one, you change one list and every drink follows. Sizes work through offers: one MenuItem for the flat white, with an Offer for each size carrying its own name, price and priceCurrency.

The seasonal board: adding an item and retiring it

Seasonal items go up in a hurry, and their removal is easy to forget. That is how a winter drink ends up on a menu page in July. Give each one a start and an end when it goes up:

  1. Add the item to a Seasonal section with its dates: "from [date], until [date] or while it lasts".
  2. In the markup, give the item's Offer an availabilityStarts and an availabilityEnds date matching the page.
  3. Change the menu's "last updated" date on the page.
  4. On the day it ends, take it off the page, out of the markup, off each delivery app and off any listing that shows your menu, in the same hour.
  5. If it returns every year and people ask for it by name, leave one line in the Seasonal section: "[Drink name] returns in [month]."

The last step is the honest version of keeping a page alive. Someone asking in March whether you still have the winter special should hear no, and a line that says when it comes back gives them a reason to return.

The template: a café menu in JSON-LD

This block describes the café as CafeOrCoffeeShop and attaches the menu with hasMenu, which the type inherits from FoodEstablishment. The sections and items are schema.org's Menu, MenuSection and MenuItem types, each item with its offers and price. Every value in brackets is a placeholder: replace each with what your page says, and leave out any line your page does not state. It is structured data in JSON-LD, placed in the page's HTML.

{
  "@context": "https://schema.org",
  "@type": "CafeOrCoffeeShop",
  "@id": "https://www.example.com/#cafe",
  "name": "[Café name, as on your sign]",
  "url": "https://www.example.com/",
  "telephone": "[the number that rings at the counter]",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "[street and number, unit if any]",
    "addressLocality": "[city]",
    "postalCode": "[postal code]",
    "addressCountry": "[country code]"
  },
  "hasMenu": {
    "@type": "Menu",
    "url": "https://www.example.com/menu",
    "hasMenuSection": [
      {
        "@type": "MenuSection",
        "name": "Espresso drinks",
        "hasMenuItem": [
          {
            "@type": "MenuItem",
            "name": "Flat white",
            "description": "[what is in the cup, as the page says it]",
            "offers": [
              { "@type": "Offer", "name": "[small size]", "price": "[price]", "priceCurrency": "[currency code]" },
              { "@type": "Offer", "name": "[large size]", "price": "[price]", "priceCurrency": "[currency code]" }
            ],
            "menuAddOn": { "@id": "https://www.example.com/menu#milks" }
          }
        ]
      },
      {
        "@type": "MenuSection",
        "@id": "https://www.example.com/menu#milks",
        "name": "Milks and extras",
        "hasMenuItem": [
          { "@type": "MenuItem", "name": "Oat milk",
            "offers": { "@type": "Offer", "price": "[price, or 0 for no charge]", "priceCurrency": "[currency code]" } },
          { "@type": "MenuItem", "name": "Extra shot",
            "offers": { "@type": "Offer", "price": "[price]", "priceCurrency": "[currency code]" } }
        ]
      },
      {
        "@type": "MenuSection",
        "name": "Bakery",
        "hasMenuItem": [
          {
            "@type": "MenuItem",
            "name": "[pastry name]",
            "description": "[your label, in your words, as on the page]",
            "suitableForDiet": "https://schema.org/VeganDiet",
            "offers": { "@type": "Offer", "price": "[price]", "priceCurrency": "[currency code]" }
          }
        ]
      },
      {
        "@type": "MenuSection",
        "name": "Seasonal",
        "hasMenuItem": [
          {
            "@type": "MenuItem",
            "name": "[seasonal drink]",
            "offers": {
              "@type": "Offer",
              "price": "[price]",
              "priceCurrency": "[currency code]",
              "availabilityStarts": "[YYYY-MM-DD]",
              "availabilityEnds": "[YYYY-MM-DD]"
            }
          }
        ]
      }
    ]
  }
}

Three checks before it goes live. Every item in the block is on the page, at the same price. Nothing in the block claims more than the page, a diet value included. And the block is in the HTML the server sends, not added later by a script. Google's structured data guidelines put the first two in their own terms: markup describes the visible page and must not mislead.

The block carries no rating: a café's own business node leaves reviews out of its markup, for the reasons in café reviews and listings. Nor is the block a lever for being named. It is a precise second copy of the page, for any parser that reads one.

Common questions

Our pastries change every day. Do we list every one?

List the ones you bake most days by name, with their labels, and add one line for the rest: "Daily bakes, from [time] until sold out; today's are on the counter." That tells a person what to expect and what to ask, and it stays true on the days a tray does not come out. Update the named list when a regular changes.

How do we mark up a drink that comes in two sizes?

One MenuItem for the drink, with two Offer entries in its offers, each with a name for the size, a price and a priceCurrency. The page shows both sizes and both prices on one line. If a milk costs extra, keep that charge in the Milks section as an add-on rather than multiplying the drinks by every milk.

Should the menu on our delivery app match the website?

Item for item and price for price, or you have two versions of your menu and one of them is wrong. If delivery prices are higher by design, say so on your menu page in one sentence, so anyone comparing the two knows why. Change both on the same day, and take seasonal items off both together.

Will a Menu block make an assistant recommend our oat latte?

Nobody can promise that, and we do not. The block states the item, its price and its milk in a form any parser reads, matching the page a person reads. Whether an assistant names your café for oat milk is something you can measure with a fixed panel of questions, as measuring a café's AI visibility explains.

Sources

The PDF and JavaScript shares are this site's own, from the audit named below, for the whole sample of 40 trades. The types and properties in the template were checked on schema.org when this was written; Google's guideline is linked where it is used.

Sources: our audit of 9,483 small-business sites, with its method · schema.org, Menu, MenuSection and MenuItem · schema.org, Offer · Google, structured data general guidelines

All articles