Why I built it
Five dining halls, each publishing its own menu on its own page with no shared format. Working out where to eat meant five tabs, three times a day.
I built a single page that pulls all five. It is the only project here that other people use daily.
What it does
One page with all five menus and a search over them.
You can search 13 cuisine types and 12 dietary and allergen tags, combine them with include, exclude and AND/OR, and fuzzy match on item names for misspelled queries. Filters narrow by meal period, day and hall. A separate endpoint lists which halls are still open, computed from each hall's schedule in US Central time.
It is still live, with more than 100 daily users, counted off the Vercel logs. I have stopped adding to it.
How it works
What happens when you open the page
- 1
You open the site.
It runs as a serverless function.
VercelFastAPI
- 2
If this week's menus are already saved, you get them straight away.
Menus change weekly, so one download serves the whole week.
Python
- 3
If not, it downloads all five dining hall pages.
One hall rejects normal requests, so it retries with fewer headers until one works.
urllib
- 4
It pulls the dishes out of each page.
Each hall's site is laid out differently, so each has its own parser.
regex
- 5
You get one page with every hall on it.
Search by dish, and filter by cuisine, diet, allergen, meal, day and hall.
Jinja2
No scraping libraries. Pages come down through urllib from the standard library and get parsed by extractors I wrote for each site's own day and meal markup. Five sites with five different structures need five bespoke parsers anyway, and a library would have added a dependency without removing the per-site rules. When one site changes its layout, only its extractor needs fixing.
The hard parts
One hall returns 406
Four of them serve their menu to an ordinary request. The fifth returns 406 Not Acceptable, based on the request headers.
Retrying through progressively smaller header sets got one accepted. That hall is why the fetch layer retries at all.
Fetching on every request does not work on serverless
A function that fetches five websites on every page load is slow, and it hammers the five sites. Each visitor pays for five fetches.
They change weekly. Cache per ISO week, refetch when the week turns over, and almost every visit becomes a local lookup. Under 200 milliseconds, no network, and the dining hall sites see a handful of requests a week.