MenuFacile2.0

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:

💡 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

  1. Apri lo Schema Markup Validator ufficiale
  2. Incolla l'URL della tua pagina del menu (live)
  3. Verifica che il blocco Restaurant sia rilevato senza errori
  4. Apri il Google Rich Results Test per simulare il rendering nella SERP

Quando aspettarti effetti

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.