Time is the SKU: modelling a massage menu as data, then printing it once
A massage studio sells minutes. Not treatments, not "experiences" — minutes in a room with a therapist in it. Once you see that, the price list stops being a design problem and becomes a small modelling one: what are the
A massage studio sells minutes. Not treatments, not "experiences" — minutes in a room with a therapist in it. Once you see that, the price list stops being a design problem and becomes a small modelling one: what are the units, what can be added to a unit, and what must never be baked into the printed artefact.
I build tiny pages for shops that have no website (and print the QR pieces that go with them, which is the part nobody warns you about), and the massage menu is the cleanest example of the pattern. Here is the model, the function, and the two printing constraints that decide whether it survives a year.
The inventory is the therapist's hour
A restaurant table can turn over. A treatment room cannot: a 90-minute booking removes exactly 90 minutes from the day, plus the ten minutes of reset nobody bills. So a studio's menu is really a capacity table:
- 60 minutes — the default slot, the one that fills a Tuesday.
- 90 minutes — the upgrade, and the one worth printing beside the 60 rather than under it, because it is where the room's fixed cost already got paid.
- 120 minutes — couples, post-race, gift vouchers. The one that needs two therapists and one clock.
That is the whole reason duration belongs in the first column and technique in the third. A person booking cannot price "deep tissue" without knowing how long; they can price "90 minutes" without knowing anything else.
The table, as shape rather than as prices
Four columns is what fits on a phone without pinching: the treatment, its 60-minute price, its 90-minute price, and what the extra half hour actually buys. The numbers below are a layout example — a studio sets its own from its own day:
| Treatment | 60 min | 90 min | What the extra half hour buys |
|---|---|---|---|
| Swedish / relaxation | $85 | $120 | Full legs and arms, not just back and shoulders |
| Deep tissue | $105 | $150 | Two problem areas instead of one, at a pace that does not rush |
| Sports / pre-race | $110 | $155 | Assessment time written into the booking |
| Couples, same room | $170 / pair | $240 / pair | Two therapists, one room, one clock |
The fourth column is the one people skip, and it is the one that does the selling. "90 minutes" is an abstract upgrade; "the hour and a half is what buys your legs as well" is a decision a body can make.
Under the table, the add-ons as prices instead of as a sentence — hot stone $20, aromatherapy $10, a twenty-minute scalp and neck $25. Two jobs in one list: nothing is a surprise at payment, and the upsell happens at booking, while the customer is still choosing a duration, instead of at the counter with a coat already on.
The model, if you keep it as data
If the menu is generated rather than typed, the useful shape is a small array of durations plus a map of add-ons, not a table of thirty treatments:
const TIERS = [
{ minutes: 60, price: 85 },
{ minutes: 90, price: 120 },
{ minutes: 120, price: 170 },
];
const ADDONS = { 'hot-stone': 20, aromatherapy: 10, 'scalp-neck': 25 };
const total = (minutes, addonKeys = []) => {
const tier = TIERS.find((t) => t.minutes === minutes);
if (!tier) throw new Error(`no tier for ${minutes} min`);
return addonKeys.reduce((sum, k) => sum + ADDONS[k], tier.price);
};
The interesting part is not the arithmetic — it is that this function must never be the thing the printed menu is generated from at print time. The printed artefact is a snapshot with a date on it, and when a price changes you want the live page to move and the paper to stay valid. That is only possible if the paper does not contain the prices at all.
The code holds the address, not the menu
A QR code is a string with error correction baked around it. Whatever you put in it is what the paper says, forever. Two choices:
- The page address. The code is a door. Prices, hours, add-ons change behind it without touching the door. One print run, years of price changes.
- A PDF of the menu. The code is a photocopy. Re-upload the PDF and it gets a new address; the laminated sign by the entrance now points at a 404 or at last season's rates. This is the most common way a "QR menu" project dies — quietly, about a year after launch.
Payload length is testable, and it matters at the printer. Measured with qrcode at error correction M, the same call the print kit makes:
| Payload | Bytes | Modules | Linear size |
|---|---|---|---|
| Clean page address | 47–62 | 33 | 100% |
| Address + tracking string | 63–70 | 37 | 112% |
| A long UTM block | 117 | 45 | 136% |
33 → 37 modules reads like 12%, but a symbol grows in both directions: it is roughly a quarter more ink for the same piece of paper, and at a 40 mm sticker that is the difference between scanning at arm's length and needing the phone closer than any customer will hold it. Track the link in the email and the bio; put the clean address on the door.
Millimetres, from the PDFs rather than from a blog post
These are measured in the actual print PDFs (93.0 × 127.5 mm card, 70 mm round sticker, A4 poster), at 33 modules plus a four-module quiet zone:
| Piece | Sheet | Code | Reads at |
|---|---|---|---|
| Counter card | 93.0 × 127.5 mm | 48 mm | ~0.5 m — the person paying |
| 70 mm sticker | 70 mm round | 40 mm | ~0.4 m — the changing-room mirror |
| A4 door poster | 210 × 297 mm | 126 mm | ~1.3 m — nose against the glass |
"Reads at" comes from the industry rule of thumb of one unit of code per ten units of distance. It is a rule of thumb, not ISO/IEC 18004, and it is a floor: behind reflective glass, under gloss laminate, or through a bowing plastic sleeve, plan for 1:8.
Two things that have cost me reprints:
- Print at 100%. "Fit to page" turns a 48 mm code into a 41 mm code, and a 41 mm code is how a code "worked on screen".
- Print one piece before the batch. A code that fails on the counter card fails fifty times.
What the page is not
It is not the booking engine. Software that holds therapist rotas, deposits and reminders is a different product with its own monthly price, and a studio that needs it should keep it. I am not quoting those vendors' prices here: their pages change monthly and none was read on a dated day for this piece — a price without a date is a rumour.
The honest comparison is not "page vs software". It is "page vs the treatment sheet that gets reprinted when a room price rises, the PDF sent to every enquiry, and the four minutes of how much for ninety, and does that include hot stone?" that a therapist answers with a towel in hand.
The write-up, if you want the clauses too
I wrote the full version, including the three sentences a studio should print under the menu (late and no-show, intake and pressure, and what a massage is not), and the measured geometry above:
→ A massage and spa treatment menu on one page
The implementation stays boring on purpose: a static page, the table as a <table>, a canonical link, and one QR image generated from the canonical address. A studio has no tables to print for, so the kit ships as the live page plus eight files — one payment of $49, delivered within 24 hours — and if you would rather print it yourself, the numbers above are all you need.
Either way, if you are weighing it against a monthly builder, the three-year calculator takes your monthly figure and shows the total, which is the only version of that comparison worth making.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.