Riso-Illustration: Der Schmitdy-Delfin an einer Druckpresse im Hafen, eine Druckplatte speist zugleich einen gedruckten Bogen und einen leuchtenden Bildschirm

Dynamische Speisekartenseite bauen: die Anleitung mit getestetem Code (2026)

TL;DR

  • Eine dynamische Speisekartenseite ist eine, bei der die Speisekarte zuerst Daten und erst dann eine Seite ist. Eine strukturierte Quelle, serverseitig gerendert, wobei die Druckdatei aus denselben Daten generiert wird, statt selbst die Daten zu sein.
  • Der Bau ist klein. Eine funktionierende Version besteht aus einer typisierten Datendatei, einer Route, einem JSON-LD-Builder und einem Revalidierungs-Endpunkt. Der Code unten wurde end-to-end gebaut und getestet.
  • Der schwierige Teil ist die Wahl einer Source of Truth, die ein Manager tatsächlich aktuell hält, denn eine Speisekarte, die korrekt aussieht und drei Monate veraltet ist, ist schlimmer als ein PDF.
  • Rendern Sie serverseitig. Wir zeigen dieselbe Speisekarte auf zwei Arten gerendert, und die clientseitig gerenderte Version liefert bei einem einfachen Fetch überhaupt nichts zurück.
  • Nur elf Werte sind in suitableForDiet zulässig. Typisieren Sie das Feld so, dass ein falscher Wert nicht kompiliert.

Eine Speisekartenseite zu bauen, die Maschinen lesen können, sind rund zwei Tage Entwicklung. Die Entscheidungen drumherum dauern länger und sind der Teil, der schiefgeht, deshalb deckt diese Anleitung beides ab.

Was folgt, ist eine echte Implementierung, kein Pseudocode. Sie wurde mit Node v22.22.3, Next.js 16.3.4 und TypeScript 5.9.3 gebaut, und jede unten zitierte Befehlsausgabe ist die tatsächliche Ausgabe aus dem echten Lauf. Wenn Sie das Argument dafür suchen, warum das überhaupt wichtig ist, finden Sie es in unserem Beitrag darüber, warum eine PDF-Speisekarte für die KI-Suche unsichtbar ist.

Was zählt als dynamische Speisekartenseite?

Nicht eine Speisekarte, die animiert. Eine Speisekarte, bei der der Inhalt strukturierte Daten sind, aus denen eine Seite generiert wird, was Ihnen vier Dinge gibt, die ein PDF nicht kann:

  1. Text, den ein Crawler lesen kann, ohne irgendetwas auszuführen.
  2. Eine eigene Adresse und Überschrift für jeden Standort und jede Speisekarte.
  3. Maschinenlesbare Preise, Abschnitte und Ernährungskennzeichnung.
  4. Eine einzige Stelle, um ein Gericht zu ändern, wobei jede Ausgabe gemeinsam aktualisiert wird.

Der vierte Punkt ist der, der entscheidet, ob das den Kontakt mit einer echten Küche übersteht.

Schritt 1: Wählen Sie die Source of Truth, bevor Sie Code schreiben

Alles Nachgelagerte ist einfach. Das ist die Entscheidung, die bestimmt, ob die Speisekarte im nächsten Frühjahr noch korrekt ist.

Source of TruthWer sie bearbeiten kannGut geeignet fürDas Risiko
Eine typisierte Datei im RepositoryNur EntwicklerEinen einzelnen Standort, einen technischen Verantwortlichen, einen schnellen StartJede saisonale Änderung braucht einen Entwickler, deshalb veraltet sie
Ein headless CMS wie Sanity oder ContentfulJeder, mit einer echten BearbeitungsoberflächeGruppen, mehrere Redakteure, geplante ÄnderungenKosten und Einrichtungszeit, bevor überhaupt etwas live geht
Eine Tabelle, die nach Zeitplan synchronisiert wirdJeder im HausTeams, die ohnehin in einer Tabelle lebenKeine Validierung, ein Tippfehler wird so zum veröffentlichten Preis
Ihr bestehendes Website-CMSWer die Website heute bearbeitetBei einer Plattform zu bleibenHängt komplett von der Plattform ab, siehe die Tabelle gegen Ende

