Let's Eat, DC: Every Restaurant Week Menu, Searchable

Restaurant Week in D.C. covers close to 400 restaurants. The official site is a grid of cards. The menus were 500-plus PDFs behind per-restaurant modals. Of those, 40 percent were re-uploads from earlier seasons. There was no way to search by dish or tell whether a $65 dinner was a deal. I built letseatdc.com to fix that. It went live eight days after the first commit. 23,000 people used it.

Research

I started by auditing 40 restaurants on the official site. I concluded it had no menus at all. I was wrong. Two days later, I clicked through the way a diner would. I found a late-bound modal with a plain GET endpoint per restaurant: 530 PDFs, 76 percent coverage. I killed the Google Places work that day. I wrote the reversal into the plan beside the original finding. I kept one rule from it. Verify load-bearing claims against the live thing. "Menu found" and "menu current" are different facts.

The pipeline

I run each PDF through an LLM extraction pass against a validated schema. A dish needs a name, a course, a price, and dietary flags. Otherwise it fails validation and runs again. Every menu carries a "verified for Summer 2026" flag. Stale re-uploads caused more data problems than anything else. Price tier is relative to the restaurant's own regular menu. That is why the page calls the $35 lunch at Casa Teresa a good deal. The same food comes to about $44 off their brunch menu. I used the free Census geocoder. No paid APIs anywhere. By mid-September the index held 655 menus and 8,199 dishes across 389 restaurants.

Design

I built the site around menus. Search by dish, then narrow by brunch, lunch, or dinner, by neighborhood, by usual price, and by collections like Great Deal, Michelin & James Beard, and Critics' Picks. Every filter state is a URL. A search for sushi near Penn Quarter becomes a link you can text to someone. List reorders use the View Transitions API with a reduced-motion fallback. I use one accent color, a butter yellow. It only appears when something just happened, like a search hit or a dish added to your order. The wordmark is "Let's eat, DC.*" with the asterisk always answered in the fine print. No mascots, no crumbs, no stars.

Build

I used Next.js 15 App Router on Cloudflare Workers through OpenNext. The map uses MapLibre with Protomaps tiles. Base UI handles popovers and toggles. nuqs handles URL state. Every restaurant page also answers Accept: text/markdown, or /r/slug.md, so an agent gets the menu as text instead of scraping the page. PostHog from day one. Claude Code wrote a good share of the code, including the extraction batches. I wrote the specs and reviewed every line.

Launch

I started research August 4, made the first commit August 9, and launched August 17 on r/washingtondc. Reddit sent about 100 people all week. On August 21 Axios D.C. ran a story. The headline said I had "hacked D.C. Restaurant Week". Their newsletter sent 924 visitors in the 6am hour. WTOP followed. Between August 10 and September 18 the site had 23,381 visitors and 164,450 page views. The average session was over six minutes. Phones accounted for 68 percent of traffic. The first two weeks brought 5,683 clicks out to Resy and OpenTable to book a table.

I ran one A/B test on the order of the collection row. It had 10,564 visitors across three arms. The result was inconclusive. The pre-declared metric moved from 34.8 to 36.0 percent. The credible intervals crossed zero. I shipped the arm that led and wrote up the secondary result as a hypothesis. Separating one point at that rate needs about 36,000 visitors per arm. That is worth knowing before I run the next one.

Lighthouse on mobile: accessibility 100, cumulative layout shift 0.