<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: xiaopeng</title>
    <description>The latest articles on DEV Community by xiaopeng (@buildwithsalt).</description>
    <link>https://dev.to/buildwithsalt</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4120479%2F1a3fa834-7036-4adf-8606-f6f9d46f2892.png</url>
      <title>DEV Community: xiaopeng</title>
      <link>https://dev.to/buildwithsalt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/buildwithsalt"/>
    <language>en</language>
    <item>
      <title>Why most shop QR codes fail: the engineering behind location QR codes</title>
      <dc:creator>xiaopeng</dc:creator>
      <pubDate>Tue, 10 Nov 2026 12:45:46 +0000</pubDate>
      <link>https://dev.to/buildwithsalt/why-most-shop-qr-codes-fail-the-engineering-behind-location-qr-codes-3mj3</link>
      <guid>https://dev.to/buildwithsalt/why-most-shop-qr-codes-fail-the-engineering-behind-location-qr-codes-3mj3</guid>
      <description>&lt;p&gt;A shop puts a QR code on the counter. A customer scans it. Nothing happens. Or something happens, but it's a Google Maps search box pre-filled with the &lt;em&gt;text&lt;/em&gt; "123 Main St, Springfield" — and the customer has to tap "search" themselves, on a phone, in a hurry, with one hand holding a bag.&lt;/p&gt;

&lt;p&gt;The owner almost never finds out. They tested it once, on their own phone, in their own scanner app, at arm's length, under the shop lights. Four variables, and three of them were wrong.&lt;/p&gt;

&lt;p&gt;A location QR code is one of the simplest QR payloads you can build, and also one of the easiest to get subtly wrong, because the failure is &lt;em&gt;silent&lt;/em&gt;: the code scans fine, the phone does something, and nobody complains.&lt;/p&gt;

&lt;p&gt;This is a working guide to the parts that actually decide whether it works in the real world: the payload, the QR version, the module pitch, the print substrate, and the test protocol. Nothing here requires a paid service.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The payload decides everything that follows
&lt;/h2&gt;

&lt;p&gt;A QR code is a container. A "location QR code" is just a container holding a string that a phone can turn into a map view. There are four realistic strings, and they behave very differently.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Payload&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Android&lt;/th&gt;
&lt;th&gt;iOS&lt;/th&gt;
&lt;th&gt;Failure mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;geo:&lt;/code&gt; URI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;geo:37.4224764,-122.0842499&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Opens default maps app&lt;/td&gt;
&lt;td&gt;Generally unhandled — nothing happens&lt;/td&gt;
&lt;td&gt;Dead on half your customers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Maps universal link&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://www.google.com/maps?q=37.4224764,-122.0842499&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Google Maps app if installed, else web&lt;/td&gt;
&lt;td&gt;Google Maps app if installed, else web&lt;/td&gt;
&lt;td&gt;None material&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apple Maps link&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://maps.apple.com/?q=37.4224764,-122.0842499&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Opens a web page&lt;/td&gt;
&lt;td&gt;Apple Maps&lt;/td&gt;
&lt;td&gt;Looks broken on Android&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plus Code (Open Location Code)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;8FVC9G8F+6W&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shows text, user must copy and paste&lt;/td&gt;
&lt;td&gt;Shows text, user must copy and paste&lt;/td&gt;
&lt;td&gt;Requires two more taps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The short version: &lt;strong&gt;use an HTTPS maps link, not &lt;code&gt;geo:&lt;/code&gt;&lt;/strong&gt;. &lt;code&gt;geo:&lt;/code&gt; is the "correct" URI scheme in the spec sense and the worst choice in practice, because iOS does not hand it to Apple Maps. You will test it on Android, it will work, and it will fail for roughly half of your customers.&lt;/p&gt;

&lt;p&gt;There is a second decision nested inside the payload: &lt;strong&gt;coordinates or a text address?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A text address (&lt;code&gt;...?api=1&amp;amp;query=123+Main+St&lt;/code&gt;) is friendly to read but brittle — it depends on the maps geocoder resolving that string, in that country, in that language. A mall unit number, a new building, or a non-Latin address will geocode badly or land on the wrong block. Coordinates are unambiguous, and they cost you nothing.&lt;/p&gt;