Entscheiden Sie danach, wer die Änderung tatsächlich vornimmt, freitags um 16 Uhr, wenn ein Lieferant nicht liefert. Wenn die Antwort ein Manager ist und kein Entwickler, brauchen Sie eine Bearbeitungsoberfläche, und Sie sollten das jetzt entscheiden, nicht erst nach dem Bau.

Beginnen Sie das Datenmodell mit den Feldern, die wirklich gebraucht werden, denn sie nachträglich einzubauen ist schmerzhaft: Standort, Speisekarte, Menütyp, Gültigkeitszeitraum, Abschnitt, Artikel, Beschreibung, Preis, Währung, Ernährungskennzeichen, Allergene, Verfügbarkeit, ob eine Speisekarte über Standorte hinweg geteilt wird oder nur für einen gilt, und Sortierreihenfolge.

Diese Unterscheidung zwischen geteilt und standortspezifisch ist wichtiger, als sie aussieht. In einer Gruppe sind manche Speisekarten geteilt und manche gehören zu einem einzigen Standort. Modellieren Sie das von Tag eins an als Many-to-many-Beziehung, sonst landen Sie beim Kopieren von Seiten, und die Gerichte eines Standorts tauchen auf der Speisekarte eines anderen Standorts auf, ein Fehler, den wir auf mehr als einer Plattform gesehen haben.

Schritt 2: Typisieren Sie die Speisekarte, damit falsche Daten nicht kompilieren

Beginnen Sie mit der Ernährungs-Enumeration, denn das ist die eine Stelle, an der Leute stillschweigend Werte erfinden.

// lib/diets.ts
// The full schema.org RestrictedDiet enumeration. Exactly these eleven.
// There is no DairyFreeDiet and no NutFreeDiet, so allergen information is
// carried as free text on the item instead.
export const RESTRICTED_DIETS = [
  "DiabeticDiet",
  "GlutenFreeDiet",
  "HalalDiet",
  "HinduDiet",
  "KosherDiet",
  "LowCalorieDiet",
  "LowFatDiet",
  "LowLactoseDiet",
  "LowSaltDiet",
  "VeganDiet",
  "VegetarianDiet",
] as const;

export type RestrictedDiet = (typeof RESTRICTED_DIETS)[number];

export function dietToIri(diet: RestrictedDiet): string {
  return `https://schema.org/${diet}`;
}

Typisieren Sie das diets-Feld eines Artikels als RestrictedDiet[], und ein erfundener Wert hört auf, ein stiller Datenfehler zu sein, und wird zu einem Build-Fehler. Wir haben das getestet, indem wir DairyFreeDiet in die Daten eingefügt und den Type Checker laufen lassen haben:

error TS2322: Type '"DairyFreeDiet"' is not assignable to type
'"DiabeticDiet" | "GlutenFreeDiet" | ... | "VegetarianDiet"'

Genau das ist der Sinn, das in einer typisierten Sprache zu tun. Der Compiler erzwingt das Vokabular, sodass ein Mensch es sich nicht merken muss.

Schritt 3: Rendern Sie die Seite auf dem Server

Die Speisekarte muss im HTML stehen, das der Server sendet, bevor irgendein JavaScript läuft. In Next.js bedeutet das eine Server-Komponente und generateStaticParams, damit jede Kombination aus Standort und Speisekarte zur Build-Zeit generiert wird.

// app/menus/[venue]/[menu]/page.tsx
import { notFound } from "next/navigation";
import { getAllVenueMenuParams, getMenuForVenue, getVenue } from "@/data/menu-data";
import { buildMenuJsonLd } from "@/lib/schema";

