# Recipe Generator — Engineering Notes

> Handoff doc covering technical decisions, design choices, and bug fixes made during initial development. Last updated: April 28, 2026.

---

## What It Is

A single-page HTML app that takes user-supplied ingredients, a food goal, dietary restrictions, and preferences, then calls the Anthropic API directly from the browser to generate a structured, chef-quality recipe with precise step-by-step instructions.

No backend. No build step. One `.html` file.

---

## Tech Stack

| Layer | Choice | Reason |
|---|---|---|
| Runtime | Vanilla HTML/CSS/JS | No framework needed; single file, zero dependencies |
| Fonts | Google Fonts (Lora + DM Sans) | Lora for editorial/recipe headings; DM Sans for UI chrome |
| AI | Anthropic API (`claude-sonnet-4-20250514`) | Direct browser fetch with `anthropic-dangerous-direct-browser-access` header |
| Output format | JSON (structured) | Predictable, renderable, extensible |

---

## Inputs Collected

| Field | Type | Notes |
|---|---|---|
| Ingredients on hand | Tag input (multi-value) | Comma or Enter to add; Backspace to remove last |
| Food goal | Free text | e.g. "a hearty weeknight pasta" |
| Dietary restrictions | Multi-select pill toggles | Vegan, Vegetarian, Gluten-free, Dairy-free, Nut-free, Low-carb |
| Cuisine style | Dropdown | 10 options + "Any cuisine" |
| Time available | Dropdown | Under 20 min → 1–2 hrs |
| Skill level | Dropdown | Beginner / Intermediate / Advanced |
| Servings | Dropdown | 1, 2, 4, 6, 8 |
| Additional notes | Textarea | Free-form catch-all |

---

## API Design

### Endpoint
```
POST https://api.anthropic.com/v1/messages
```

### Required Headers
```js
{
  'content-type': 'application/json',
  'anthropic-version': '2023-06-01',
  'anthropic-dangerous-direct-browser-access': 'true'
}
```

> ⚠️ **Do not include `x-api-key`** when running inside the Claude.ai artifact environment — the platform injects the key automatically. Passing an empty string causes an auth failure (see Bug Fixes below).

### Model
`claude-sonnet-4-20250514` — good balance of quality and speed for recipe generation. Max tokens set to `1800`.

### Prompt Architecture

Two-part: system prompt + user message.

**System prompt** defines:
- Role: expert chef and culinary writer
- Explicit anti-vagueness instruction with a concrete example of bad vs. good instruction style
- Strict JSON-only output requirement (no markdown fences, no preamble)
- The exact JSON schema expected
- Hard rules: respect dietary restrictions, 5–10 steps, precise sensory details

**User message** is assembled dynamically from form state, filtering out any empty fields:
```
Ingredients available: pasta, garlic, tomato, tofu
Food goal: quick but protein packed pasta
Dietary restrictions: vegetarian, dairy-free
Time constraint: under 20 minutes
Servings: 6
```

### Response Schema
```json
{
  "title": "string",
  "description": "string",
  "servings": 2,
  "prepTime": "string",
  "cookTime": "string",
  "ingredients": [
    { "amount": "string", "unit": "string", "name": "string" }
  ],
  "steps": [
    { "title": "string", "instruction": "string" }
  ],
  "notes": "string | null"
}
```

- `servings` is typed as a **number** (not string) — explicitly called out in system prompt to avoid JSON parse issues
- `unit` can be omitted for whole/countable items (the renderer handles `undefined` gracefully)
- `notes` is nullable

---

## Prompt Engineering Decisions

### Anti-vagueness instruction
The single most impactful prompt decision. Rather than just saying "be detailed," the system prompt gives a concrete counter-example:

> Instead of "cook the onions", write "cook the onions in the butter over medium heat, stirring occasionally, for 8–10 minutes until they turn translucent and just begin to turn golden at the edges."

This anchors the model's output quality at the instruction level, not just the ingredient level.

### JSON-only output enforcement
The system prompt instructs the model to return nothing before or after the JSON. To be defensive against any stray text, the parser uses:
```js
const jsonStart = raw.indexOf('{');
const jsonEnd = raw.lastIndexOf('}');
const recipe = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
```
This is more robust than `JSON.parse(raw)` directly, which would fail on any leading/trailing characters.

---

## Design Decisions

### Tag input for ingredients
Standard text input UX isn't great for multi-item lists. Tags let users see all their ingredients at a glance and remove individual ones without retyping. Implemented in vanilla JS — no library.

- **Enter** or **comma** commits a tag
- **Backspace** on empty input removes the last tag
- Duplicate ingredients are silently ignored (case-insensitive)

### Pill toggles for dietary restrictions
Checkboxes are functional but visually coarse for this context. Pills feel more natural for preference selection and render well on mobile. The native `<input type="checkbox">` is hidden; state is managed via `.checked` property and a `.checked` CSS class on the parent.

### Typography split
- **Lora** (serif): recipe title, step numbers — editorial, food-magazine feel
- **DM Sans** (sans-serif): all UI chrome, form labels, body text — clean and modern

### Recipe output layout
- Meta chips (servings / prep / cook time) give a quick overview before diving in
- Ingredients in a bordered list table — scannable, easy to follow while cooking
- Steps as numbered items with a bold title + detail body — mirrors professional cookbook formatting
- Chef's notes in a left-bordered aside box — visually distinct from the main method

---

## Bug Fixes

### Bug 1 — "Load failed" / auth error on API call
**Root cause:** The initial implementation passed `'x-api-key': ''` as a header. In the Claude.ai artifact environment, the API key is injected by the platform — but only when the header is absent. Passing an explicit (even empty) `x-api-key` header overrides the platform injection and results in an authentication failure before the request reaches the model.

**Fix:** Remove the `x-api-key` header entirely from the fetch call.

```js
// ❌ Before
headers: {
  'Content-Type': 'application/json',
  'x-api-key': '',   // <-- causes auth failure
  'anthropic-version': '2023-06-01',
  'anthropic-dangerous-direct-browser-access': 'true'
}

// ✅ After
headers: {
  'content-type': 'application/json',
  'anthropic-version': '2023-06-01',
  'anthropic-dangerous-direct-browser-access': 'true'
}
```

### Bug 2 — Silent generic error message
**Root cause:** The original catch block showed a hardcoded "Something went wrong" message regardless of what the API returned, making it impossible to diagnose failures.

**Fix:** Parse the API error response body and surface the actual `error.message` field:
```js
const data = await resp.json();
if (!resp.ok) {
  throw new Error(data.error?.message || `API error ${resp.status}`);
}
```

### Bug 3 — Missing required browser-access header
**Root cause:** `anthropic-dangerous-direct-browser-access: true` was absent in the initial version, which causes the API to reject browser-origin requests as a CORS/security policy enforcement.

**Fix:** Added the header explicitly to all fetch calls.

---

## Possible Next Steps

- **Streaming** — use the Anthropic streaming API so the recipe appears token by token rather than all at once after a delay
- **Persistent recipe book** — use the Claude.ai artifact storage API to save generated recipes across sessions
- **Serving scaler** — the structured JSON makes it trivial to multiply ingredient amounts; add a stepper UI to the output
- **Nutrition estimates** — add a `nutritionFacts` field to the JSON schema and ask the model to estimate macros
- **Shopping list view** — cross-reference the generated ingredients against what the user said they have, and produce a "what to buy" list
- **Image generation** — call an image model with the recipe title and description to generate a hero photo
- **Export to PDF** — render the recipe output as a printable/downloadable PDF
