Feature: generate a printable PDF menu (with QR ideas) #1

Open
opened 2026-05-02 08:05:20 +00:00 by padreug · 0 comments
Owner

Background

Restaurants want a physical, printable menu they can hand customers,
laminate, tape to walls, or fold into a table tent. Today the menu
only exists in the database, on Nostr, and as live UI in the customer
webapp. There's no path from the operator's CMS to a print-ready
artifact.

A PDF generator inside the extension closes that loop. It also
becomes a useful bridge between the printed and the digital
worlds — every item in the database carries enough structure
(name, description, price, dietary, allergens, nostr_event_id,
ancestor menu-tree context) that a well-rendered PDF can encode
all of it, and QR codes on the page can deep-link printed items back
to the live, dynamic experience.

This is a brainstorm issue. We can break it into smaller deliverables
once the shape is clearer.

Why this is interesting

  • Offline-friendly fallback. If wifi or the local Nostr relay
    hiccups, the printed menu is still complete and current as of the
    last print.
  • Marketing artifact. A nicely-laid-out PDF is also what a
    restaurant emails to customers, posts on social, or sends to a
    print shop.
  • Bridge to the webapp. A QR on the printed menu can take a
    customer straight into the live ordering flow — no app install,
    no typing URLs.
  • Bridge to Nostr. A QR can encode an naddr1... pointing at a
    kind-30402 listing so any Nostr client (not just our webapp) can
    resolve and act on a printed item.

Goals

  • An operator clicks a button in the cms and gets back a
    print-ready PDF of the current menu, branded for their restaurant.
  • The PDF reflects the live state at generation time:
    prices, sold-out flags, availability windows, modifier groups.
  • The PDF embeds at least one QR code; the exact set is a design
    decision (see below).
  • The endpoint is gated by the wallet's admin key so the PDF can
    also be fetched programmatically by integrations.

Non-goals (for v1)

  • Editing the PDF's layout WYSIWYG-style. Operators pick a preset
    theme or supply their own template; that's it.
  • Generating per-table QR codes / table-tent variants. Stretch.
  • Print-shop-grade color profiles, bleeds, crops. Operators going
    to a real print shop should expect to feed the PDF through a pro
    tool.

Open design questions

1. Where does PDF generation happen?

  • Server-side, Python with WeasyPrint,
    reportlab, or fpdf2.
    WeasyPrint is the obvious pick: it consumes HTML+CSS, so the same
    Jinja templates that drive the cms can be the source of
    truth for layout. New paths just produce alternate stylesheets.
  • Headless browser (Playwright / Puppeteer). Cleaner CSS support,
    bigger dependency.
  • Client-side JS (jsPDF, pdfmake). Fastest iteration, but means
    every consumer has to render — the operator clicks "Download" and
    the browser does the work.

Recommendation to validate: WeasyPrint server-side, with a Jinja
template at templates/restaurant/menu_print.html and an admin
endpoint GET /api/v1/restaurants/{id}/menu.pdf?theme=<name> that
streams application/pdf. Keeps the source of truth on the server
and matches LNbits's existing template conventions.

2. What does the layout look like?

A first cut:

  • Cover (optional first page): logo, banner image, name,
    tagline, location, social links, header QR.
  • Body: walks the menu-tree depth-first.
    • Top-level node = section heading.
    • Subcategories = subsection headings, indented or boxed.
    • Items at any node level (since [[menu-tree|items can attach
      anywhere]]).
    • Item row: name, dietary icons, optional small image, dotted
      leader to the price.
    • Optional second line: description, allergen badges, modifier
      hints ("with chicken +5 / tofu +3").
  • Footer: restaurant pubkey (so a curious customer can find them
    on Nostr), generation timestamp, page numbers.

Open: which preset themes to ship? Modern minimal, rustic warm,
classic monochrome? My instinct: ship one solid default, plus a
single CSS hook for operator overrides.

3. Currency / multi-currency