export function generateStaticParams() {
  return getAllVenueMenuParams();
}

export default async function MenuPage({
  params,
}: PageProps<"/menus/[venue]/[menu]">) {
  const { venue: venueSlug, menu: menuSlug } = await params;

  const venue = getVenue(venueSlug);
  const menu = getMenuForVenue(venueSlug, menuSlug);
  if (!venue || !menu) {
    notFound();
  }

  const jsonLd = buildMenuJsonLd(venue, menu);

  return (
    <main>
      <h1>{`${venue.name} ${menu.name}`}</h1>
      {menu.sections.map((section) => (
        <section key={section.name}>
          <h2>{section.name}</h2>
          <ul>
            {section.items.map((item) => (
              <li key={item.slug}>
                <strong>{item.name}</strong>
                <span>{item.currency} {item.price.toFixed(2)}</span>
                <p>{item.description}</p>
                {item.allergens.length > 0 && (
                  <p>Allergens: {item.allergens.join(", ")}</p>
                )}
              </li>
            ))}
          </ul>
        </section>
      ))}
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
    </main>
  );
}

Zwei Details darin sind wichtiger, als sie aussehen.

Die Überschrift variiert. ${venue.name} ${menu.name} erzeugt für jede Kombination eine andere H1. Eine einzige statische Überschrift, die für jeden Standort und jede Speisekarte wiederverwendet wird, ist einer der häufigsten Fehler bei einem Multi-Standort-Bau, und er macht einen Großteil des Nutzens separater Seiten überhaupt zunichte. Geprüft über drei Routen:

$ curl -s http://localhost:3417/menus/borough-market/dinner | grep -o '<h1>[^<]*</h1>'
<h1>Borough Market Dinner Menu</h1>

$ curl -s http://localhost:3417/menus/shoreditch/dinner | grep -o '<h1>[^<]*</h1>'
<h1>Shoreditch Dinner Menu</h1>

$ curl -s http://localhost:3417/menus/borough-market/sunday-roast | grep -o '<h1>[^<]*</h1>'
<h1>Borough Market Sunday Roast</h1>

Schreiben Sie die Überschrift als einen einzigen Ausdruck. Das wirkt pedantisch, ist es aber nicht. Wenn Sie <h1>{venue.name} {menu.name}</h1> als zwei benachbarte Ausdrücke schreiben, fügt React zwischen ihnen einen leeren HTML-Kommentar in die Server-Ausgabe ein, was still jedes Tooling bricht, das die Überschrift mit einem einfachen Muster liest. Ein einzelnes Template-Literal als Kind vermeidet das. Uns ist das während des Baus real passiert.

Schritt 4: Generieren Sie die strukturierten Daten aus denselben Objekten

Schreiben Sie JSON-LD niemals von Hand neben eine Speisekarte. Es driftet innerhalb einer Saison auseinander, und ein Schema-Block, der der sichtbaren Seite widerspricht, ist schlimmer als gar keiner.

// lib/schema.ts
export function buildMenuJsonLd(venue: Venue, menu: Menu) {
  return {
    "@context": "https://schema.org",
    "@type": "Restaurant",
    name: venue.name,
    address: {
      "@type": "PostalAddress",
      streetAddress: venue.streetAddress,
      addressLocality: venue.city,
      postalCode: venue.postalCode,
      addressCountry: "GB",
    },
    hasMenu: {
      "@type": "Menu",
      name: menu.name,
      hasMenuSection: menu.sections.map((section) => ({
        "@type": "MenuSection",
        name: section.name,
        hasMenuItem: section.items.map((item) => ({
          "@type": "MenuItem",
          name: item.name,
          description: item.description,
          offers: {
            "@type": "Offer",
            price: item.price,
            priceCurrency: item.currency,
          },
          suitableForDiet: item.diets.map(dietToIri),
        })),
      })),
    },
  };
}