&lt;p&gt;Also relevant: &lt;strong&gt;precision&lt;/strong&gt;. Six decimal places is about 11 cm. Seven is noise. Writing &lt;code&gt;37.4224764&lt;/code&gt; when &lt;code&gt;37.4225&lt;/code&gt; would do is pure payload bloat, and payload bytes translate directly into a denser QR code (more on that in the next section). Truncate to 5–6 decimals and stop.&lt;/p&gt;

&lt;p&gt;If your location genuinely has no usable address — a trailhead, a market stall, a warehouse bay — the Plus Code is the right answer, because it resolves offline and does not depend on any geocoder. It just requires one extra user action, so it is a fallback, not a default.&lt;/p&gt;

&lt;p&gt;One more payload trap: &lt;strong&gt;non-ASCII characters&lt;/strong&gt;. A Chinese, Japanese or Arabic address inside a URL has to be percent-encoded, and every encoded character becomes three bytes. If you are building payloads from user input, encode the query component and prefer coordinates when the address is non-Latin.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Payload length → version → module size (the math nobody does)
&lt;/h2&gt;

&lt;p&gt;QR codes come in versions 1 through 40. The rule is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;modules per side = 17 + 4 × version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So version 1 is 21×21 modules, version 4 is 33×33, version 10 is 57×57, version 40 is 177×177.&lt;/p&gt;

&lt;p&gt;An encoder picks the &lt;em&gt;smallest&lt;/em&gt; version that fits your payload at your chosen error-correction level. Byte-mode capacity at level M is roughly: 14 bytes at v1, 26 at v2, 42 at v3, 62 at v4, 106 at v6, 213 at v10.&lt;/p&gt;

&lt;p&gt;Now the thing that matters: &lt;strong&gt;the QR code's printed width is fixed by your design, so version and module size trade off against each other.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module pitch (mm) = printed width (mm) / modules per side
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take a 50 mm counter sticker — a typical, reasonable choice.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Payload&lt;/th&gt;
&lt;th&gt;Length&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Modules&lt;/th&gt;
&lt;th&gt;Module pitch at 50 mm&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Short maps URL, coordinates&lt;/td&gt;
&lt;td&gt;54 chars&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;33×33&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.52 mm&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maps URL with a text address&lt;/td&gt;
&lt;td&gt;75 chars&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;37×37&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.35 mm&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maps URL + a tracking parameter&lt;/td&gt;
&lt;td&gt;120 chars&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;45×45&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.11 mm&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;geo: URI, same precision&lt;/td&gt;
&lt;td&gt;28 chars&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;29×29&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.72 mm&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is the whole argument in miniature: the &lt;code&gt;geo:&lt;/code&gt; version is &lt;em&gt;denser-looking but physically coarser&lt;/em&gt;, because it is shorter. Fewer characters means fewer modules means fatter modules at the same print size means it scans from further away, in worse light, with a cheaper phone. Every character you add costs you scan robustness.&lt;/p&gt;

&lt;p&gt;This is also why "just add a &lt;code&gt;utm_source&lt;/code&gt; and an ID and a timestamp" wrecks a sticker that used to work.&lt;/p&gt;

&lt;p&gt;Practical consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep the payload under 60 characters if the code will be printed small (under 60 mm).&lt;/li&gt;
&lt;li&gt;Do not append analytics parameters to a printed code. If you need scan counts, see section 7.&lt;/li&gt;
&lt;li&gt;Print from &lt;strong&gt;vector&lt;/strong&gt; output (SVG) whenever possible. A QR code is geometry; letting a print shop resample a 300 px PNG is how you get soft module edges.&lt;/li&gt;
&lt;li&gt;If you must ship a raster: at 0.9 mm module pitch you need at least 2 px per module, so a 50 mm/49-module code needs about 98 px across — but give the printer 1000+ px and let them downscale. Never upscale.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Scan distance is a geometric problem
&lt;/h2&gt;