Items have their own currency field. The PDF should:

  • Show prices in their declared unit (50 GTQ, 1500 sat, etc.).
  • Optionally show a converted reference (e.g. (~$6.40 USD)) using
    LNbits's fiat_amount_as_satoshis plumbing.
  • Surface a single restaurant-wide display preference in the
    settings panel (display in declared currency only, declared +
    converted, sat only, etc.).

4. QR codes — three flavors, all worth shipping

We can do all three; they aren't mutually exclusive. They're listed
roughly in increasing complexity so we know what to land first.

  • "Order online" QR in the cover header → links to the
    customer webapp's restaurant page, e.g.
    https://atitlan.io/r/<slug>. Tap to enter the live menu.
  • WiFi QR (WIFI:T:WPA;S:<ssid>;P:<password>;;) using
    credentials from a new (optional) restaurant.wifi_* field. Lots
    of cafés already use these on table tents.
  • Restaurant Nostr profile QR encoding the kind-0
    pubkey as nostr:npub1... so a Nostr-native client can follow
    the restaurant directly.

These are all once-per-PDF and don't require any per-item tracking.

b) Section / category QRs

  • A small QR next to each section heading that deep-links into the
    webapp at that branch of the menu, e.g.
    https://atitlan.io/r/<slug>#node=<id>. Useful when the printed
    menu is a tease (limited space) and the digital one has the full
    range.
  • Optionally encodes the menu-tree so a
    client without our webapp can still parse the link.

