Dev.to WebDev 🛠 Dev 👁 0 📖 11 min read

Half of all café Wi-Fi QR signs fail on the string, not the printing. Here is the WIFI: URI, five traps, and the A4 four-up that carries it.

A café prints a card with a QR code that joins the guest Wi-Fi. It works on the owner's iPhone. It does not work on the Android at table four, and the staff explain that the customer "needs the app", which is not true. A

A café prints a card with a QR code that joins the guest Wi-Fi. It works on the owner's iPhone. It does not work on the Android at table four, and the staff explain that the customer "needs the app", which is not true. A week later the card is taped over with a Post-it that says the password, and the QR code becomes a lesson about technology that nobody asked for.

I build a one-page site generator for small shops, and the Wi-Fi card is one of the printed pieces it produces. Debugging our own sheets for a year turned up an uncomfortable split: the failures I get mail about are almost never print failures. They are payload failures. The code is sharp, the size is generous, the sharpness is fine — and the string inside it is wrong in a way that one phone forgives and another does not.

So this is the other half of the QR printing story. The measured print side is worth reading, but here I am going to stay inside the data: what a WIFI: payload is, five ways it breaks, and the four-up A4 geometry that carries two codes on one card.

The payload is a URI, not a URL

A QR code holds text. Nothing about the symbol knows or cares what the text means. For Wi-Fi, the text is a URI in a scheme called wifi:, and that scheme is not folklore — it started as a convention in the ZXing barcode library, got adopted by every generator on the internet, and is now written into the Wi-Fi Alliance's WPA3 specification, section 7.1, with the scheme registered with IANA on 26 August 2020.

The minimal form:

WIFI:T:WPA;S:Kestrel Coffee;P:oatlatte2026;;

Four pieces of punctuation do all the work:

  • WIFI: marks the scheme.
  • Each field is KEY:value and every field ends in a semicolon.
  • The string ends with a second semicolon — two in a row. It closes the URI, and it is the single most commonly dropped character in hand-built codes. Some readers accept one; others stop parsing and quietly refuse.
  • The fields that matter are T (security type), S (SSID), P (password) and H (hidden network). A R: field exists for transition-disable mode, which almost no café needs.

That is a small enough grammar to get right by hand. Which is exactly why the failures are so strange: the grammar is not the problem. The problem is everything the grammar does not say.

Trap 1: T:WPA2 and T:WPA3 are not values

The T field takes a security type. In the specification, password-protected networks are WPA — one value that covers WPA, WPA2 and WPA3 alike, because the underlying credential is a pre-shared key in all three. Open networks are nopass.

WPA2, WPA3, SAE and WPA2-PSK are not values in this grammar. Generators emit them anyway, because a form asked "which security type" and offered a dropdown of the router's own terminology. A reader that does not recognise the value may fall back to "unknown type" and refuse, or may ignore the field and try the password anyway, which is why the same wrong string "works everywhere except Steve's phone".

Same family of bug: an open network written as P: with nothing after it. That is not an open network, that is a malformed password field on some readers. An open network is T:nopass with no P: at all:

WIFI:T:nopass;S:Kestrel Coffee;;

And one more, from the router end: WEP. The WPA3 specification does not define WEP at all. If a shop is still broadcasting WEP, the QR code is not the thing to fix that afternoon.

Trap 2: the semicolon that two platforms escape differently

This is the one that produces the "it works on my phone" support call, and it took me a while to believe it because it looks like a bug in someone else's parser.

The semicolon is the field separator. If a password contains one, it has to be escaped. There are two escaping conventions in the wild:

  1. Percent-encoding, which is what the specification says: characters outside the allowed set are encoded as %XX, so ; becomes %3B.
  2. Backslash escaping, the older ZXing convention, which Android still emits: ; becomes \;.

The two decoders do not agree. A code built to the letter of the specification can be read by one platform and misread by another, and a code built with backslashes has the mirror problem. A password like cafe;2026 is therefore not a password problem or a printing problem — it is a payload dialect problem that nobody can fix at the counter.

