Internal Product & Engineering Audit
Secondary Audit — Internal

Restaurant Ranker — AI Visibility Grader

A review of what we're actually building in the ranker tool: how it scores, what's live versus theatrical, where the bugs and security exposure sit, and the gap to a product we can stand behind.

Scope  restaurant-grader/index.html (1,816 lines) + restaurant-grader-proxy worker Stack  Vanilla JS + Cloudflare Worker Audited  29 May 2026 Reference demo  original preserved (frozen) at /curtain/
01 — What we're building

A lead-gen demo that grades AI visibility

Restaurant Ranker is a public, no-signup tool that takes a single restaurant URL and runs a ~52-second cinematic sequence — terminal crawl, radar scan, review collage, competitor map, neural-net animation, AI-engine probes — ending in two headline scores (SEO Health and GEO Readiness), an estimated $/month lost figure, a 5-axis radar, three copy-paste quick wins, and a long detailed report. It is a sales funnel for Primi Digital, not a precision analytics product — and that framing is the right one to judge it by.

~30%
of the output is genuinely live (SEO / GEO / $ from the worker)
~70%
is hardcoded Boqueria sample data (reviews, map, engine verdicts, report body)
0
client-side secrets found — architecture is clean on this front
0
real AI-engine probes implemented yet (the headline feature)
02 — How the score is computed

Two scoring systems — and they disagree

The live path runs in the Cloudflare worker (computeScores). The fallback path ships a hardcoded sample. The detailed report below the fold is static regardless of which path runs.

Live SEO score — 12 weighted on-page checks (ok 10 / warn 5 / bad 0, normalised to 100)

CheckPass / warn / fail logic
HTTPShttps → ok, else bad
Server-rendered HTMLbody word-count > 200 → ok, else warn
Meta titlemissing → bad; >65 or <25 chars → warn; else ok
Meta descriptionmissing → bad; <60 or >200 chars → warn; else ok
Canonical / Viewport / Robotspresent-and-sane → ok
Open Graphall 3 (title+desc+image) → ok; any → warn; none → bad
H1exactly 1 → ok; >1 → warn; 0 → bad
Alt-text coverage>0.7 → ok; >0.3 → warn; else bad
Internal links≥5 → ok; ≥1 → warn; else bad
Schema presentRestaurant/LocalBusiness/FoodEstablishment → ok; any JSON-LD → warn; none → bad

Live GEO score — base 50 + signal bonuses

Restaurant schema +10, AggregateRating +8, Menu +4, OpeningHours +4, server-rendered +4, meta description +3, canonical +2 (clamped 0–100). The base of 50 means a site with literally nothing still scores 50/100 — defensible as a demo, not as a real metric.

Dollar-loss model — additive penalties, capped at $5,000

No Restaurant schema +$800, no AggregateRating +$250, no meta description +$300, incomplete OG +$150, low alt coverage +$150, h1≠1 +$100, no canonical +$100, not server-rendered +$200. A heuristic with no documented basis — fine for a hook, but we should never present it as a measured figure.

03 — How it works technically

Static frontend, one scraping worker, no LLM

04 — Correctness, trust & security

What needs attention, by priority

P0 The report misdescribes any non-Boqueria URL

Header pills and the verdict update from live data, but the entire detailed report body stays hardcoded to Boqueria — reviews ("2,195"), competitor map (Pil Pil, Zoi…), engine snippets and the $2,050 breakdown. A live audit of another restaurant produces a confident report whose body contradicts its own live header. The radar chart even mixes live and sample axes.

P0 Fake share URL

copyShareUrl() copies a hardcoded dead link (restaurant-grader.demo/audit/boqueria-ues-x7k2) for every user — there's no share/persistence backend, yet the CTA promises "Share it, keep it."

P1 The headline feature isn't built

Real AI-engine mention probes — the core promise — don't exist. This needs API keys + budget (Places/Yelp + LLM calls), stored as Worker env secrets. The architecture is correctly set up for that; the feature simply isn't wired.

P2 Worker hardening: SSRF, rate limiting, regex parsing

The worker fetches an arbitrary user URL server-side and validates only the protocol — no block on localhost/private/link-local hosts (low severity on Workers, but a classic SSRF shape and an open scraper others can abuse). No rate limiting. Regex HTML parsing is fragile on reordered/multiline attributes and counts any href containing the host as "internal".

Good Security posture is clean

No client-side secrets — a full search returned only a CSS mask-image false positive. The proxy uses no API keys, content-type gating and a 600 KB body cap. This directly honours our standing rule: never embed tokens in client HTML. The .wrangler/ account cache is untracked — keep it gitignored so it never lands in history.

P2 Page-level & a11y polish

Two H1s once the report renders; no canonical and no SoftwareApplication/Organization schema on the tool's own page (ironic for a GEO tool); prefers-reduced-motion is computed but unused (52s of animation with only a Skip button); dead constants (hardcoded radar legend, stale report date).

05 — Recommendation

Make it true, then make it real

The proxy is clean and well-scoped; the frontend is a high-production-value demo. The single most important move is to stop the report from presenting hardcoded Boqueria data as a live audit of other restaurants — either make the detailed section data-driven from the worker response, or gate it so a live, non-Boqueria audit renders only what was actually measured. Fix the fake share URL and soften the "real AI probes" claim until the probe layer exists.
Then build the actual product: wire real AI-engine probes and live review/competitor data behind the worker (keys as env secrets, never client-side), add SSRF + rate-limit guards, and give the tool page its own SoftwareApplication schema and canonical. Sequenced this way, Restaurant Ranker goes from a convincing demo to something we can put a client's name into without caveats.

Priority order