&lt;p&gt;Two constraints, both real:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Module pitch vs camera resolution.&lt;/strong&gt; At close range a phone can resolve a module of about 0.3–0.4 mm on matte paper under decent light. Below that, decode fails on older cameras.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Field of view vs distance.&lt;/strong&gt; A rough industry rule: the usable scan distance is about &lt;strong&gt;10× the code's physical width&lt;/strong&gt;. A 50 mm code wants the phone within about 50 cm. A 300 mm poster code wants about 3 m.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Combine them when you &lt;em&gt;design&lt;/em&gt; the placement rather than the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;required width (mm) ≈ scan distance (mm) / 10
required module pitch (mm) = required width / modules per side
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a QR code has to be read from 3 m (a poster in a window, seen from the street), with a 33-module code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;width = 3000 / 10 = 300 mm
pitch = 300 / 33 = 9.1 mm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 30 cm QR code for a 3 m read. That feels absurdly large to a designer and is exactly right for a doorway. Conversely, a 40 mm table-tent card read from 25 cm only needs a 0.75 mm pitch — achievable, but only if the payload stays short enough to keep the version low.&lt;/p&gt;

&lt;p&gt;If you want a single number to remember: &lt;strong&gt;a 50 mm sticker with a short HTTPS payload is the sweet spot for counter and tabletop use.&lt;/strong&gt; Below 40 mm, stop optimizing the design and start shortening the payload.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Error correction: the advice you'll read everywhere is half wrong
&lt;/h2&gt;

&lt;p&gt;QR error correction levels recover approximate fractions of codewords: &lt;strong&gt;L ≈ 7%, M ≈ 15%, Q ≈ 25%, H ≈ 30%.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The common recommendation — "always use H, it's the most robust" — is wrong for printed stickers, and wrong for a mechanical reason: higher error correction means &lt;em&gt;more&lt;/em&gt; data has to fit in the same code, which pushes the version up, which shrinks the modules at your fixed print size. You have traded real, physical module size for an abstract recovery budget you probably don't need.&lt;/p&gt;

&lt;p&gt;Use H only when something is physically eating the code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A logo or graphic is overlaid. Keep it under 30% of the area if you use H. Under 15% with Q.&lt;/li&gt;
&lt;li&gt;The code is on a curved, dirty or partially-covered surface (a table edge, a crate, a machine panel).&lt;/li&gt;
&lt;li&gt;The code will be behind scratched acrylic, or outdoors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Otherwise, &lt;strong&gt;M with maximum physical size beats H with a denser grid&lt;/strong&gt; nearly every time. A clean 33-module code at 1.5 mm pitch will out-scan a 45-module code at 1.1 mm pitch in every real-world condition, including the one you care about: a phone at an angle, in a lit shop, by someone who is not trying very hard.&lt;/p&gt;

&lt;p&gt;One caveat where H genuinely does earn its keep: &lt;strong&gt;the code is going to get damaged over its life&lt;/strong&gt; — stickers on a counter get scratched, sprayed, and picked at. If the code must survive three years of that, H plus a laminating pocket is defensible. Just measure the resulting module size before you commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Print mechanics that quietly break scanning
&lt;/h2&gt;

&lt;p&gt;These are not QR-spec issues. They are the reasons a technically perfect file scans badly on a counter.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quiet zone.&lt;/strong&gt; The spec asks for 4 modules of empty margin. Brand templates trim it to look tight, and trimming it is one of the most common causes of "it works for me but not for customers." Whatever margin your generator emits, keep at least 4 modules, and more if the code sits on a busy background.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contrast, not color.&lt;/strong&gt; Dark modules on light background, high luminance contrast. Avoid mid-tone pairs (navy on teal scans badly at small sizes) and avoid metallic foils — gold on black has almost no luminance contrast despite looking like contrast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inversion.&lt;/strong&gt; A white code on a black sticker looks great and decodes unreliably; many scanners assume dark-on-light. If you invert, test on the oldest phone you can find.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specular highlights.&lt;/strong&gt; Gloss laminate plus a ceiling spotlight equals a blown-out patch — exactly where the finder patterns are, if you're unlucky. Matte laminate or uncoated stock is the cheap fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refraction.&lt;/strong&gt; A code printed on paper and placed &lt;em&gt;behind&lt;/em&gt; glass or acrylic gets a double image at an angle. Print on the outside face, or use a matte film on the inside.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Placement geometry.&lt;/strong&gt; Roughly 1.2–1.5 m above floor, flat to the viewer, ideally not directly under a downlight. A code stuck flat on a horizontal counter is scanned at a steep angle — a small angled stand fixes more real-world failures than any encoder setting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Building one in the browser (no server, no account)
&lt;/h2&gt;

