AI-powered menu generation: van user prompt naar structured output in 3 minuten
AI-Powered Menu Generation: Van User Prompt naar Structured Output in 3 Minuten
Het Probleem: Menu Samenstellen Kost Belachelijk Veel Tijd
Restaurant eigenaren kennen de pijn:
- π 2 uur staren naar recepten
- π Eindeloos wikken en wegen tussen gangen
- π§ Decision fatigue al voordat je eerste gast binnenkomt
Of je nu een kerstdiner plant voor 40 personen of een 21-diner voor jezelf, het is hetzelfde verhaal: te veel opties, te weinig structuur.
De Oplossing: Structured AI Output in 180 Seconden
We bouwden een systeem dat:
- β Begrijpt wat je wilt (natural language input)
- β Genereert complete menu's (AI-powered)
- β Levert gestructureerde data (geen rommelende tekstblobs)
- β Werkt in 3 minuten (inclusief verfijnen)
Tech Stack:
- Frontend: React + TypeScript
- Backend: Supabase Edge Functions
- AI: Lovable AI (Gemini 2.5 Flash)
- Type Safety: Zod schemas
Laten we onder de motorkap duiken π§
Architectuur: Van Input naar Structured Menu
Flow Diagram
User Input (dietary preferences, allergies, etc.)
β
React Form Validation
β
Supabase Edge Function
β
Lovable AI Gateway (Gemini 2.5 Flash)
β
Tool Calling (Zod Schema Validation)
β
Structured JSON Output
β
Frontend Rendering + PDF Export
Stap 1: Type-Safe User Input
Zod Schema voor Preferences
// src/lib/schemas/menuPreferences.ts
import { z } from "zod";
export const menuPreferencesSchema = z.object({
dietaryRestrictions: z.array(z.string()).default([]),
allergies: z.array(z.string()).default([]),
cuisinePreference: z.string().optional(),
spicyLevel: z.enum(["mild", "medium", "hot"]).default("medium"),
alcoholPreference: z.enum(["with", "without", "optional"]).default("optional"),
courses: z.number().min(3).max(7).default(4),
});
export type MenuPreferences = z.infer<typeof menuPreferencesSchema>;
Waarom Zod?
- β Runtime validation (catch bugs vroeg)
- β TypeScript types worden automatisch gegenereerd
- β Developer experience: autocomplete + type checking
Stap 2: Edge Function met AI Tool Calling
Prompt Engineering voor Tool Calling
// supabase/functions/generate-menu/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};
serve(async (req) => {
if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });
try {
const { preferences, theme } = await req.json();
const LOVABLE_API_KEY = Deno.env.get("LOVABLE_API_KEY");
// System prompt based on theme (kerst vs 21-diner)
const systemPrompt = theme === "christmas"
? "Je bent een AI chef gespecialiseerd in feestelijke kerstdiners..."
: "Je bent een AI chef gespecialiseerd in 21-diner menu samenstelling...";
const response = await fetch("https://ai.gateway.lovable.dev/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${LOVABLE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "google/gemini-2.5-flash",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: `Maak een menu met: ${JSON.stringify(preferences)}` }
],
tools: [
{
type: "function",
function: {
name: "create_menu",
description: "Genereer een gestructureerd restaurant menu",
parameters: {
type: "object",
properties: {
menu: {
type: "object",
properties: {
title: { type: "string" },
courses: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
dish_name: { type: "string" },
description: { type: "string" },
pairing: {
type: "object",
properties: {
drink: { type: "string" },
why: { type: "string" }
}
}
},
required: ["name", "dish_name", "description", "pairing"]
}
}
},
required: ["title", "courses"]
}
},
required: ["menu"]
}
}
}
],
tool_choice: { type: "function", function: { name: "create_menu" } }
}),
});
const data = await response.json();
const toolCall = data.choices[0].message.tool_calls[0];
const menuData = JSON.parse(toolCall.function.arguments);
return new Response(
JSON.stringify({ success: true, menu: menuData.menu }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
} catch (error) {
console.error("Error generating menu:", error);
return new Response(
JSON.stringify({ success: false, error: error.message }),
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
});
Stap 3: Frontend Integration
React Hook voor Menu Generation
// src/hooks/useMenuGeneration.ts
import { useState } from "react";
import { supabase } from "@/integrations/supabase/client";
import { useToast } from "@/hooks/use-toast";
import type { MenuPreferences } from "@/lib/schemas/menuPreferences";
export function useMenuGeneration(theme: "christmas" | "casual") {
const [isLoading, setIsLoading] = useState(false);
const [menu, setMenu] = useState(null);
const { toast } = useToast();
const generateMenu = async (preferences: MenuPreferences) => {
setIsLoading(true);
try {
const { data, error } = await supabase.functions.invoke("generate-menu", {
body: { preferences, theme }
});
if (error) throw error;
setMenu(data.menu);
toast({
title: "β
Menu gegenereerd!",
description: "Je menu is klaar om te bekijken.",
});
} catch (error) {
toast({
title: "β Fout",
description: "Er ging iets mis. Probeer opnieuw.",
variant: "destructive"
});
} finally {
setIsLoading(false);
}
};
return { generateMenu, menu, isLoading };
}
Waarom Tool Calling > Prompt Hacking?
Zonder Tool Calling (oude methode):
// β Fragile prompt engineering
const prompt = `
Genereer een JSON menu in dit formaat:
{
"title": "...",
"courses": [...]
}
BELANGRIJK: Output ALLEEN valid JSON, geen extra tekst!
`;
// Problemen:
// - Model voegt soms "Hier is je menu:" toe vΓ³Γ³r JSON
// - JSON formatting errors (trailing commas, quotes)
// - Geen schema validatie β runtime crashes
Met Tool Calling (nieuwe methode):
// β
Gegarandeerd structured output
tools: [{
function: {
parameters: {
type: "object",
properties: { /* exact schema */ }
}
}
}]
// Voordelen:
// β
Model MOET aan schema voldoen
// β
Geen parsing errors
// β
Type-safe vanaf AI tot frontend
Performance: Hoe Halen We 3 Minuten?
Bottleneck Analyse
| Stap | Tijd | Optimalisatie | |------|------|--------------| | User input | 30s | Autocomplete + defaults | | API call | 5-8s | Gemini Flash (snelste model) | | Rendering | < 1s | React virtualization | | Refinement | 60-90s | Partial regeneration |
Total: ~3 minuten inclusief user tijd.
Model Keuze: Gemini Flash vs Pro
// Gemini 2.5 Flash (onze keuze)
// - Latency: 5-8 seconden voor 4-gangen menu
// - Cost: β¬0.02 per menu
// - Quality: Perfect voor gestructureerde output
// Gemini 2.5 Pro (overkill)
// - Latency: 12-15 seconden
// - Cost: β¬0.08 per menu
// - Quality: Marginaal beter, niet waard voor use case
Conclusie: Flash is de sweet spot voor menu generation.
Lessons Learned: 3 Maanden Productie
1. Dietary Restrictions Are Complex
Eerste versie:
dietaryRestrictions: z.enum(["vegetarian", "vegan", "gluten-free"])
Realiteit:
dietaryRestrictions: z.array(z.string()).default([])
// Users combineren: "vegan + gluten-free + low-sodium"
Fix: Multi-select met custom input field.
2. Alcohol is Culturally Sensitive
// β Assumed everyone wants wine pairing
pairing: { type: "wine", name: "..." }
// β
Added user preference
alcoholPreference: z.enum(["with", "without", "optional"])
// Model suggests tea/juice alternatives if "without"
3. Menu Length Variability
// β Fixed 4 courses
courses: z.array(courseSchema).length(4)
// β
User-defined range
courses: z.number().min(3).max(7).default(4)
// Christmas dinners often want 5-7 courses
SEO & Discovery: Hoe Vinden Klanten Ons?
Primary Keywords
- "kerstmenu generator AI"
- "21 diner menu samenstellen"
- "restaurant menu maken met AI"
Content Strategy
- Blog posts (like this one) β SEO voor educational queries
- Tool pages (
/kerst,/21-diner) β Conversion optimized - Case studies β Social proof + backlinks
Result: 60% organic traffic binnen 8 weken na launch.
Conclusie: Structured AI is The Future
Wat we bouwden:
- β Type-safe AI integration
- β Sub-10 second generation
- β Foolproof structured output
- β Production-ready error handling
Waarom dit schaalt:
- π Zelfde architectuur voor alle seizoenen (Pasen, BBQ, Valentijn)
- π Easy internationalization (Engels, Frans support)
- π A/B testing via prompt variations
Probeer Het Zelf
Klaar om in 3 minuten je eigen menu te genereren?
- π Kerstdiner Generator - Perfect voor feestdagen
- π½οΈ 21-Diner Generator - Klassiek Nederlands menu
Of bouw je eigen versie:
Volledige source code + architectuur breakdown beschikbaar in ons GitHub repo.
Questions? Stuur me een DM op LinkedIn of check onze andere blog posts voor meer AI implementation deep-dives.
Wil je weten hoe deze AI-oplossing voor jouw bedrijf werkt?