Weil es aus denselben Objekten gebaut wird, die die Seite rendert, können die beiden nicht widersprüchlich sein. Setzen Sie Ihre Erwartungen trotzdem richtig: Google dokumentiert kein eigenes Rich-Result für Menu, und seine Local-Business-Anleitung verlangt nur eine Menü-URL. Veröffentlichen Sie das, weil es Ihre Fakten für jeden Konsumenten explizit macht, nicht weil ein Rich-Result darauf wartet.

Schritt 5: Machen Sie die gedruckte Speisekarte zu einer abgeleiteten Ausgabe

Das ist der Schritt, der den einzigen echten Einwand gegen die ganze Übung beantwortet, nämlich dass das PDF existiert, weil die Küche etwas zum Ausdrucken braucht.

Pflegen Sie nicht zwei Speisekarten. Generieren Sie die druckbare aus denselben Daten:

$ npx tsx scripts/generate-print-menu.ts borough-market dinner
Wrote out/borough-market-dinner-print.html

Ein Print-Stylesheet über generiertem HTML reicht, und es lässt sich leichter neu gestalten als eine PDF-Pipeline. Der Designer behält das Layout, die Küche behält ihre gedruckte Speisekarte, und niemand pflegt dieselbe Gerichtliste zweimal.

Schritt 6: Ermöglichen Sie, eine Änderung ohne Deploy zu veröffentlichen

Wenn eine Preisänderung einen Entwickler braucht, ist die Speisekarte bis März falsch. Stellen Sie einen Revalidierungs-Endpunkt bereit, den das CMS oder ein internes Tool aufrufen kann:

// app/api/revalidate/route.ts
export async function POST(request: Request) {
  const secret = request.headers.get("x-revalidate-secret");
  if (!process.env.REVALIDATE_SECRET || secret !== process.env.REVALIDATE_SECRET) {
    return Response.json({ revalidated: false }, { status: 401 });
  }
  const { path } = await request.json();
  revalidatePath(path);
  return Response.json({ revalidated: true, path });
}

Getestet mit einem falschen Secret, ohne konfiguriertes Secret und mit dem richtigen Secret, mit den Rückgaben 401, 401 und:

{"revalidated":true,"path":"/menus/borough-market/dinner"}

Sichern Sie ihn mit einem Secret ab und behandeln Sie eine fehlende Umgebungsvariable als Fehlschlag statt als offene Tür.

Schritt 7: Beweisen Sie, dass eine Maschine sie wirklich lesen kann

Das ist der Abnahmetest, und es ist der eine Schritt, den niemand auslassen sollte. Prüfen Sie die Rohantwort, niemals den Browser. Ein Browser führt Ihr JavaScript aus, er wird Ihnen also sagen, die Speisekarte sei in Ordnung, wenn sie es nicht ist.

Wir haben dieselbe Speisekarte zweimal aus einer Datendatei gebaut, um die Größe des Unterschieds zu zeigen. Serverseitig gerendert:

$ curl -s http://localhost:3417/menus/borough-market/dinner | grep -o "Bavette Steak"
Bavette Steak

$ curl -s http://localhost:3417/menus/borough-market/dinner | grep -o "26.00" | head -1
26.00

Dasselbe Gericht, auf einer Seite, die ihre Speisekarte stattdessen im clientseitigen State setzt:

$ curl -s http://localhost:3417/csr-demo | grep -o "Bavette Steak"
$ curl -s http://localhost:3417/csr-demo | grep -o '<main><h1>[^<]*</h1></main>'
<main><h1>Loading menu…</h1></main>

Nichts. Der gesamte Rohtext ist ein Lade-Platzhalter. Im Browser identisch, bei einem einfachen Fetch unsichtbar.

Ihre Abnahme-Checkliste:

curl -s <your-menu-url> | grep -o "<a dish you are known for>"
curl -s <your-menu-url> | grep -o '<h1>[^<]*</h1>'
curl -s <your-menu-url> | grep -c "application/ld+json"
curl -sI <your-menu-url> | grep -i "x-robots-tag"
curl -s <your-domain>/robots.txt