&lt;p&gt;A location QR code needs no backend. The payload is a string, the encoder is a library, and the output is geometry.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// payload: coordinates win over text addresses&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sb"&gt;`https://www.google.com/maps?q=${lat.toFixed(6)},${lng.toFixed(6)}`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 0 = auto version, 'M' = error correction level&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;qr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;qrcode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'M'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;qr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Byte'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;qr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// margin: 4 -&amp;gt; the 4-module quiet zone; scalable -&amp;gt; vector output&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'out'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="n"&gt;qr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createSvgTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;scalable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four things to notice in those seven lines, because each one is a real decision:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;toFixed(6)&lt;/code&gt; — six decimals is centimetre accuracy. More is payload bloat.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0&lt;/code&gt; for version — let the encoder pick the smallest that fits. Hard-coding a version is how you end up with a code that stops scanning when someone adds a character to the address.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'M'&lt;/code&gt;, not &lt;code&gt;'H'&lt;/code&gt; — see section 4.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;margin: 4&lt;/code&gt; and &lt;code&gt;scalable: true&lt;/code&gt; — the quiet zone travels with the file, and the print shop gets vectors instead of pixels.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you serve this as a page, generate client-side and never round-trip the coordinates through your server: it's a single string, and a client-side generator means the tool keeps working when your hosting bill doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Static vs dynamic: pick based on the sticker's lifespan
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Static&lt;/strong&gt; (payload embedded in the code): works forever, works offline, needs no domain, cannot be revoked by anyone. Scans cannot be counted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dynamic&lt;/strong&gt; (short URL that redirects): destination is editable, scans are measurable — and the code dies the day the domain, the subscription, or the redirect service dies.&lt;/p&gt;

&lt;p&gt;A printed sticker on a shop counter lives 3–5 years. A free short-link service does not reliably live 3–5 years, and when it dies the shop has a dead square on its counter and no idea why. If you need the measurements, host the redirect yourself on a domain you own, with a static fallback: the redirect page should show the address and a maps link as plain text, so a dead redirect degrades to a usable page instead of a 404.&lt;/p&gt;

&lt;p&gt;For a small business, static plus a self-hosted one-line redirect behind your own domain is the only combination that survives contact with reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Test protocol (the part everyone skips)
&lt;/h2&gt;

