MenuFindr | Sacramento Restaurant Directory
WordPress restaurant directory hub for the Sacramento area, built on a custom plugin with 7 database tables, structured menu data, free Nominatim geocoding, a REST write API, and a Stripe-backed listing claim flow. GeneratePress child theme, ACF Pro, no CI required.
The problem
You want pad thai. Which Sacramento restaurants have it? Good luck finding out. You would need to check dozens of individual restaurant websites, many with menus buried in PDFs or hopelessly outdated pages. Yelp and Google Maps let you search restaurants, but you cannot search by dish. The question “who has good chicken mole near me?” should not take 20 minutes to answer.
What I built
MenuFindr is the directory hub for the Sacramento restaurant cluster. It runs on WordPress, but almost nothing about it is off the shelf. The work lives in two deliverables I built in-tree: a plugin called menufindr-core that carries the entire data model, and a GeneratePress child theme that renders it.
The reason it is a directory rather than a pure search engine is that the data problem comes first. You cannot search menus by dish until the menus exist as structured data, and restaurant menus in the wild are PDFs, images, and hand-typed HTML. So the plugin is built around getting menu data into a real schema and keeping it there.
The plugin
menufindr-core registers the custom post types and taxonomies, defines the ACF field groups, and owns 7 custom database tables for the data that does not belong in WordPress post meta. On top of that it provides a read and write REST API, a bulk importer, geocoding, and the listing claim flow.
Menus are modeled as a structured repeater with sections and items, which is what makes dish-level search possible. A WYSIWYG menu field exists as a fallback for restaurants whose menus refuse to be structured, but the repeater is the primary path and everything downstream reads from it.
Geocoding without a paid API
Every restaurant needs coordinates for maps and proximity. The obvious answer is the Google Geocoding API, and I did not use it. The directory runs on free Nominatim instead, which means living inside a 1 request per second courtesy limit and an hourly cap.
That constraint shaped the design. Geocoding runs as a throttled queue on the WordPress save pipeline rather than inline, failures retry through wp-cron instead of blocking a save, and the whole subsystem sits behind a kill switch so it can be disabled without touching code. The result costs nothing per month and has no API key to rotate or leak.
The save pipeline runs in a fixed order on every restaurant save: geocode, then sync, then location assignment, then slug generation. Getting that order wrong produces listings with the right address and the wrong map pin, which is the kind of bug nobody reports and everybody notices.
Slugs that do not move
Restaurant URLs are location-based and frozen: title, city, state. Once a listing saves for the first time, the slug locks and stops regenerating.
This looks like over-engineering until you have watched a directory quietly rewrite a few hundred URLs because someone corrected a restaurant’s punctuation. Long ugly slugs that never change beat short pretty slugs that break inbound links. Older bare-title URLs still resolve through WordPress permalink guessing, so nothing that was ever indexed 404s.
Claiming a listing
Restaurant owners can claim their listing through a Stripe-backed flow. Claimed listings unlock the premium presentation layer: maps, photo galleries, and a reservation button. Unclaimed listings still get a complete, useful directory page, because a directory full of thin placeholder pages is worth nothing to the diners it is supposed to serve.
Everything premium gates on a single claim status field, which keeps the authorization surface small enough to reason about.
How it ships
There is no CI on this project, and that is deliberate. The host strips authorization headers at origin and caches GET responses hard enough that a wrong password can return a cached success. An automated deploy pipeline on top of that would be a machine for producing false confidence.
Instead, releases are versioned zips uploaded through wp-admin, and verification happens through writes rather than reads: POST something, then read it back with a cache buster and confirm the id that came back this run. Tests are plain PHP files with a small assertion helper, covering the pure logic. The parts that touch the database, the geocoder, and REST auth get verified against the live site, because that is the only place they behave honestly.
Where it stands
Structured menus shipped in the 1.9 line and are live-verified on the production site. The directory anchors the restaurant cluster, with the sibling restaurant website platform building individual client sites that point back to it. Both live on the same server, which is why the deploy tooling for the static sites is fenced off from the WordPress document root by an explicit rule rather than by good intentions.
Like what you see?
I build tools that solve real problems. If you have an idea or a project that needs engineering, let's talk.
Get in Touch