Medium effort. Mostly a layout decision (where to put them so they
don't dominate the page).

c) Per-item QRs (the Nostr-native one)

  • Each item gets a small QR encoding either:
    • nostr:naddr1... — a NIP-19 addressable pointer to the
      kind-30402 nostr-layer, so any Nostr
      client can resolve and act. This is the most
      protocol-pure option
      — works without our webapp, works
      offline-of-our-server, works in the future once more clients
      exist.
    • or a "scan to add to cart" URL (https://atitlan.io/r/<slug>/ add?item=<id>) that loads the item into the webapp cart
      pre-selected.

Heaviest visually — more QR codes per page. Worth doing for the
"scan to order" use case (kiosk-less ordering, scan-to-pay flows
on tables) and for the protocol story.

A toggle in the settings panel chooses which flavor of per-item QR
the PDF emits (or off).

5. Generation pipeline

Two flavors:

  • On-demand: operator clicks "Download PDF", we generate
    synchronously and stream it. Works fine for restaurants in the
    5–50 item range.
  • Pre-rendered + cached: re-generate on every menu change,
    cache the result, serve cached. Worth it if PDFs end up bigger
    or if we want to publish a NIP-94
    file metadata event referencing a permanent URL for the PDF —
    i.e. the printed menu itself becomes a Nostr-discoverable
    artifact.

Recommendation: ship on-demand first. Add caching + NIP-94 publish
as a second issue once we know the generator is solid.

6. Versioning + provenance

A printed menu has a shelf life. Helpful additions:

  • Generation timestamp in the footer, in the restaurant's local
    timezone.
  • Optional short hash (last 8 chars of a SHA over the menu state)
    next to the timestamp, so the operator can tell two printed
    versions apart at a glance.
  • The footer QR could resolve to a permanent record of that exact
    menu version
    (NIP-94 event with file URL + content hash). Nice
    to have, not v1.

7. Accessibility / readability

  • Sane defaults: 11pt body, 14pt section heads, real font (not the
    Quasar-via-CDN system stack). Pull a free print-quality face
    (e.g. Inter, Source Sans, IBM Plex) and embed it.
  • High-contrast mode toggle for venues with low light.
  • Allergen icons should be both visual + textual ("⚠ Nuts") for
    screen-reader / OCR friendliness.

Stretch goals

  • Multi-language layout — English + local language side-by-side
    using item description i18n (when we add it).
  • Per-table QR variants — same PDF, but each table gets a
    unique QR encoding ?table=<n> so orders carry table metadata.
  • Auto-publish — when the operator generates a PDF, optionally
    upload it to Blossom / a self-hosted file host and publish a
    NIP-94 event so the menu artifact is part of the restaurant's
    Nostr presence.
  • Daily-specials sheet — a short variant filtered to
    is_featured=true or items in the lunch availability window.
  • NFC pairing — same payload as the QRs, NDEF-encoded, for the
    next generation of phones.

Acceptance criteria for v1

(Subject to the design decisions above being settled. Listed so we
have a checkpoint when we break this issue down.)

  • GET /api/v1/restaurants/{id}/menu.pdf returns a
    application/pdf stream, gated by the admin key.
  • At least one preset theme; operator can override with a
    custom CSS string in restaurant settings.
  • Layout walks the full menu-tree (any depth up to the
    cap), renders items at any level, respects sold-out + dietary
    + allergen + modifier hints + currency.
  • At least the restaurant-level QR set (cover + WiFi +
    Nostr profile) lands in v1. Section + per-item QRs can be
    follow-up issues if they balloon.
  • CMS adds a "Download PDF" button on the menu page.
  • Footer carries a generation timestamp and a short version
    hash.
  • docs/index gets a pdf-export note describing the
    pipeline + theme system.

Files / surfaces touched (rough)

  • views_api.py — new route
  • services.py (or new pdf.py) — generation pipeline
  • templates/restaurant/menu_print.html — print stylesheet
  • static/css/print/*.css — preset themes
  • static/js/menu.js — Download button
  • models.py / migrations.py — possibly new restaurant fields
    (wifi_ssid, wifi_password, print_theme)
  • pyproject.tomlweasyprint (or chosen lib) + qrcode
  • docs/pdf-export.md (new) + cross-link from
    docs/index.md, docs/cms.md, docs/architecture.md

References

## Background Restaurants want a physical, printable menu they can hand customers, laminate, tape to walls, or fold into a table tent. Today the menu only exists in the database, on Nostr, and as live UI in the customer webapp. There's no path from the operator's CMS to a print-ready artifact. A PDF generator inside the extension closes that loop. It also becomes a useful **bridge** between the printed and the digital worlds — every item in the database carries enough structure (`name`, `description`, `price`, `dietary`, `allergens`, `nostr_event_id`, ancestor [[menu-tree]] context) that a well-rendered PDF can encode all of it, and QR codes on the page can deep-link printed items back to the live, dynamic experience. This is a brainstorm issue. We can break it into smaller deliverables once the shape is clearer. ## Why this is interesting - **Offline-friendly fallback.** If wifi or the local Nostr relay hiccups, the printed menu is still complete and current as of the last print. - **Marketing artifact.** A nicely-laid-out PDF is also what a restaurant emails to customers, posts on social, or sends to a print shop. - **Bridge to the webapp.** A QR on the printed menu can take a customer straight into the live ordering flow — no app install, no typing URLs. - **Bridge to Nostr.** A QR can encode an `naddr1...` pointing at a kind-30402 listing so any Nostr client (not just our webapp) can resolve and act on a printed item. ## Goals - An operator clicks a button in the [[cms]] and gets back a print-ready PDF of the current menu, branded for their restaurant. - The PDF reflects the *live* state at generation time: prices, sold-out flags, availability windows, modifier groups. - The PDF embeds at least one QR code; the exact set is a design decision (see below). - The endpoint is gated by the wallet's admin key so the PDF can also be fetched programmatically by integrations. ## Non-goals (for v1) - Editing the PDF's layout WYSIWYG-style. Operators pick a preset theme or supply their own template; that's it. - Generating per-table QR codes / table-tent variants. Stretch. - Print-shop-grade color profiles, bleeds, crops. Operators going to a real print shop should expect to feed the PDF through a pro tool. ## Open design questions ### 1. Where does PDF generation happen? - **Server-side, Python** with [WeasyPrint](https://weasyprint.org/), [reportlab](https://www.reportlab.com/), or [fpdf2](https://py-pdf.github.io/fpdf2/). WeasyPrint is the obvious pick: it consumes HTML+CSS, so the same Jinja templates that drive the [[cms]] can be the source of truth for layout. New paths just produce alternate stylesheets. - **Headless browser** (Playwright / Puppeteer). Cleaner CSS support, bigger dependency. - **Client-side JS** (jsPDF, pdfmake). Fastest iteration, but means every consumer has to render — the operator clicks "Download" and the browser does the work. Recommendation to validate: WeasyPrint server-side, with a Jinja template at `templates/restaurant/menu_print.html` and an admin endpoint `GET /api/v1/restaurants/{id}/menu.pdf?theme=<name>` that streams `application/pdf`. Keeps the source of truth on the server and matches LNbits's existing template conventions. ### 2. What does the layout look like? A first cut: - **Cover** (optional first page): logo, banner image, name, tagline, location, social links, header QR. - **Body**: walks the [[menu-tree]] depth-first. - Top-level node = section heading. - Subcategories = subsection headings, indented or boxed. - Items at any node level (since [[menu-tree|items can attach anywhere]]). - Item row: name, dietary icons, optional small image, dotted leader to the price. - Optional second line: description, allergen badges, modifier hints ("with chicken +5 / tofu +3"). - **Footer**: restaurant pubkey (so a curious customer can find them on Nostr), generation timestamp, page numbers. Open: which preset themes to ship? *Modern minimal*, *rustic warm*, *classic monochrome*? My instinct: ship one solid default, plus a single CSS hook for operator overrides. ### 3. Currency / multi-currency Items have their own `currency` field. The PDF should: - Show prices in their declared unit (`50 GTQ`, `1500 sat`, etc.). - Optionally show a converted reference (e.g. *(~$6.40 USD)*) using LNbits's `fiat_amount_as_satoshis` plumbing. - Surface a single restaurant-wide display preference in the settings panel (display in declared currency only, declared + converted, sat only, etc.). ### 4. QR codes — three flavors, all worth shipping We can do all three; they aren't mutually exclusive. They're listed roughly in increasing complexity so we know what to land first. #### a) Restaurant-level QRs (cover / footer) - **"Order online" QR** in the cover header → links to the customer webapp's restaurant page, e.g. `https://atitlan.io/r/<slug>`. Tap to enter the live menu. - **WiFi QR** (`WIFI:T:WPA;S:<ssid>;P:<password>;;`) using credentials from a new (optional) `restaurant.wifi_*` field. Lots of cafés already use these on table tents. - **Restaurant Nostr profile QR** encoding the kind-0 pubkey as `nostr:npub1...` so a Nostr-native client can follow the restaurant directly. These are all once-per-PDF and don't require any per-item tracking. #### b) Section / category QRs - A small QR next to each section heading that deep-links into the webapp at that branch of the menu, e.g. `https://atitlan.io/r/<slug>#node=<id>`. Useful when the printed menu is a tease (limited space) and the digital one has the full range. - Optionally encodes the [[menu-tree|materialized path]] so a client without our webapp can still parse the link. Medium effort. Mostly a layout decision (where to put them so they don't dominate the page). #### c) Per-item QRs (the Nostr-native one) - Each item gets a small QR encoding either: - `nostr:naddr1...` — a NIP-19 addressable pointer to the kind-30402 [[nostr-layer|classified listing]], so any Nostr client can resolve and act. **This is the most protocol-pure option** — works without our webapp, works offline-of-our-server, works in the future once more clients exist. - or a "scan to add to cart" URL (`https://atitlan.io/r/<slug>/ add?item=<id>`) that loads the item into the webapp cart pre-selected. Heaviest visually — more QR codes per page. Worth doing for the "scan to order" use case (kiosk-less ordering, scan-to-pay flows on tables) and for the protocol story. A toggle in the settings panel chooses which flavor of per-item QR the PDF emits (or off). ### 5. Generation pipeline Two flavors: - **On-demand**: operator clicks "Download PDF", we generate synchronously and stream it. Works fine for restaurants in the 5–50 item range. - **Pre-rendered + cached**: re-generate on every menu change, cache the result, serve cached. Worth it if PDFs end up bigger or if we want to publish a [NIP-94](https://github.com/nostr-protocol/nips/blob/master/94.md) *file metadata* event referencing a permanent URL for the PDF — i.e. the printed menu itself becomes a Nostr-discoverable artifact. Recommendation: ship on-demand first. Add caching + NIP-94 publish as a second issue once we know the generator is solid. ### 6. Versioning + provenance A printed menu has a shelf life. Helpful additions: - Generation timestamp in the footer, in the restaurant's local timezone. - Optional short hash (last 8 chars of a SHA over the menu state) next to the timestamp, so the operator can tell two printed versions apart at a glance. - The footer QR could resolve to a permanent record of *that exact menu version* (NIP-94 event with file URL + content hash). Nice to have, not v1. ### 7. Accessibility / readability - Sane defaults: 11pt body, 14pt section heads, real font (not the Quasar-via-CDN system stack). Pull a free print-quality face (e.g. Inter, Source Sans, IBM Plex) and embed it. - High-contrast mode toggle for venues with low light. - Allergen icons should be both visual + textual ("⚠ Nuts") for screen-reader / OCR friendliness. ## Stretch goals - **Multi-language layout** — English + local language side-by-side using item description i18n (when we add it). - **Per-table QR variants** — same PDF, but each table gets a unique QR encoding `?table=<n>` so orders carry table metadata. - **Auto-publish** — when the operator generates a PDF, optionally upload it to Blossom / a self-hosted file host and publish a NIP-94 event so the menu artifact is part of the restaurant's Nostr presence. - **Daily-specials sheet** — a short variant filtered to `is_featured=true` or items in the lunch availability window. - **NFC pairing** — same payload as the QRs, NDEF-encoded, for the next generation of phones. ## Acceptance criteria for v1 (Subject to the design decisions above being settled. Listed so we have a checkpoint when we break this issue down.) - [ ] `GET /api/v1/restaurants/{id}/menu.pdf` returns a `application/pdf` stream, gated by the admin key. - [ ] At least one preset theme; operator can override with a custom CSS string in restaurant settings. - [ ] Layout walks the full [[menu-tree]] (any depth up to the cap), renders items at any level, respects sold-out + dietary + allergen + modifier hints + currency. - [ ] At least the **restaurant-level QR set** (cover + WiFi + Nostr profile) lands in v1. Section + per-item QRs can be follow-up issues if they balloon. - [ ] CMS adds a "Download PDF" button on the menu page. - [ ] Footer carries a generation timestamp and a short version hash. - [ ] [[docs/index|Docs]] gets a `pdf-export` note describing the pipeline + theme system. ## Files / surfaces touched (rough) - `views_api.py` — new route - `services.py` (or new `pdf.py`) — generation pipeline - `templates/restaurant/menu_print.html` — print stylesheet - `static/css/print/*.css` — preset themes - `static/js/menu.js` — Download button - `models.py` / `migrations.py` — possibly new restaurant fields (`wifi_ssid`, `wifi_password`, `print_theme`) - `pyproject.toml` — `weasyprint` (or chosen lib) + `qrcode` - `docs/pdf-export.md` (new) + cross-link from `docs/index.md`, `docs/cms.md`, `docs/architecture.md` ## References - WeasyPrint: https://weasyprint.org/ - NIP-19 (`naddr1...`): https://github.com/nostr-protocol/nips/blob/master/19.md - NIP-94 (file metadata): https://github.com/nostr-protocol/nips/blob/master/94.md - NIP-99 (classified listings — what we already publish): https://github.com/nostr-protocol/nips/blob/master/99.md - WiFi QR format spec: https://en.wikipedia.org/wiki/QR_code#WiFi_network_login
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/restaurant#1
No description provided.