&lt;p&gt;Print one sheet with three sizes — 40 mm, 60 mm, 100 mm — before you commit to a run of stickers. Then check, in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Three phones&lt;/strong&gt;: one iPhone, one Android, one at least four years old.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two readers&lt;/strong&gt;: the stock camera app, and one third-party scanner. The stock camera is the one your customers use; third-party apps are more forgiving and will hide your problems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expected distance, not arm's length.&lt;/strong&gt; Scan a counter sticker from where a customer actually stands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actual lighting.&lt;/strong&gt; Daylight, evening, and directly under the shop's downlight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After lamination&lt;/strong&gt;, if you're laminating.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After a week of dust and a thumbprint.&lt;/strong&gt; This is not paranoia; counter stickers get touched.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Failure symptoms map to causes with almost boring regularity:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nothing happens at all&lt;/td&gt;
&lt;td&gt;Code doesn't decode: module pitch too small, quiet zone trimmed, or contrast too low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opens a maps search box with text in it&lt;/td&gt;
&lt;td&gt;Payload was a text address, not coordinates or a maps URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opens a web page instead of the app&lt;/td&gt;
&lt;td&gt;No universal-link handler, or the app isn't installed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pin lands in the ocean&lt;/td&gt;
&lt;td&gt;Latitude and longitude swapped — the classic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opens zoomed out to city level&lt;/td&gt;
&lt;td&gt;No explicit zoom on the maps link&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works on your phone, not on customers'&lt;/td&gt;
&lt;td&gt;You tested with a lenient scanner app, at arm's length&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The one I'd underline: &lt;strong&gt;swapped lat/lng&lt;/strong&gt;. It produces a map that opens, looks plausible, and is wrong by thousands of kilometres. Your customers see it, assume the code is broken, and leave. Validate coordinates with a range check before generating: latitude −90 to 90, longitude −180 to 180.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. A size table to work from
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Placement&lt;/th&gt;
&lt;th&gt;Printed width&lt;/th&gt;
&lt;th&gt;Target payload&lt;/th&gt;
&lt;th&gt;Max version&lt;/th&gt;
&lt;th&gt;Module pitch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Counter sticker / table tent&lt;/td&gt;
&lt;td&gt;40–60 mm&lt;/td&gt;
&lt;td&gt;under 60 chars&lt;/td&gt;
&lt;td&gt;4 (33×33)&lt;/td&gt;
&lt;td&gt;at least 1.2 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Window decal (read outside)&lt;/td&gt;
&lt;td&gt;120–150 mm&lt;/td&gt;
&lt;td&gt;under 80 chars&lt;/td&gt;
&lt;td&gt;6 (41×41)&lt;/td&gt;
&lt;td&gt;at least 2.5 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Menu card, tabletop stand&lt;/td&gt;
&lt;td&gt;40–50 mm&lt;/td&gt;
&lt;td&gt;42 chars or less&lt;/td&gt;
&lt;td&gt;3 (29×29)&lt;/td&gt;
&lt;td&gt;at least 1.3 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poster / street-facing sign&lt;/td&gt;
&lt;td&gt;250–300 mm&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;10 or less&lt;/td&gt;
&lt;td&gt;at least 5 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vehicle, moving target&lt;/td&gt;
&lt;td&gt;300–400 mm&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;8 or less&lt;/td&gt;
&lt;td&gt;at least 9 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What this adds up to
&lt;/h2&gt;

&lt;p&gt;Almost every location QR code failure I've looked at reduces to one of four causes, none of which is the encoder's fault:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The payload was the wrong &lt;strong&gt;type&lt;/strong&gt; (&lt;code&gt;geo:&lt;/code&gt; on iOS, or a text address instead of coordinates).&lt;/li&gt;
&lt;li&gt;The payload was too &lt;strong&gt;long&lt;/strong&gt;, so the code was too dense for its print size.&lt;/li&gt;
&lt;li&gt;The print violated &lt;strong&gt;quiet zone, contrast, or flatness&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Nobody &lt;strong&gt;tested at the right distance, in the right light, on the right phone&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fix those and the code works — not "works when I try it", but works for a stranger in a hurry holding a bag.&lt;/p&gt;

&lt;p&gt;I build a small set of browser-side QR tools, and this one came out of exactly the failures above: enter an address or a pair of coordinates, get a vector PNG/SVG back, no account, no tracking, no redirect service in the middle. If it's useful, it's here: &lt;a href="https://qrcraftapp.com/tools/location-qr-generator" rel="noopener noreferrer"&gt;Location QR Code Generator&lt;/a&gt;. The payload logic in section 6 is the whole tool, so you can also just build your own — that's the more durable option.&lt;/p&gt;

</description>
      <category>qrcode</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>design</category>
    </item>
    <item>
      <title>Free QR Code Generator for Email, URL, Text — No Signup Required</title>
      <dc:creator>xiaopeng</dc:creator>
      <pubDate>Sun, 01 Nov 2026 13:42:29 +0000</pubDate>
      <link>https://dev.to/buildwithsalt/free-qr-code-generator-for-email-url-text-no-signup-requiredtools-webdev-qrcode-productivity-4n8k</link>
      <guid>https://dev.to/buildwithsalt/free-qr-code-generator-for-email-url-text-no-signup-requiredtools-webdev-qrcode-productivity-4n8k</guid>
      <description>&lt;p&gt;Looking for a simple, free QR code generator without ads or login?&lt;br&gt;
I built &lt;a href="https://qrcraftapp.com" rel="noopener noreferrer"&gt;qrcraftapp.com&lt;/a&gt; to make QR codes fast.&lt;/p&gt;