Die vierte Zeile fängt den Fehler ab, der bei dieser Art von Bau am meisten Geld verbrennt: ein noindex, das noch von Staging übrig ist. Eine perfekte Speisekarte, die Engines sagt, sie sollen nicht hinschauen, ist ein Totalverlust, und er ist unsichtbar, wenn Sie nicht den Header prüfen.

So bauen Sie das mit Claude Code oder Codex

Beide kommen damit gut zurecht, weil die Aufgabe klein, typisiert und testbar ist. Was das Ergebnis verändert, ist, dem Modell das Constraint-Set zu geben statt die Feature-Anfrage.

Ein Prompt, der funktioniert:

Bauen Sie in dieser Next.js-App eine serverseitig gerenderte Menü-Route unter /menus/[venue]/[menu]. Lesen Sie die Speisekarte aus einem einzigen typisierten Datenmodul. Anforderungen: Jeder Gerichtname und jeder Preis muss im rohen HTML erscheinen, ohne dass JavaScript ausgeführt wird; die H1 muss je nach Standort und Speisekarte variieren und als einzelnes Template-Literal geschrieben sein; erzeugen Sie Restaurant > hasMenu > Menu > hasMenuSection > hasMenuItem-JSON-LD, gebaut aus denselben Objekten, die die Seite rendert; typisieren Sie suitableForDiet gegen die elf echten schema.org-RestrictedDiet-Werte, sodass ein ungültiger Wert den Type Check scheitern lässt. Verifizieren Sie danach mit curl und zeigen Sie mir die Ausgabe.

Der letzte Satz macht den Großteil der Arbeit. Fragen Sie nach der Verifizierung, bekommen Sie einen Agenten, der curl ausführt und die Antwort liest. Lassen Sie ihn weg, bekommen Sie Code, der richtig aussieht.

Zwei weitere Gewohnheiten lohnen sich. Fragen Sie auch nach dem Fehlerfall, also einer absichtlich clientseitig gerenderten Version, damit Sie den Kontrast in Ihrem eigenen Projekt sehen statt es einfach zu glauben. Und lassen Sie nach der Generierung der Datendatei den Type Check laufen, denn ein erfundener Ernährungswert ist der mit Abstand wahrscheinlichste Fehler bei diesem Bau, und der Compiler fängt ihn sofort ab.

Wenn Sie keinen Individualbau umsetzen können

Die meisten Restaurants werden keine Next.js-App betreiben, und sie müssen es auch nicht. Entscheidend ist, ob die Plattform lesbaren Text plus strukturierte Daten ausgeben kann, die eine technisch nicht versierte Person aktuell halten kann.

PlattformDer WegWorauf Sie achten sollten
SquarespaceNatives Menu-Block für den Text, seitenweite Code Injection für JSON-LDCode Injection braucht mindestens Core, Squarespace gibt auf jeder Seite eigenes Local-Business-Schema aus, das mit Ihrem kollidieren kann, und der Menu-Block speist Ihr injiziertes Schema nicht, sodass die beiden auseinanderdriften
WebflowEine CMS Collection für Artikel und Abschnitte, mit JSON-LD gebunden an CMS-Felder im Collection TemplateFeld- und Zeichenlimits bei einer umfangreichen Speisekarte, und jede zusätzliche Sprachversion braucht ihr eigenes Schema
WordPressEin dediziertes Menü-Plugin oder handgeschriebenes JSON-LD im TemplateYoast und Rank Math generieren kein Menu-Schema, was die allgemeinen Ratschläge auch behaupten
ShopifyMetaobjects für Speisekarteninhalte plus JSON-LD im ThemeGerichte als Produkte zu modellieren zieht Checkout-Verhalten mit sich, das Sie nicht wollen
WixDie App Restaurants MenusDie Dokumentation sagt nicht, was ihre Option für strukturierte Daten ausgibt, prüfen Sie also selbst die Live-Seite
Toast, Popmenu, Flipdish, UpMenuDie eigenen Speisekartenseiten des Anbieters auf Ihrer DomainDie Anbindung Ihrer Domain ist ein separater Schritt, und eine nicht angebundene Seite wird nicht gefunden

