You don’t need a $39/month SaaS to ship a phone-readable menu. You need a weekend, a free Vercel account, a domain (or a Vercel-hosted URL for week one), and a willingness to delete the PDF. Below is the workflow we’d hand to a restaurant owner who is comfortable copy-pasting into a code editor or has a slightly technical friend.
TLDR
By Sunday evening you’ll have: a static HTML menu page at https://yourdomain.com/menu, allergen icons with alt text, a tap-to-call header, a print stylesheet, and a dynamic QR sticker that points to that URL (so future menu updates don’t require a sticker reprint, S-024). Cost: $0 if you already have a domain, ~$12 for one year of a domain if not.
Why this is worth a weekend
The pain we documented in the 10-row mobile audit: diners get a PDF, pinch, give up, ask for paper (S-001, S-002). The reason most independents haven’t fixed it isn’t technical difficulty — it’s that the “right” tool is unclear. Existing menu builders take 6-14 minutes per price update in our 2026-05 timing test (S-303), and many of them want $20-40/month for what is structurally a static HTML page.
So the simplest fix is: write the HTML once, host it free, update it in a text editor (or eventually our editor).
Saturday: write the menu, host the page
Step 1 — Get the menu into a structured form (30 min)
Open the PDF in Preview. In a new text file, type each section heading, then each item as a line:
Starters
Burrata, peach, basil — 14
Brussels, lemon, parmesan — 11
Mains
Bucatini cacio e pepe — 22
Hanger steak, salsa verde — 28
Desserts
Olive oil cake — 9
If you have allergens, mark them in brackets:
Bucatini cacio e pepe [gluten, dairy] — 22
Step 2 — Convert to HTML (45 min)
You can use any starter, but here’s the minimum. Save as menu.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dinner menu — Your Restaurant</title>
<meta name="description" content="Dinner menu for Your Restaurant. Updated weekly.">
<link rel="canonical" href="https://yourdomain.com/menu">
<style>
body { font: 17px/1.6 -apple-system, system-ui, sans-serif; color: #1d2a2e; background: #faf6ec; max-width: 36rem; margin: 0 auto; padding: 1.5rem; }
header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; }
h1 { font-size: 1.5rem; margin: 0; }
h2 { font-size: 1.15rem; margin: 1.5rem 0 0.5rem; padding-bottom: 0.25rem; border-bottom: 1px solid #e8e2d3; }
.item { display: flex; justify-content: space-between; gap: 1rem; padding: 0.4rem 0; }
.item .name { flex: 1; }
.item .price { font-variant-numeric: tabular-nums; font-weight: 600; }
.allergens { font-size: 0.8rem; color: #566066; }
a.tel { color: #1f6c75; font-weight: 600; }
@media print {
body { background: white; max-width: none; }
header, footer { display: none; }
}
</style>
</head>
<body>
<header>
<h1>Your Restaurant — Dinner</h1>
<a class="tel" href="tel:+15551234567">555-123-4567</a>
</header>
<h2 id="starters">Starters</h2>
<div class="item"><span class="name">Burrata, peach, basil <span class="allergens" aria-label="contains dairy">(dairy)</span></span><span class="price">14</span></div>
<div class="item"><span class="name">Brussels, lemon, parmesan <span class="allergens" aria-label="contains dairy">(dairy)</span></span><span class="price">11</span></div>
<h2 id="mains">Mains</h2>
<div class="item"><span class="name">Bucatini cacio e pepe <span class="allergens" aria-label="contains gluten and dairy">(gluten, dairy)</span></span><span class="price">22</span></div>
<h2 id="desserts">Desserts</h2>
<div class="item"><span class="name">Olive oil cake</span><span class="price">9</span></div>
<footer>
<p style="margin-top: 2rem; font-size: 0.85rem; color: #566066;">Updated Sat 25 May. Prices in USD, tax included. Allergens listed where present; please ask if you're unsure.</p>
</footer>
</body>
</html>
That’s the entire page. Total size: ~3KB. LCP on a slow-4G connection: under 1s (S-015).
Step 3 — Deploy to Vercel (15 min)
- Make a folder, put
menu.htmlinside (rename toindex.htmlif you want it at the root URL). - Run
npx vercel(Node 18+ required). Sign in. Confirm the deploy. - Vercel gives you a URL like
your-restaurant.vercel.app. Test it on your phone.
Step 4 — Connect your domain (30 min)
If you have a domain already (in Cloudflare or anywhere):
- In Vercel, project → settings → Domains → add
yourdomain.com. - Vercel shows the DNS records to add — usually
A 76.76.21.21for the apex andCNAME cname.vercel-dns.comforwww. - In your DNS provider, add those records. Set proxy/cloud to DNS-only (gray cloud) in Cloudflare — orange-cloud proxy breaks Vercel cert provisioning.
- Wait 5 minutes. The cert auto-provisions. Your menu is live at
https://yourdomain.com.
Don’t have a domain? You can stay on the vercel.app URL forever if you want — it’s free and works fine for the QR sticker.
Sunday: dynamic QR, allergen polish, print stylesheet
Step 5 — Switch to a dynamic QR (30 min)
The killer feature of dynamic QR codes is the destination URL can change without reprinting the sticker (S-024). Use any free dynamic QR provider that lets you set the URL — they’ll give you a https://qr.example.com/abc123-style redirector. Print that onto your sticker. Pointing it at yourdomain.com/menu means you can move the menu later (e.g., from Vercel to your own server) without touching the physical stickers.
If you’ve already printed static QR stickers that encode the URL directly, that’s fine — keep them for now and budget to reprint with a dynamic redirector at the next sticker run.
Step 6 — Allergen icons (45 min)
Find an icon set with peanut, tree-nut, dairy, gluten, soy, egg, shellfish, fish, sesame — these are the FDA “big 9” (S-017). The FoodAllergy.org Teal Pumpkin SVGs work; so do the open-source icons from a few designer-released kits. Add each as a small <img> next to the item, with descriptive alt text:
<img src="/icons/peanut.svg" alt="contains peanuts" width="16" height="16" loading="lazy">
Then add a legend at the bottom of the menu listing what each icon means.
Step 7 — Print stylesheet (30 min)
The CSS above already has @media print block. Test it: in Chrome, File → Print → Save as PDF. It should hide the header/footer and lay out clean prices on the right. Print one copy and hand it to the kitchen.
Step 8 — Audit your own page (15 min)
Run the 10-row mobile audit against your fresh page. You should pass all 10 rows. If you don’t, the audit tells you which row to fix. Most failures at this stage are: missing tel: link in the header (row 9), or you forgot to add the print stylesheet (row 10).
Common mistakes
- Embedding the menu inside a
<iframe>of a Google Doc. It looks like HTML to you; it’s still effectively a Google Docs page on mobile. - Auto-redirecting from a “decision” page (
/menu-options) to the menu. Don’t add interstitials. - Using a Wix/Squarespace template that lazy-loads everything. That tanks LCP. Test on slow-4G.
- Forgetting hreflang for bilingual menus. See the bilingual menu post.
- Updating the QR sticker every time prices change. Use a dynamic QR (step 5).
FAQ
What if I’m not technical at all?
Then the right move is to hire a designer or a college student for one weekend ($150-300). The HTML in step 2 is the entire spec; anyone who can copy-paste can ship it.
Does this work for a multi-location restaurant?
Yes — give each location its own page at /menu-downtown, /menu-westside, etc. Each gets its own QR. The static-page architecture scales fine to dozens of locations.
What about online ordering?
Out of scope for this guide. If you need ordering, you probably want Toast/Square or a Stripe-payment-link bolt-on. See the Toast vs BentoBox vs DIY post for when DIY is the wrong answer.
Can I run this off WordPress?
Yes, but WordPress + a Page builder is overkill and slower. A static HTML file on free hosting beats it on every Core Web Vital.
Will Google rank it?
Eventually. HTML menus rank much better than PDF menus (S-016). Make sure the page has a unique title, meta description, and a canonical URL pointing at itself. Submit the URL to Google Search Console.
When you’re done, run the 10-row audit against your new page. If you want the deep methodology behind each row, see why your QR menu is killing conversions.