&lt;p&gt;You can create QR codes for:&lt;br&gt;
✅ Website links / URL&lt;br&gt;
✅ Plain text notes&lt;br&gt;
✅ Email QR (pre-filled recipient, subject &amp;amp; message)&lt;br&gt;
More QR types will be added later.&lt;/p&gt;

&lt;p&gt;All QR codes are generated locally on your browser.&lt;br&gt;
Your data never gets uploaded to servers. No registration, no hidden fees.&lt;/p&gt;

&lt;p&gt;👉 Try it here: &lt;a href="https://qrcraftapp.com" rel="noopener noreferrer"&gt;https://qrcraftapp.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s a lightweight tool for marketers, small business owners and developers.&lt;br&gt;
If you need quick QR codes for flyers, business cards or testing, this works great.&lt;/p&gt;

&lt;p&gt;Feedback is welcome!&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>tools</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>4 free password tools — all in your browser</title>
      <dc:creator>xiaopeng</dc:creator>
      <pubDate>Mon, 12 Oct 2026 09:18:53 +0000</pubDate>
      <link>https://dev.to/buildwithsalt/-4-free-password-tools-all-in-your-browser-30jl</link>
      <guid>https://dev.to/buildwithsalt/-4-free-password-tools-all-in-your-browser-30jl</guid>
      <description>&lt;p&gt;A few weeks ago I wrote about &lt;a href="https://dev.to/buildwithsalt/i-built-a-password-generator-that-never-sends-your-data-anywhere-heres-the-crypto-behind-it-54b1"&gt;the crypto behind my password generator&lt;/a&gt; — rejection sampling, a Fisher-Yates shuffle driven by the Web Crypto API, and honest entropy math.&lt;/p&gt;

&lt;p&gt;That post became a full tool site: &lt;a href="https://mypasswordgen.com" rel="noopener noreferrer"&gt;SecurePass Gen&lt;/a&gt;. This week I shipped 4 new tools on it. Every one of them runs 100% client-side — no account, no server, no data leaving your device. Here's the thinking behind each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Memorable Password Generator
&lt;/h2&gt;

&lt;p&gt;The industry advice "just use a random string like xK29!fQz" fails one test: humans. People write gibberish on sticky notes or reset it every month.&lt;/p&gt;

&lt;p&gt;A passphrase solves this differently. A tool like the &lt;a href="https://mypasswordgen.com/memorable-password-generator" rel="noopener noreferrer"&gt;Memorable Password Generator&lt;/a&gt; picks 4–5 random words from a wordlist and joins them: Brave-Tiger-Storm-42.&lt;/p&gt;

&lt;p&gt;The math: 5 words drawn from a 1,000-word list gives you about 50 bits of entropy — comparable to a 9-character random password, but something your brain can actually hold at 3 AM.&lt;/p&gt;

&lt;p&gt;The critical detail: the words must be picked by a secure random generator, not by you. Humans are terrible at randomness — we gravitate to pet names and birthdays, which live in every cracking dictionary.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. 8-Character Password Generator
&lt;/h2&gt;

&lt;p&gt;Some systems still cap passwords at exactly 8 characters — legacy portals, router admin panels, IoT setup screens. When a cap is enforced, variety is all you have left.&lt;/p&gt;

&lt;p&gt;I put the honest math on the &lt;a href="https://mypasswordgen.com/8-character-password-generator" rel="noopener noreferrer"&gt;8-character generator&lt;/a&gt; page. Eight characters, three levels of variety:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lowercase only: about 200 billion combinations — cracked in seconds.&lt;/li&gt;
&lt;li&gt;Letters + digits: about 200 trillion — cracked in hours.&lt;/li&gt;
&lt;li&gt;Letters + digits + symbols: about 7 quadrillion — years on average.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same 8 characters. The difference between "seconds" and "years" is purely character variety. And if the site allows more than 8 — take them, every extra character multiplies the search space for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Password Strength Checker
&lt;/h2&gt;

&lt;p&gt;Here's a thing that still bugs me: most online "password checkers" ask you to paste your real password into a stranger's web form. Some of them send it to their backend "for analysis."&lt;/p&gt;

