Deployed

Cooking Recipes

A collection of recipes without the intrusive ads. Minimally designed to make grocery shopping and cooking easier.

CookingStatic Site GeneratorHaskellHTMLCSSDSL

Recipe websites embody some of the worst aspects of the modern web. This app is a custom static site generator, written in Haskell, that converts structured Markdown recipe files into mobile-friendly HTML pages without ads or a hostile user experience. A small amount of plain JavaScript adds search, ingredient checklists, and step tracking to make grocery shopping and cooking easier.

Tech Stack

  • Haskell (GHC, Cabal, Hspec, commonmark)
  • HTML/CSS/JS
  • ImageMagick
  • NGINX

Architecture

  • Produces a static website hosted behind an NGINX reverse proxy.
  • Recipes are structured Markdown files with fixed sections (Ingredients, Directions, Notes, etc.).
  • Inside the generator:
    • Each Markdown file is parsed into a Markdown AST with the commonmark library, plus a small extension for <<quantity>> markers in the directions.
    • The AST is checked against the expected sections and converted into a type-safe Recipe data type. A file that doesn’t fit fails the build with an error naming the line.
    • Recipes can also be written in Haskell with a builder DSL using Haskell’s & operator.
    • HTML templates with {{PLACEHOLDER}} placeholders are used to generate pages in the dist/ output directory.
    • Public assets (pictures of final dishes, mostly) are copied to the dist/ directory.
  • Hspec regression tests compare the latest build output to expected HTML pages.
  • The index page has keyword search across names, tags, and ingredients, plus tag filters and sorting. The current search is kept in the URL so it survives a refresh or a shared link.
  • Recipe pages are designed around cooking from a phone.
    • The first photo of the dish is a hero image at the top of the page.
    • Ingredients are a checklist with a progress bar, and the list can be collapsed.
    • Tapping a direction step highlights it so you can keep your place.
    • Quantities in the directions are highlighted so you don’t have to scroll back to the ingredient list.
    • A “Keep screen awake” switch uses the Screen Wake Lock API so the phone doesn’t dim mid recipe.
    • Uncommon tools are listed above the ingredients, and sources and notes sit in a small Kitchen notes panel below the directions.
  • Photos are resized and recompressed with an ImageMagick script (53.8 MB down to 4.4 MB), and only the hero image loads eagerly.
  • Fonts are self hosted, so pages make no requests to Google Fonts.
  • New recipes are added with a Claude Code skill. It takes a URL, a Markdown file, or a folder of photos of cookbook pages and writes the recipe file in the standard format. After I check the file, it rebuilds the site and updates the tests.
  • Recipe pages have print styles that hide the controls and fit most recipes on two pages.

Challenges

  • Designing a Markdown structure that is strict enough to parse reliably but flexible enough for real recipes.
    • E.g., optional sections, numbered steps with sublists.
  • Balancing the desire for minimal CSS with the need for a highly accessible and useful UI.
  • Regression tests require manual fixture updates whenever templates change, which adds friction to UI tweaks.
  • Adding interactive features without breaking accessibility. Each one needs to work with a keyboard, a screen reader, and Windows High Contrast mode, and the page still needs to make sense with JavaScript turned off.

Learnings

  • This was my first practical Haskell project, so I put many functional programming patterns into practice.
  • Haskell’s type system makes data modeling expressive.
    • The type-safe DSL and builder pattern are readable and checked at compile time.
  • Line-by-line parsing was enough to get started, but it silently dropped anything it didn’t expect. Parsing into a real Markdown AST first made strict checks with useful errors easy to add.
  • Regression tests come at the cost of a manual update step (or at least manual review).
  • A minimal template engine (plain string replacement) is entirely sufficient for a static site of this scope.
  • I redesigned the site as a stack of a dozen small pull requests. Each one was deployed for review on my phone and checked by a separate adversarial AI review agent before merging. The agent reviews caught real bugs that I would have missed, like CSS rules from one PR silently overriding another and an unclosed quantity marker that hid the rest of a step.
  • AI agents are very reliable for extracting recipe info from images or URLs, and can reformat directions to be easier to follow during actual cooking.
    • E.g., “Mix in 1/2 cup tomatoes” vs “Mix in the tomatoes,” where the second forces you to scroll back to the ingredient list for the quantity.

For the Future

  • The recipe skill stops before committing. It could also commit the new recipe and open a PR for me to review on my phone.