Your QR code should hold a URL, not a price list — a 40-character lesson from a grooming shop
A dog groomer's front desk gets the same question all day: how much to groom a cockapoo? The answer is not a number. It is a weight band, a coat, and a condition — three facts the person on the other end of the phone has
A dog groomer's front desk gets the same question all day: how much to groom a cockapoo? The answer is not a number. It is a weight band, a coat, and a condition — three facts the person on the other end of the phone has not thought about yet.
That makes it a data-modelling problem hiding inside a sign-printing problem. And the printing half has arithmetic that bites you about a year later, when a printed code points at last season's prices.
The table is the product
Five rows, four columns. That is the whole model:
| Band | Bath & brush | Full groom | What moves it |
|---|---|---|---|
| Under 10 lb | $45 | $65 | Matted ears, a shave-down, a nervous first visit |
| 10–25 lb | $55 | $80 | Hand-dry vs kennel-dry, de-shed, teeth |
| 25–50 lb | $70 | $100 | Drying time, spaniel ears, a double coat |
| 50–80 lb | $85 | $120 | De-shed season, an undercoat that has blown |
| Over 80 lb | $100 | $150 | A second pair of hands, a second table |
Those prices are a layout example, not an average — a shop sets its own numbers from its own day. The part worth copying is the shape: the row a customer can check at home (their bathroom scale), two prices they can compare, and a column that names the thing which changes the price, so the change is not a surprise at collection.
If you are writing this as data rather than as HTML, the model is one array and one function:
const BANDS = [
{ max: 10, bath: 45, groom: 65 },
{ max: 25, bath: 55, groom: 80 },
{ max: 50, bath: 70, groom: 100 },
{ max: 80, bath: 85, groom: 120 },
{ max: Infinity, bath: 100, groom: 150 },
];
const bandFor = (lbs) => BANDS.find((b) => lbs <= b.max);
The function is trivial. The interesting constraint is that it can never be the source of truth for the printed sign, because the sign is laminated and the price is not.
Why the code has to hold the address
A QR code is a string with error-correction baked around it. Whatever you put in it is what the paper says, forever. So the only question that matters is which string:
- The page address. The code is a door. Everything behind the door — prices, hours, add-ons — changes without touching the door. One print run, five years of price changes.
- A PDF of the price list. The code is a photocopy. Re-upload the PDF and it gets a new address, so the laminated sign points at a 404, or at last season's prices. This is the single most common way a "QR menu" project dies quietly.
Same goes for the payload length, and this is testable. The module count grows with the string, and the module is the only thing that has to survive your printer. Measured with qrcode at error correction M, the same code the print kit uses:
| 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 looks like 12%. It is not: a QR symbol grows in both directions, so the printed square takes about a quarter more ink for the same piece of paper. At 40 mm on a sticker, that is the difference between a code that scans at arm's length and one that needs the phone to be closer than the customer will hold it.
So: track the link in the email, the bio and the analytics dashboard. Put the clean address on the chipboard.
Millimetres, not vibes
Print sizes are where "it should be fine" gets expensive. The numbers below are measured from the actual PDFs a print kit produces (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 |
| 70 mm sticker | 70 mm round | 40 mm | ~0.4 m |
| A4 window poster | 210 × 297 mm | 126 mm | ~1.3 m |
"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 and expect the phone to take a second longer.
Two implementation notes that have cost me reprints:
- Print at 100%. "Fit to page" is how a 48 mm code becomes a 41 mm code, and a 41 mm code is how it "worked on screen".
- Print one piece before the batch. A code that fails on the counter card fails fifty times. Print it, put it where it will live, and scan it twice — once standing, once leaning.
What the page is not
It does not run the calendar. Grooming software with online booking, deposits, text reminders and card-on-file does that, and it charges for it — MoeGo's own pricing page, read on 14 November 2026, lists its mobile-grooming plans at $49, $99 and $159 a month (Basic, Growth, Ultimate).
That is a fair price for a calendar that fills itself. It is not a price list on a wall, and no customer has ever scanned a QR code hoping to reach a booking calendar with no prices on it.
The honest comparison, then, is not "page vs software". It is "page vs the price sheet that gets reprinted whenever a supplier raises a fee, the PDF emailed to every new customer, and the four minutes of how much, and does that include nails?" — the work that a single page and a single code actually remove.
The build, if you want to copy it
I wrote this up in full, with the clauses a groomer can print under the table (matting as a shave, handling, no-shows) and the measured print geometry:
→ Dog grooming prices on one page
The implementation is deliberately boring: a static page with the table as a <table>, the weight band function above if it ever becomes dynamic, a <link rel="canonical">, and a QR image generated once from the canonical address. No build step, no framework, no database — the thing has to load in about a second on a phone held by someone standing in a shop doorway with a dog on a lead.
If you want the printing side without building it yourself, the kit I ship is one payment ($49 for the eight-file version, delivered within 24 hours). If you would rather do it yourself, the geometry above is everything you need — measure your own card, print at 100%, and put the code where the question is asked.
And 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 way that comparison is honest.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.