&lt;p&gt;So the &lt;a href="https://mypasswordgen.com/password-strength-checker" rel="noopener noreferrer"&gt;Password Strength Checker&lt;/a&gt; computes everything in your browser — entropy in bits, estimated crack time, and pattern warnings (dictionary words, repeats, sequences, letters-only). Open your network tab while typing: there is no request to intercept, because there is no backend.&lt;/p&gt;

&lt;p&gt;My suggestion for how to use it safely: &lt;strong&gt;test a pattern, not your real password.&lt;/strong&gt; Try a password shaped like yours, see what the checker flags, then apply the lesson.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. WiFi Password Ideas
&lt;/h2&gt;

&lt;p&gt;Your WiFi password has a unique job: it gets spoken aloud across the room and typed on a guest's phone. It needs to be strong &lt;em&gt;and&lt;/em&gt; sayable — a rare combination.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://mypasswordgen.com/wifi-password-ideas" rel="noopener noreferrer"&gt;WiFi Password Ideas&lt;/a&gt; page covers patterns like Copper-Maple-Rocket-71 (four random words plus digits), WPA2/WPA3 minimums, why WPS should be off, and why your guest network should have a different password from your main one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern across all four
&lt;/h2&gt;

&lt;p&gt;One design rule drove everything: &lt;strong&gt;if a feature would need a server, it doesn't ship.&lt;/strong&gt; That constraint killed every dark pattern before it could exist — no password ever touches a network. No analytics on keystrokes. Nothing to leak, because there's nothing stored.&lt;/p&gt;

&lt;p&gt;All four tools are live at &lt;a href="https://mypasswordgen.com" rel="noopener noreferrer"&gt;mypasswordgen.com&lt;/a&gt; — free, no signup, and they keep working once the page loads.&lt;/p&gt;

&lt;p&gt;What would you add next? I'm collecting ideas for tool #5.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>sideprojects</category>
      <category>beginners</category>
    </item>
    <item>
      <title>4 free password tools — all in your browser</title>
      <dc:creator>xiaopeng</dc:creator>
      <pubDate>Mon, 12 Oct 2026 08:44:34 +0000</pubDate>
      <link>https://dev.to/buildwithsalt/4-free-password-tools-all-in-your-browser-3hoj</link>
      <guid>https://dev.to/buildwithsalt/4-free-password-tools-all-in-your-browser-3hoj</guid>
      <description></description>
      <category>webdev</category>
      <category>security</category>
      <category>sideprojects</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I built a password generator that never sends your data anywhere — here's the crypto behind it</title>
      <dc:creator>xiaopeng</dc:creator>
      <pubDate>Fri, 11 Sep 2026 08:18:04 +0000</pubDate>
      <link>https://dev.to/buildwithsalt/i-built-a-password-generator-that-never-sends-your-data-anywhere-heres-the-crypto-behind-it-54b1</link>
      <guid>https://dev.to/buildwithsalt/i-built-a-password-generator-that-never-sends-your-data-anywhere-heres-the-crypto-behind-it-54b1</guid>
      <description>&lt;p&gt;&lt;strong&gt;Try it here → &lt;a href="https://mypasswordgen.com" rel="noopener noreferrer"&gt;https://mypasswordgen.com&lt;/a&gt;&lt;/strong&gt; (no signup, works offline once loaded)&lt;/p&gt;

&lt;p&gt;Every "free password generator" website I've used had the same quiet problem: I have no idea what happens to the password after it's generated. Some sites send it to their server. Some embed analytics everywhere. And almost all of them ask you to &lt;em&gt;trust&lt;/em&gt; them, without showing any proof.&lt;/p&gt;

&lt;p&gt;So as a side project, I built &lt;a href="https://mypasswordgen.com" rel="noopener noreferrer"&gt;SecurePass Gen&lt;/a&gt; — a password generator where every single line of logic runs in your browser. Open DevTools, watch the Network tab: you'll see zero requests when you click "Generate". The password never leaves your device, because there's no code on a server to send it to.&lt;/p&gt;