Unabhängig von der Plattform: Packen Sie die Speisekarte nicht in einen iFrame und verlassen Sie sich nicht darauf, dass ein Buchungs-Widget sie ausliefert. Eingebetteter Anbieterinhalt trägt häufig ein eigenes noindex, und ein Assistent, der das Skript nie ausführt, sieht ohnehin nie in den Frame hinein.

Die Fallen, in die wir getappt sind

Zwei Dinge haben bei diesem Bau echte Zeit gekostet, sie warten wahrscheinlich auch auf Sie.

Benachbarte JSX-Ausdrücke in der Überschrift. Oben behandelt. Das führte zu einer Überschrift, die im Browser korrekt aussah und an einer einfachen automatisierten Prüfung scheiterte, die schlimmste Art von Fehler, weil der sichtbare Befund sagt, alles sei in Ordnung.

Pfad-Aliasse in eigenständigen Skripten. Ein einfaches Node-Skript kann kein TypeScript-Modul importieren, das einen @/-Alias verwendet. Führen Sie solche Skripte über tsx aus, das die Pfade in Ihrer TypeScript-Konfiguration respektiert.

Wofür sich das lohnt

Der Bau ist der kleine Teil. Der Wert entsteht daraus, dass die Speisekarte zu Daten wird: eine Stelle, um ein Gericht zu ändern, eine Seite pro Standort, eine Druckdatei, die nicht mehr aus dem Takt geraten kann, und Gericht-Vokabular, das endlich irgendwo existiert, wo eine Maschine es lesen kann.

Wir übernehmen diese Arbeit für Restaurants im Rahmen der monatlichen Gebühr statt als separates Projektangebot, für jeden Standort und jede Speisekarte. Wenn Sie das lieber nicht selbst bauen möchten, das ist unser Angebot.

Quellen

  1. Schema.org, "RestrictedDiet" und "suitableForDiet", https://schema.org/RestrictedDiet, geprüft 2026-09-05.
  2. Google Search Central, "Local Business (LocalBusiness) Structured Data", https://developers.google.com/search/docs/appearance/structured-data/local-business, zuletzt aktualisiert 10 December 2025, geprüft 2026-09-05.
  3. Google Search Central, "Structured Data Markup that Google Search Supports", https://developers.google.com/search/docs/appearance/structured-data/search-gallery, zuletzt aktualisiert 15 June 2026, geprüft 2026-09-05.
  4. Squarespace Help Center, "Using code injection", https://support.squarespace.com/hc/en-us/articles/205815908-Using-code-injection, geprüft 2026-09-05.
  5. Squarespace Help Center, "Menu blocks", https://support.squarespace.com/hc/en-us/articles/206544087-Menu-blocks, geprüft 2026-09-05.

Häufig gestellte Fragen

Marco Lobo
Marco Lobo

Gründer, Schmitdy

Marco entwickelt Wachstumssysteme für die KI-Suche, die Prompts, Quellen, Inhalte und Agenten in Umsatz verwandeln.

Wir bauen Ihre Speisekartenseiten, ohne AufpreisJeder Standort, jede Karte, raus aus dem PDF und rein in Seiten, die Handy und Crawler lesen können. Enthalten in Managed AI Search ab 1.450 € im Monat, nicht separat berechnet.
Speisekarten umbauen lassen

Ähnliche Artikel

Passt zu Ihrem Website-Stack

Behalten Sie die Plattform. Verbessern Sie das Ergebnis.

Plattform-Funktionen ansehen
SquarespaceVercelWebflowSanityShopifyHubSpot