Schema.org JSON-LD per SEODEV
Genera dati strutturati Restaurant + Menu da iniettare nella pagina del cliente per migliorare la SERP Google.
Obiettivo: trasformare la response di GET /api/v1/menu in un blocco JSON-LD schema.org/Restaurant con hasMenu annidato. Google e Bing lo leggono e mostrano risultati arricchiti (rich snippet con prezzi, sezioni, immagini) nella SERP.
Perché serve
I motori di ricerca premiano i siti che descrivono i propri contenuti con vocabolario schema.org. Per un ristorante:
- Aiuta a comparire nelle ricerche "ristorante a Ragusa" con stelline e prezzo medio
- Su mobile, Google può mostrare il menu in carosello sotto il risultato
- Compatibile con Google Knowledge Panel (la card di destra nel SERP)
💡 MenuFacile già inietta JSON-LD sul menu pubblico ospitato su
<tenant>.menufacile.it. Questa ricetta serve quando il cliente ha il suo sito separato (es.ducatarocco.it) e vuole gli stessi vantaggi SEO sulla sua pagina "Il nostro menu".
Codice (Node.js / Next.js)
// app/menu/page.jsx (Next.js App Router)
async function getMenu() {
const res = await fetch('https://ducatarocco.menufacile.it/api/v1/menu/it', {
next: { revalidate: 600 }, // 10 min ISR
});
return res.json();
}
function buildJsonLd(data) {
const { settings, sections } = data;
return {
'@context': 'https://schema.org',
'@type': 'Restaurant',
name: settings.name,
address: {
'@type': 'PostalAddress',
streetAddress: settings.address,
},
telephone: settings.phone,
url: 'https://ducatarocco.it',
priceRange: '€€',
servesCuisine: 'Italian',
hasMenu: {
'@type': 'Menu',
name: `Menu di ${settings.name}`,
hasMenuSection: sections
.filter(s => s.is_active)
.map(section => ({
'@type': 'MenuSection',
name: section.name,
description: section.notes || undefined,
hasMenuItem: section.items.map(item => ({
'@type': 'MenuItem',
name: item.name,
description: item.description || undefined,
offers: {
'@type': 'Offer',
price: item.price.toFixed(2),
priceCurrency: 'EUR',
},
suitableForDiet: mapAllergensToDiet(item.allergens),
})),
})),
},
};
}
function mapAllergensToDiet(allergens = []) {
// Le key reali dell'API sono quelle di GET /allergens (con underscore).
const diets = [];
if (!allergens.includes('latte')) diets.push('https://schema.org/LowLactoseDiet');
if (!allergens.includes('cereali_glutine')) diets.push('https://schema.org/GlutenFreeDiet');
return diets.length ? diets : undefined;
}
export default async function MenuPage() {
const { data } = await getMenu();
const jsonLd = buildJsonLd(data);
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<main>
{/* il tuo HTML del menu qui */}
</main>
</>
);
}
Codice (PHP / WordPress)
Estende lo shortcode WordPress della ricetta precedente:
function menufacile_inject_jsonld($data) {
$sections = array_map(function ($section) {
return [
'@type' => 'MenuSection',
'name' => $section['name'],
'description' => $section['notes'] ?? null,
'hasMenuItem' => array_map(function ($item) {
return [
'@type' => 'MenuItem',
'name' => $item['name'],
'description' => $item['description'] ?? null,
'offers' => [
'@type' => 'Offer',
'price' => number_format((float) $item['price'], 2, '.', ''),
'priceCurrency' => 'EUR',
],
];
}, $section['items']),
];
}, array_filter($data['sections'], fn($s) => !empty($s['is_active'])));
$jsonld = [
'@context' => 'https://schema.org',
'@type' => 'Restaurant',
'name' => $data['settings']['name'],
'address' => [
'@type' => 'PostalAddress',
'streetAddress' => $data['settings']['address'] ?? '',
],
'telephone' => $data['settings']['phone'] ?? null,
'priceRange' => '€€',
'hasMenu' => [
'@type' => 'Menu',
'name' => "Menu di {$data['settings']['name']}",
'hasMenuSection' => array_values($sections),
],
];
echo '<script type="application/ld+json">'
. wp_json_encode($jsonld, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
. '</script>';
}
Da chiamare dentro wp_head solo sulla pagina del menu.
Validazione
- Apri lo Schema Markup Validator ufficiale
- Incolla l'URL della tua pagina del menu (live)
- Verifica che il blocco
Restaurantsia rilevato senza errori - Apri il Google Rich Results Test per simulare il rendering nella SERP
Quando aspettarti effetti
- Google indicizza l'aggiornamento dello schema in 1-4 settimane
- I rich result possono apparire o no: dipende dal ranking del sito e dalla qualità complessiva del markup
- Non esiste garanzia di rich snippet, ma il segnale aiuta in ranking
Considerazioni
Coerenza con la pagina
Lo schema JSON-LD deve corrispondere a ciò che è effettivamente visibile sulla pagina. Se ometti i prezzi nel JSON-LD ma li mostri nell'HTML (o viceversa), Google penalizza. Tieni l'unica fonte di verità: l'API.
Allergeni → suitableForDiet
Schema.org ha un vocabolario limitato per le diete (GlutenFreeDiet, LowLactoseDiet, VegetarianDiet, VeganDiet, ecc.). Mappare 14 allergeni MenuFacile a diete schema.org è imperfetto: la versione "negativa" (manca latte → low-lactose) è una semplificazione utile, non è prescrizione medica.
Prezzo come stringa
Schema.org richiede price come stringa in formato "9.00" (non float 9.0). Il toFixed(2) / number_format è obbligatorio.
Cache
Lo schema è inline nell'HTML: cambia solo quando cambia la pagina. La cache della response API (5-10 min) è sufficiente; non serve cache separata per il JSON-LD.
Hai bisogno di aiuto?
Scrivi a info@menufacile.it.