&lt;p&gt;In this post I want to share the three cryptographic details that were more interesting than I expected.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. &lt;code&gt;Math.random()&lt;/code&gt; is not random enough — and fixing that isn't as simple as &lt;code&gt;crypto.getRandomValues()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The classic mistake in password generators is using &lt;code&gt;Math.random()&lt;/code&gt;. It's a PRNG seeded with something predictable — it's fine for games, disastrous for security. The browser's answer is the Web Crypto API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// cryptographically secure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But here's the trap most tutorials skip: &lt;strong&gt;modulo bias&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You need a random integer in a range like &lt;code&gt;[0, 26)&lt;/code&gt; for letters. The naive approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// ❌ biased&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A byte has 256 possible values. &lt;code&gt;256 % 26 = 22&lt;/code&gt;, so the first 22 letters are slightly more likely to appear than the last 4. Each character individually is only &lt;em&gt;slightly&lt;/em&gt; biased — but across a 16-character password, the bias compounds and shrinks your real entropy.&lt;/p&gt;

&lt;p&gt;The fix is rejection sampling — discard values that would skew the distribution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;secureRandomInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// largest multiple of max in [0, 256)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// reject and retry → uniform distribution&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every character in the pool is exactly equally likely. Boring detail, real security impact.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. "At least one of each character type" — without revealing &lt;em&gt;where&lt;/em&gt; they are
&lt;/h2&gt;

&lt;p&gt;Users expect that checking "uppercase + digits + symbols" guarantees at least one of each appears. The common implementation prepends one required character per class... which leaks structure: the first 4 characters are always one of each type. An attacker who knows your generator can exploit that.&lt;/p&gt;

&lt;p&gt;My approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick one random character from each selected class (using the unbiased RNG above)&lt;/li&gt;
&lt;li&gt;Fill the remaining slots from the full combined pool&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shuffle everything with a cryptographically secure Fisher-Yates&lt;/strong&gt; — so the required characters end up at unpredictable positions
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shuffle&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;secureRandomInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// crypto-secure, not Math.random&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One subtlety: the shuffle must also use the CSPRNG. A Fisher-Yates driven by &lt;code&gt;Math.random()&lt;/code&gt; is still a biased shuffle — I see this bug in a lot of open-source generators.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Honest entropy math beats a fake "strength meter"
&lt;/h2&gt;

&lt;p&gt;Most strength meters check "does it have a symbol? +10 points!" — theater, not math. Entropy is measurable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;entropy (bits) = length × log₂(charsetSize)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 16-character password from a 96-character pool: &lt;code&gt;16 × log₂(96) ≈ 105 bits&lt;/code&gt;. At a pessimistic 10 billion guesses/second (offline attack on a fast hash), that's ~10²¹ years.&lt;/p&gt;

&lt;p&gt;But raw entropy lies about &lt;em&gt;human&lt;/em&gt; passwords. &lt;code&gt;P@ssw0rd123!&lt;/code&gt; has decent theoretical entropy and is still garbage — it's in every cracking wordlist. So on top of the entropy calculation I apply penalties for known-bad patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commonPatterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;123456&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;qwerty&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abc123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;letmein&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;welcome&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iloveyou&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;monkey&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dragon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="c1"&gt;// pattern match: -30 bits; triple repeats: -10; sequences (abcd/1234): -10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generator also offers a passphrase mode (XKCD #936 style — &lt;code&gt;Brave-Tiger-Storm-42&lt;/code&gt;) for people who need to actually &lt;em&gt;type&lt;/em&gt; their password.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A real strength checker should ship a compressed common-password list and do actual dictionary lookups, not substring matching&lt;/li&gt;
&lt;li&gt;I'd like to add a Web Worker so generation can never block the UI on very long lengths&lt;/li&gt;
&lt;li&gt;Maybe an offline PWA version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to poke at it: &lt;strong&gt;&lt;a href="https://mypasswordgen.com" rel="noopener noreferrer"&gt;mypasswordgen.com&lt;/a&gt;&lt;/strong&gt; — it's free, no account, no tracking, and everything is computed locally.&lt;/p&gt;

&lt;p&gt;Feedback welcome — especially from anyone who spots a weakness in the crypto approach. That's the whole point of writing this up.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>security</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
