How We Turned 30 Restaurant Nutrition PDFs into Calculators
Restaurant chains publish their nutrition facts, but rarely in a form you can use while ordering. Chipotle's guide lists every ingredient on its own row. Domino's lists toppings per serving, separately for each crust and size. Subway lists a 6-inch sub, and you work out the footlong yourself. None of them adds up the order you are actually about to eat.
So we built TrayFacts: one calculator per chain, built only from each chain's official numbers. You tap what goes in your bowl, sub, or box, and the calories, protein, carbs, fat, and sodium add up as you go. It now covers 30 chains and more than 10,000 menu items, sizes, and ingredients. This post is about the parts that turned out harder than expected.
1. Getting the data: every chain is different
There is no common format. In the first 30 chains, we met at least four:
- PDF tables (Chipotle, QDOBA, Moe's, Subway, Dunkin', SONIC and others). Some are dated, some are not.
- Data inside the page itself. Chick-fil-A ships its whole nutrition table as JSON in the page's state; Panda Express renders a plain HTML table.
- Nutritionix tables that the chain embeds on its own site (Taco Bell, Wawa, Sheetz, Culver's).
- Per-ingredient guides where the "item" is a topping per serving per crust (Domino's) or a sauce sized to a wing count (Buffalo Wild Wings).
We made one rule early and kept it: only official data. If a chain does not publish its numbers itself, or embed them on its own site, it is not on the site. A few big chains are missing for now because we could not get their official numbers in a usable form yet. A calculator built on scraped third-party numbers is easy to make and hard to trust.
2. Parsing: one small script per chain
Each chain has its own script that reads the source and writes a JSON file. A few things we learned about PDFs:
-
pdftotext -layoutlooks right but misaligns rows once a name wraps onto two lines.pdftotext -tablekeeps rows together far more often. - When that still fails, reading every word with its x/y position (we used pypdf) and rebuilding rows yourself works. In one guide, each row's numbers sit a few points above the item name, so you match by position, not by line.
- Some PDFs keep their numbers in a form layer that one library cannot read and another can. Try a second tool before writing a cleverer regex.
Nothing is typed by hand. If a number is wrong on the site, it is either wrong in the chain's own file, or there is a bug in a script we can fix and rerun.
3. Checking the data before trusting it
Official does not mean consistent. Every item goes through a few checks:
- Calories against macros. Fat × 9 + carbs × 4 + protein × 4 should land near the listed calories. Big gaps get looked at by hand.
- Duplicates. The build refuses a menu step that lists the exact same row twice.
- Claims in the page copy. Every sentence that says "lightest" or "most protein" is generated from the data, and the script stops if the claim is not true.
When a chain's own numbers contradict themselves (a drink with more sugar than carbohydrates, say), we keep them exactly as published and add a note, rather than "fixing" official data.
4. One engine for 30 menus
Every chain is described by the same JSON shape:
- Steps: bread, protein, cheese, veggies, sauces; each with a minimum and maximum number of choices.
- Order types that change the steps: a Subway footlong doubles the bread, protein, cheese, veggies, and sauce; a Chipotle taco order swaps the base; a Wawa Shorti hoagie only allows the Shorti roll.
- Multipliers for per-serving data: on Domino's you pick how many slices you eat and every topping scales with it.
- Caps like "up to two portions of protein" where the chain sells a double.
The calculator is one small Preact component on an otherwise static Astro site, hosted on Cloudflare Pages. Every order is encoded in the URL, so any order can be shared as a link and opens with the same totals.
5. A side effect: a small study
Once every menu was in one format, questions that used to take an afternoon became a loop. For example: what is the most protein you can get for 500 calories or less at each chain? For build-your-own chains, that means checking every combination the calculator allows, not just the menu items.
The answer surprised us: Texas Roadhouse's 11 oz sirloin, at 84 g of protein for 460 calories. At 17 of the 29 chains, you can get 40 g of protein without going over 500 calories. The full ranking is here, and every row opens that exact order in a calculator so anyone can check it.
What's next
More chains (the big burger chains are next, once we have their official guides), and more "healthiest order" guides built from the same data. If you have hit the same PDF problems, or know a chain that publishes clean data, we would love to hear about it in the comments.