What is Soustack Core?

Soustack Core is the reference implementation for the Soustack Standard. It provides validation, parsing, and scaling logic to turn static recipe data into dynamic, computable objects.

Most recipe formats (like Schema.org) are descriptive—they tell you what a recipe is. Soustack is computational—it understands how a recipe behaves.

The Problems We Solve

  • The "Salty Soup" Problem (Intelligent Scaling):
    Old Way: Doubling a recipe doubles every ingredient blindly.
    Soustack: Understands that salt scales differently than flour, and frying oil shouldn't scale at all. It supports Linear, Fixed, Discrete, and Baker's Percentage scaling modes.
  • The "Lying Prep Time" Problem:
    Old Way: Authors guess "Prep: 15 mins."
    Soustack: Calculates total time dynamically based on the active/passive duration of every step.
  • The "Timing Clash" Problem:
    Old Way: A flat list of instructions.
    Soustack: A Dependency Graph that knows you can chop vegetables while the water boils.

Installation

Install soustack@0.2.0 (or newer) to access the normalized image fields described in this guide.

npm install soustack@0.2.0

Image support requires Soustack Core 0.2.0 or later.

What's Included

  • Validation: validateRecipe() validates Soustack JSON against the bundled schema.
  • Scaling & Computation: scaleRecipe() produces a flat, UI-ready "computed recipe" (scaled ingredients + aggregated timing).
  • Parsers:
    • Ingredient parsing (parseIngredient, parseIngredientLine)
    • Duration parsing (smartParseDuration)
    • Yield parsing (parseYield)
  • Schema.org Conversion:
    • fromSchemaOrg() (Schema.org JSON-LD → Soustack)
    • toSchemaOrg() (Soustack → Schema.org JSON-LD)
  • Web Scraping: scrapeRecipe() fetches a recipe page and extracts Schema.org recipe data from:
    • JSON-LD (<script type="application/ld+json">)
    • Microdata (itemscope/itemtype)

Programmatic Usage

Basic Operations

import {
  scrapeRecipe,
  fromSchemaOrg,
  toSchemaOrg,
  validateRecipe,
  scaleRecipe
} from 'soustack';

// Validate a Soustack recipe JSON object
validateRecipe(recipe);

// Scale a recipe to a target yield amount (returns a "computed recipe")
const computed = scaleRecipe(recipe, 2);

// Scrape a URL into a Soustack recipe (throws if no recipe is found)
const scraped = await scrapeRecipe('https://example.com/recipe');

// Convert Schema.org → Soustack
const soustack = fromSchemaOrg(schemaOrgJsonLd);

// Convert Soustack → Schema.org
const jsonLd = toSchemaOrg(recipe);

Scaling Recipes

The scaleRecipe() function handles intelligent scaling based on the scaling rules defined in each ingredient. It respects:

  • Linear scaling: Ingredients scale proportionally
  • Fixed scaling: Ingredients don't scale (e.g., frying oil)
  • Discrete scaling: Ingredients scale in discrete steps
  • Baker's Percentage: Ingredients scale relative to a reference ingredient
import { scaleRecipe } from 'soustack';

// Scale to 2x the original yield
const doubled = scaleRecipe(recipe, 2);

// Scale to a specific number of servings
const scaled = scaleRecipe(recipe, { servings: 8 });

Schema.org Conversion

Use the conversion helpers to move between Schema.org JSON-LD and Soustack's structured recipe format.

import { fromSchemaOrg, toSchemaOrg } from 'soustack';

// Convert from Schema.org to Soustack
const soustackRecipe = fromSchemaOrg(schemaOrgJsonLd);

// Convert from Soustack to Schema.org
const schemaOrgRecipe = toSchemaOrg(soustackRecipe);

Working with Images

Soustack 0.2.0 adds normalized image fields across recipes and instructions. Use the helpers in soustack or the guidelines below to maintain parity with Schema.org.

Extracting from Schema.org

Input FormatHow to Handle
stringUse directly as the hero image URL.
string[]Preserve order; treat the first entry as the primary image.
ImageObjectExtract the url property. Ignore objects without URLs.
ImageObject[]Map to an array of url values, dropping nulls and duplicates.

Image normalization

  • Accept string or string[] and normalize to an array internally.
  • Trim whitespace, resolve relative paths, and remove falsy entries.
  • Deduplicate URLs while keeping the first entry for hero placement.
  • Mirror the normalized array back to string when exporting a single URL.

Detecting image patterns

function detectPattern(recipe) {
  const hasHero = Array.isArray(recipe.image)
    ? recipe.image.length > 0
    : !!recipe.image;

  const hasSteps = (recipe.instructions || []).some((instruction) =>
    typeof instruction !== 'string' && !!instruction.image
  );

  if (hasHero && hasSteps) return 'hybrid';
  if (hasHero) return 'hero';
  if (hasSteps) return 'step-by-step';
  return 'none';
}

UI recommendations

  • Hero: Display a large image above the title or header band.
  • Step-by-step: Render inline thumbnails adjacent to each instruction.
  • Hybrid: Combine a hero image with inline step callouts.
  • None: Hide the image container or show a neutral placeholder.

Handling multiple images

Use the first URL for your primary slot and expose the rest via galleries or pickers.

function getPrimaryImage(recipe) {
  if (!recipe.image) return undefined;
  return Array.isArray(recipe.image) ? recipe.image[0] : recipe.image;
}

When exporting back to Schema.org, include all normalized URLs so downstream clients can select the best rendition for their layout or aspect ratio.

Web Scraping

scrapeRecipe(url, options) supports basic fetch tuning:

  • timeout (ms, default 10000)
  • userAgent (string, optional)
  • maxRetries (default 2, retries on non-4xx failures)
import { scrapeRecipe } from 'soustack';

const recipe = await scrapeRecipe('https://example.com/recipe', {
  timeout: 15000,
  maxRetries: 3
});

Parsing Utilities

Soustack Core includes parsers for common recipe data formats:

import {
  parseIngredient,
  parseIngredientLine,
  smartParseDuration,
  parseYield
} from 'soustack';

// Parse an ingredient string into structured data
const ingredient = parseIngredientLine('2 cups flour, sifted');

// Parse duration strings (e.g., "15 minutes", "1 hour 30 minutes")
const minutes = smartParseDuration('1 hour 30 minutes'); // 90

// Parse yield strings (e.g., "4 servings", "12 cookies")
const yield = parseYield('4 servings');

CLI Usage

Soustack Core includes a command-line interface for common operations:

Validation

# Validate a Soustack recipe file
npx soustack validate recipe.soustack.json

Scaling

# Scale a recipe to 2x the original yield
npx soustack scale recipe.soustack.json 2

# Scale to a specific number of servings
npx soustack scale recipe.soustack.json --servings 8

Schema.org Import/Export

# Import from Schema.org JSON-LD
npx soustack import recipe.jsonld -o recipe.soustack.json

# Export to Schema.org JSON-LD
npx soustack export recipe.soustack.json -o recipe.jsonld

Web Scraping

# Scrape a recipe from a URL
npx soustack scrape "https://example.com/recipe" -o recipe.soustack.json