Script for Recipe Websites
Structured Data Requirements
After you've added the happycart script to your pages, all recipes contained in the website will be parsed and made shoppable. The script can only parse recipes which are structured according to Google's Structured Metadata for Recipes or which have our custom script tag. We strongly recommend structuring recipes this way regardless of your happycart usage, as this is essential for ranking well for recipes on Google.
Custom json data for happycart
Since schema data doesn’t natively support ingredient groups, and most recipes rely on them, we require a custom-structured data format for parsing when crawling recipe pages. This allows us to efficiently match recipes during the initial crawl and track changes (like updates to images, amounts, units, or ingredients) when revisiting the page.
To achieve this, each ingredient group and ingredient must have a stable, unique ID that remains consistent across requests. If a new ingredient or group is added, that’s acceptable, but existing ones should retain their original IDs. In the event an ingredient is modified, our system will handle those changes accordingly.
Example:
<script id="happycart-data" type="application/json">
{
"recipe_id": 43015,
"name": "Grilled Rib-Eye Steak mit Bratkartoffeln",
"image": "https://assets.happyplates.dev/media/2367/happy-plates-rezept-grilled-rib-eye-steak-mit-bratkartoffeln-fleisch-43015.jpg",
"servings": 4,
"serving_type": "PORTION",
"ingredient_groups": [
{
"id": "g-6982",
"name": "",
"order": 0,
"ingredients": [
{
"id": "ri-49427",
"order": 0,
"amount": "1000",
"unit": "g",
"prefix": "",
"name": "Rib Eye Steak",
"suffix": ""
},
{
"id": "ri-49428",
"order": 1,
"amount": "3",
"unit": "g",
"prefix": "",
"name": "Rosmarin",
"suffix": "frisch"
},
{
"id": "ri-49429",
"order": 2,
"amount": "2",
"unit": "EL",
"prefix": "",
"name": "Olivenöl",
"suffix": ""
},
{
"id": "ri-49430",
"order": 3,
"amount": "1",
"unit": "Prise",
"prefix": "",
"name": "Fleur de Sel",
"suffix": ""
},
{
"id": "ri-49431",
"order": 4,
"amount": "1",
"unit": "Prise",
"prefix": "",
"name": "Pfeffer",
"suffix": "frisch gemahlen"
}
]
},
{
"id": "g-6983",
"name": "Bratkartoffeln ",
"order": 1,
"ingredients": [
{
"id": "ri-49432",
"order": 0,
"amount": "1000",
"unit": "g",
"prefix": "",
"name": "Kartoffel",
"suffix": "festkochend"
},
{
"id": "ri-49433",
"order": 1,
"amount": "3",
"unit": "g",
"prefix": "",
"name": "Rosmarin",
"suffix": "frisch"
},
{
"id": "ri-49434",
"order": 2,
"amount": "1",
"unit": "Stk",
"prefix": "",
"name": "Zitrone",
"suffix": "unbehandelt"
},
{
"id": "ri-49435",
"order": 3,
"amount": "2",
"unit": "Stk",
"prefix": "",
"name": "Knoblauchzehe",
"suffix": ""
},
{
"id": "ri-49436",
"order": 4,
"amount": "6",
"unit": "EL",
"prefix": "",
"name": "Olivenöl",
"suffix": ""
},
{
"id": "ri-49437",
"order": 5,
"amount": "1",
"unit": "TL",
"prefix": "",
"name": "Fleur de Sel",
"suffix": ""
}
]
}
]
}
</script>
Example React Component
import type { FC } from "react"
import { useTranslation } from "react-i18next"
interface IngredientListJsonProps {
recipe: Domain.Recipe.DataTransferObjects.RecipeData
current_servings: number
}
interface FormattedData {
recipe_id: string // unique identifier for the recipe
name: string // name of the recipe
description: string // short description of the recipe
image: string // URL to the image, ideally a high resolution image
servings: number // integer
serving_type: string // either "PORTION" or "WHOLE"
ingredient_groups: {
id: string // id of the group, which shouldn't change between requests, so we can identify the group and compare if it has changed
name: string | null // name of the group
order: number // order of the group in the list of groups (array_index)
ingredients: {
id: string // id of the ingredient, which shouldn't change between requests, so we can identify the ingredient and compare if it has changed
order: number // order of the ingredient in the list of ingredients (array_index)
amount: string // string
unit: string // abbreviated unit (e.g. "g", "ml", "Stk.") - we can handle pretty much any unit string here, but we recommend using the abbreviated form
prefix: string | null // prefix of the ingredient (e.g. "halbierte, entsteinte")
name: string // name of the ingredient (e.g. "Zwetschken")
suffix: string | null // suffix of the ingredient (e.g. "frisch")
optional: boolean // if the ingredient is optional to use or not
}[]
}[]
}
const IngredientListJson: FC<IngredientListJsonProps> = ({ recipe }) => {
const { t } = useTranslation<string>("userArea")
// examples of formatted ingredients
// 5 Eiklar (Größe M)
// amount: 5, unit: "Stk.", prefix: "", name: "Eiklar", suffix: "Größe M"
// 800 g halbierte, entsteinte Zwetschken (frish)
// amount: 800, unit: "g", prefix: "halbierte, entsteinte", name: "Zwetschken", suffix: "frisch"
const formattedData: FormattedData = {
recipe_id: recipe.id,
name: recipe.name,
description: recipe.description,
image: recipe?.media?.permalink,
servings: recipe.default_servings,
serving_type: recipe.default_serving_name,
ingredient_groups: (recipe.ingredient_groups || []).map((group) => ({
id: `g-${group.key}`,
name: group.name || "",
order: group.order_column,
ingredients: (group.recipe_ingredients || []).map(function (ingredient) {
return {
id: `ri-${ingredient.key}`,
order: ingredient.order_column,
amount: `${ingredient.amount ?? 0}`,
unit: ingredient.unit
? t(`common:abbreviatedUnits.${ingredient.unit.toLowerCase()}`, { count: ingredient.amount ?? 0 })
: "",
prefix: ingredient?.prefix,
name: ingredient.name,
suffix: ingredient?.suffix,
optional: ingredient?.optional ?? false,
}
}),
})),
}
// Convert the formatted data to JSON and return it inside a script tag
return (
<script
id="happycart-data"
type="application/json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(formattedData),
}}
/>
)
}
export default IngredientListJson
Recipe Metadata Example
<html>
<head>
<title>Party Coffee Cake</title>
<script type="application/ld+json">
{
"@context": "https://schema.org/",
"@type": "Recipe",
"name": "Party Coffee Cake",
"image": [
"https://example.com/photos/1x1/photo.jpg",
"https://example.com/photos/4x3/photo.jpg",
"https://example.com/photos/16x9/photo.jpg"
],
"author": {
"@type": "Person",
"name": "Mary Stone"
},
"datePublished": "2018-03-10",
"description": "This coffee cake is awesome and perfect for parties.",
"prepTime": "PT20M",
"cookTime": "PT30M",
"totalTime": "PT50M",
"keywords": "cake for a party, coffee",
"recipeYield": "10",
"recipeCategory": "Dessert",
"recipeCuisine": "American",
"nutrition": {
"@type": "NutritionInformation",
"calories": "270 calories"
},
"recipeIngredient": [
"2 cups of flour",
"3/4 cup white sugar",
"2 teaspoons baking powder",
"1/2 teaspoon salt",
"1/2 cup butter",
"2 eggs",
"3/4 cup milk"
],
"recipeInstructions": [
{
"@type": "HowToStep",
"name": "Preheat",
"text": "Preheat the oven to 350 degrees F. Grease and flour a 9x9 inch pan.",
"url": "https://example.com/party-coffee-cake#step1",
"image": "https://example.com/photos/party-coffee-cake/step1.jpg"
},
{
"@type": "HowToStep",
"name": "Mix dry ingredients",
"text": "In a large bowl, combine flour, sugar, baking powder, and salt.",
"url": "https://example.com/party-coffee-cake#step2",
"image": "https://example.com/photos/party-coffee-cake/step2.jpg"
},
{
"@type": "HowToStep",
"name": "Add wet ingredients",
"text": "Mix in the butter, eggs, and milk.",
"url": "https://example.com/party-coffee-cake#step3",
"image": "https://example.com/photos/party-coffee-cake/step3.jpg"
},
{
"@type": "HowToStep",
"name": "Spread into pan",
"text": "Spread into the prepared pan.",
"url": "https://example.com/party-coffee-cake#step4",
"image": "https://example.com/photos/party-coffee-cake/step4.jpg"
},
{
"@type": "HowToStep",
"name": "Bake",
"text": "Bake for 30 to 35 minutes, or until firm.",
"url": "https://example.com/party-coffee-cake#step5",
"image": "https://example.com/photos/party-coffee-cake/step5.jpg"
},
{
"@type": "HowToStep",
"name": "Enjoy",
"text": "Allow to cool and enjoy.",
"url": "https://example.com/party-coffee-cake#step6",
"image": "https://example.com/photos/party-coffee-cake/step6.jpg"
}
],
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "5",
"ratingCount": "18"
},
"video": {
"@type": "VideoObject",
"name": "How to make a Party Coffee Cake",
"description": "This is how you make a Party Coffee Cake.",
"thumbnailUrl": [
"https://example.com/photos/1x1/photo.jpg",
"https://example.com/photos/4x3/photo.jpg",
"https://example.com/photos/16x9/photo.jpg"
],
"contentUrl": "http://www.example.com/video123.mp4",
"embedUrl": "http://www.example.com/videoplayer?video=123",
"uploadDate": "2018-02-05T08:00:00+08:00",
"duration": "PT1M33S",
"interactionStatistic": {
"@type": "InteractionCounter",
"interactionType": { "@type": "WatchAction" },
"userInteractionCount": 2347
},
"expires": "2019-02-05T08:00:00+08:00"
}
}
</script>
</head>
<body></body>
</html>
Testing the Metadata
You can test your structured data with Google's testing tool. If the page shows up with "Recipes" as detected structured data, even if it shows mistakes, it should be possible to crawl.
Working with Wordpress and other CMS
If you're using a CMS tool such as Wordpress, you should look into using a plugin that generates this metadata. A lot of SEO plugins also do this out-of-the-box, so it's likely the recipe is already correctly structured.