I wrote the two escapers to see exactly what comes out:

const SPECIAL = /([\\;,":])/g;
const backslash = (s) => s.replace(SPECIAL, (m) => "\\" + m);
const percent = (s) =>
  s.replace(SPECIAL, (m) => "%" + m.codePointAt(0).toString(16).toUpperCase().padStart(2, "0"));

backslash("cafe;2026"); // "cafe\;2026"  — code points 99,97,102,101,92,59,50,48,50,54
percent("cafe;2026");   // "cafe%3B2026"

Two strings, one password, two possible behaviours. And the code that scans the card has no way to tell the customer which one it is.

My conclusion after testing this on both platforms is the boring, correct one: if a password is going on paper, it should be letters and digits only. No punctuation, no spaces. A twelve-character lowercase-and-digits string is stronger than FlatWhite2019! anyway, and it removes the entire class of failure. On the router, the guest SSID password is a setting; on the card, it is a decision made once and reprinted a few times a year. Fix it where it is cheap to fix.

Trap 3: H:true when the network is not hidden

H:true tells the phone the SSID does not broadcast. A phone given H:true for a network that is broadcasting will look for it as a hidden network, which changes the association flow, and on some handsets fails outright — while the network is sitting there, strong, with the same name.

If you did not deliberately turn off SSID broadcast on the router, H: does not belong in the string. If you did, H:true is required, and you should ask why the SSID is hidden at all — a hidden SSID is a mild inconvenience to an attacker and a real one to customers.

Trap 4: the SSID that is almost the SSID

The S field has to be the network name exactly as the phone displays it: same case, same spaces, same suffix.

  • Kestrel Coffee is not KestrelCoffee, and it is not kestrel coffee.
  • A dual-band router often broadcasts Kestrel Coffee on 2.4 GHz and Kestrel Coffee 5G on 5 GHz. Those are two networks. A code for one is not a code for the other, and the phone will happily fail on the one it is standing next to.
  • A trailing space in the SSID field is invisible in every editor and breaks the match on every phone. If you build these strings in code, trim the SSID where the owner types it, not in the encoder — a legitimately trailing space is a thing routers allow, and you want to see it if it happens.

The cheap test is the one I now do on every batch: print the string in plain letters under the code. Every reader that does not understand the format shows the raw text instead of joining, and a customer who can read Kestrel Coffee / oatlatte2026 is joined in ten seconds. It also makes the wrong SSID visible to you before 200 cards are printed.

Trap 5: expecting the code to do something it cannot

Three things the wifi: URI does not carry, which I have been asked for more than once:

  • A captive portal. The phone joins and the "accept the terms" page appears exactly as before. The format has no field for it. The code still saves the typing; it cannot skip the portal. Write the line "then tap Connect on the page" on the card and the customer knows what to expect.
  • Enterprise (802.1X) credentials. Username-and-certificate networks are outside this scheme. A café's pre-shared key is the case it solves.
  • A page and a network in one symbol. One QR symbol carries one payload. A phone cannot be told to join a network and open a URL by the same code, because a browser is not allowed to change the network. That is why the card has two codes side by side instead of one clever one — and why "can't you just put both in one code" has a technical answer, not a design preference.

The card that carries both codes, in millimetres

The card I ship is an A4 sheet that cuts into four cards. Not a mock-up: a generated PDF with vector everything, measureable in the file. The numbers below are read from it.

What Measured Why
Page 210 × 297 mm (A4) Four cards per sheet: one print, four cards
Card 93.0 × 127.5 mm Four-up on A4 with 8 mm gutters
Gutter / margins 8 mm, 16 top, 18 bottom Guillotine clearance with a white edge
Code width 48 mm each, two per card The customer's hand to a card at the till is 40–60 cm
Wi-Fi symbol version 3, 29 modules 37 modules with the quiet zone → 1.30 mm per module
Menu symbol version 4, 33 modules 41 modules with the quiet zone → 1.17 mm per module
Quiet zone 4.7–5.2 mm per side Four modules: Denso Wave's FAQ and the GS1 retail guideline
Scan distance ≈ 50 cm The 1:10 rule of thumb — a tenth of the reading distance

Two things in that table are worth dwelling on, because they are the parts people get wrong when they design a card by eye.

The two codes are not the same version, and that is not an error. The WIFI: string is 44 characters for that example; a menu URL is longer. Longer payload, higher version, more modules, smaller modules in the same 48 mm box. Both land far above the floor — 1.17 mm against Denso Wave's stated 0.17 mm minimum for a common laser printer, and far above the 0.33 mm floor implied by "at least four printer dots per module" on a 300 dpi thermal printer — but the arithmetic explains why a designer's instinct to "make the codes identical" produces one code that is visibly denser than the other.

The 1:10 rule is a rule of thumb, not a standard. It is not in ISO/IEC 18004. It is what business cards, menus and signage converge on because it works at typical arm lengths: a 48 mm code at 50 cm, a 126 mm code on an A4 poster from a metre and a half. If your code has to survive two metres through a shop window, the honest move is to test it at two metres rather than to trust the ratio.

And the same generated file answers the question a designer would ask next: the 48 mm code is 37 modules including the quiet zone, so nothing may be printed in that border — no logo, no rule, no "SCAN ME" arrow crossing it. The quiet zone is not decoration. It is what tells the camera where the symbol ends.

Four print decisions that are really one decision

Print at 100%, never "fit to page" — A4 artwork on US Letter is scaled to about 94%, and every module shrinks with it. Matt laminate, because gloss under a downlight throws a highlight exactly when someone tilts the phone. Black on white, because inverted codes fail on a meaningful share of cameras. Two phones, at the counter, before the batch: one test sheet costs cents and a reprint of two hundred costs an afternoon.

The fourth decision is the one I did not expect: generate the card from the same record as the page. Not because it is elegant, but because the alternative is a password that changes on the router in March and a card that still says the old one in June. The Wi-Fi string, the SSID in plain text under the code, and the URL under the menu code all come from the same fields the owner typed once. A password rotation is then one edit and one reprint — and the card and the page behind the menu code cannot drift apart.

What it costs, honestly

The Wi-Fi sheet is 10-wifi-pop-A4.pdf in the printed kit this site produces: fifteen printed pieces for a shop with tables from $49 once, eight for a shop without tables. The kit is not the only way to get a Wi-Fi QR code, and it should not pretend to be. A copy shop prints an A4 in colour for cents. Canva has free templates. The router's own app will draw a code for you. Each of those is a fine answer to "I need one code this afternoon".

What none of them does is keep the card true over time, and that is the only claim I am confident in, because it is the failure I see in real shops: three files named wifi-final, wifi-final2 and wifi-final-print, and nobody knows which one matches the router.

If the real question under all this is whether a shop should be paying a builder every month for a page and a printed card, the three-year cost calculator does the arithmetic with the vendor list prices read on 14 November 2026: Squarespace Basic at $19 a month billed annually ($25 monthly) is $684 over three years; Linktree Pro at $14 annually ($17 monthly) is $504; Wix Light at $17 on a year plan is $612. None of them includes a printed Wi-Fi card, and none of them will tell you that the password on it has a semicolon in it.

A checklist for the next café that asks

  1. Router: guest SSID, isolated from the till and the office. Password of letters and digits, twelve characters or more.
  2. String: T:WPA for a password, T:nopass for an open network, S exactly as broadcast, H:true only if the SSID is hidden, and the closing double semicolon.
  3. Two codes: the WIFI: payload and a menu URL, side by side, never one symbol for both.
  4. Print: 48 mm codes on the card, 100% scale, matte, and the quiet zone empty.
  5. Proof: one sheet, cut one card, scan it with an iPhone and an Android at 50 cm with the shop's lights on.
  6. Fallback: SSID and password in plain letters under the code, page address under the menu code.
  7. Rotation: quarterly is realistic; one edit, one reprint.

The card is not clever. It is two answers, printed at a size that scans, from a string that both platforms parse the same way — which is most of the work, and none of it is in the printer.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.