Living Document Notice
Published 2026-09-17. The evolving architecture, revisions, and connected notes for this dispatch live in the Stax Digital Garden.

From Scraper to Galley Markdown

From Scraper to Galley Markdown: Warm golden amber P20 vector CRT macro showing raw data points funneling through grid into branching Galley DAG tree and document wireframe

Summary

Extracting structured recipe data from the web is only half of the conversion task. Once raw JSON-LD or microdata records are parsed and normalized, they must be formatted into clean, durable plain-text files that integrate with offline kitchen tools and text editors. The target document format must support human readability, checklist interaction, and machine-readable frontmatter.

FreeMyRecipes formats extracted culinary data into standardized Markdown files adhering to the Galley data exchange specification. This dispatch details the mapping pipeline that transforms nested Schema.org properties into flat YAML metadata, ingredient checklists, and step-by-step instructions.

The Schema Mapping Architecture

The transformation process converts nested JSON-LD objects into linear Markdown files containing structured frontmatter blocks.

Schema.org JSON-LD Payload
           │
           ▼
┌──────────────────────────────────────────────┐
│ Translation Engine                           │
│ - Map schema types to scalar strings         │
│ - Unroll ISO durations to human minutes      │
│ - Format ingredients as markdown tasks       │
│ - Number instructions sequentially           │
└──────────────────────────────────────────────┘
           │
           ▼
Portable Markdown Document (.md)

By standardizing on plain Markdown with YAML headers, the resulting recipes can be viewed in Obsidian, parsed with static site generators, or rendered on e-ink displays without custom client software.

Property Mapping Specification

The table below defines the mapping contract between Schema.org fields and the generated Markdown frontmatter attributes.

Schema.org / JSON-LD Source KeyTarget Frontmatter AttributeInternal RepresentationFallback Behavior
nametitleStringExtracted from HTML <title> tag
descriptiondescriptionStringFirst paragraph of body content
recipeYieldyieldStringDefaults to "Not specified"
prepTimeprep_time_minutesIntegerCalculated from ISO 8601 string
cookTimecook_time_minutesIntegerCalculated from ISO 8601 string
totalTimetotal_time_minutesIntegerSum of prep and cook times
recipeCategorycategoryStringOmitted if absent
recipeCuisinecuisineStringOmitted if absent
recipeIngredientDocument ChecklistMarkdown List Items (- [ ])Raw text lines
recipeInstructionsDocument BodyOrdered Numbered List (1. )Paragraph blocks

Template Generation Code

The Markdown generator uses a template pipeline that processes the normalized dictionary and outputs a complete document.

from datetime import date
from typing import Any, Dict
 
def render_recipe_markdown(data: Dict[str, Any]) -> str:
    frontmatter = [
        "---",
        f"title: \"{data.get('name', 'Untitled Recipe')}\"",
        f"date: {date.today().isoformat()}",
        f"yield: \"{data.get('recipeYield', 'Unspecified')}\"",
        f"prep_time: {data.get('prepTime', 'PT0M')}",
        f"cook_time: {data.get('cookTime', 'PT0M')}",
        f"source: \"{data.get('source_url', '')}\"",
        "draft: false",
        "tags:",
        "  - recipe",
        f"  - cuisine/{data.get('recipeCuisine', 'general').lower()}",
        "---",
        "",
        f"# {data.get('name', 'Untitled Recipe')}",
        "",
        data.get("description", "").strip(),
        "",
        "## Ingredients",
        ""
    ]
    
    for ing in data.get("recipeIngredient", []):
        frontmatter.append(f"- [ ] {ing}")
        
    frontmatter.append("")
    frontmatter.append("## Instructions")
    frontmatter.append("")
    
    for i, step in enumerate(data.get("recipeInstructions", []), start=1):
        step_text = step.get("text") if isinstance(step, dict) else str(step)
        frontmatter.append(f"{i}. {step_text}")
        
    frontmatter.append("")
    return "\n".join(frontmatter)

CLI Invocation Example

FreeMyRecipes allows piping extraction output directly into document writers:

# Extract recipe schema and stream markdown output
curl -s "https://example.com/herb-roasted-chicken" \
  | freemyrecipes parse --input-format=html \
  | freemyrecipes format --target=galley-markdown \
  > ~/recipes/herb-roasted-chicken.md
---
title: "Herb-Roasted Whole Chicken"
date: 2026-09-15
yield: "4 servings"
prep_time: PT20M
cook_time: PT75M
source: "https://example.com/herb-roasted-chicken"
draft: false
tags:
  - recipe
  - cuisine/french
---
 
# Herb-Roasted Whole Chicken
 
Crispy-skin roast chicken seasoned with coarse salt and fresh rosemary.
 
## Ingredients
- [ ] 1 whole chicken (4 lbs)
- [ ] 2 tbsp unsalted butter, softened
- [ ] 1 tbsp fresh rosemary, chopped
- [ ] 1 tsp kosher salt
 
## Instructions
1. Preheat oven to 425 degrees Fahrenheit.
2. Pat chicken dry with paper towels and season with salt.
3. Rub butter and chopped herbs under the breast skin.
4. Roast in a cast-iron skillet for 75 minutes until internal temp reaches 165F.

  • Directus Target: freemyrecipes
  • Garden Source Reference: galley-pkm-bridge, bosun-ast-specs, MOC - Data Liberation Workbenches, MOC - Culinary & Domain Workspaces