<?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: Tea-sip</title>
    <description>The latest articles on DEV Community by Tea-sip (@yinyingring).</description>
    <link>https://dev.to/yinyingring</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%2F4046204%2F99ddba08-256f-4204-96a3-1c0b5d5a5ee2.jpg</url>
      <title>DEV Community: Tea-sip</title>
      <link>https://dev.to/yinyingring</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yinyingring"/>
    <language>en</language>
    <item>
      <title>Engineer-to-Engineer: Building a Typing Test That Doesn't Lie to You</title>
      <dc:creator>Tea-sip</dc:creator>
      <pubDate>Fri, 14 Aug 2026 22:05:37 +0000</pubDate>
      <link>https://dev.to/lizely/engineer-to-engineer-building-a-typing-test-that-doesnt-lie-to-you-5822</link>
      <guid>https://dev.to/lizely/engineer-to-engineer-building-a-typing-test-that-doesnt-lie-to-you-5822</guid>
      <description>&lt;p&gt;If you've ever shipped a "typing speed" feature into a developer tool, an educational dashboard, or a hiring pipeline, you've probably noticed that the simplest-looking metric in computing — words per minute — is also one of the easiest to game and the hardest to defend in a code review. The original article in this series pushes past the 80 WPM plateau. This one is for the people building the measurement itself: how do you write a typing test that survives scrutiny from an accessibility lead, a data engineer, and a skeptical senior dev all at the same time?&lt;/p&gt;

&lt;p&gt;We'll walk through the constraints, the formulas, the edge cases that bite in production, and a checklist you can paste into a PR description. Where the underlying mechanics of WPM calculation matter, I'll point to a thorough walkthrough at &lt;a href="https://www.lizecheng.net/productivity/guides/how-does-a-typing-test-work-the-wpm-formula-explained/" rel="noopener noreferrer"&gt;Lizely's WPM formula guide&lt;/a&gt; so you don't have to reinvent the theory from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two Meanings of "Word" You Need to Settle Before Anything Else
&lt;/h2&gt;

&lt;p&gt;A typing test fundamentally answers "how many characters did a user commit, and over what interval?" Everything else is convention. Before you write a single line of measurement code, decide which of these two definitions your product will use, because they produce noticeably different numbers for the same user:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The classic 5-character word.&lt;/strong&gt; Divide total characters typed (including a trailing space) by 5. This is the convention most office-suite benchmarks report, and it's the one most "casual" testers expose to end users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The linguistically grounded word.&lt;/strong&gt; Count whitespace-delimited tokens that are actually present in the source text. This is closer to how a linguist or a reading researcher would count.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You cannot ship both silently. Either pick one and document it, or expose the formula in a tooltip and let the user see the math. The 5-character convention has a long history in typing research (see &lt;a href="https://en.wikipedia.org/wiki/Words_per_minute" rel="noopener noreferrer"&gt;Wikipedia's coverage of the words-per-minute metric&lt;/a&gt;), and it's still the right default for general-purpose typing products because it normalizes across languages and punctuation density.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Time Window Is Where Most Bugs Hide
&lt;/h2&gt;

&lt;p&gt;A naive implementation measures &lt;code&gt;Date.now()&lt;/code&gt; from the first keypress to the last keypress and divides. That's wrong in three ways that show up in real bug reports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Idle gaps inflate the denominator unfairly.&lt;/strong&gt; If a user pauses for 12 seconds to think, their WPM collapses even though their burst speed was fine. The common fix is to stop the clock after, say, 3 seconds of no input, or to compute WPM over the active interval and surface "active time" alongside "elapsed time."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold-start padding distorts short runs.&lt;/strong&gt; The first keypress almost always takes longer than the average character. For runs under 10 seconds, either run a warm-up prompt (most competitive testers do this) or display a disclaimer that sub-15-second runs are indicative, not definitive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backspace handling changes the meaning of the score.&lt;/strong&gt; Does correcting a typo count against the user? Industry convention is "yes for accuracy, no for raw speed" — you compute raw CPM over all characters committed, and accuracy as &lt;code&gt;correct / total_attempted&lt;/code&gt;. Don't conflate the two into a single score, or your accuracy and speed will appear to trade off against each other when they actually don't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you instrument the active-time vs. elapsed-time split, you'll also catch a class of "are they really typing?" bots: if elapsed time exceeds active time by a large factor, you're probably looking at a paste event, not keystrokes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Measure vs. What the User Sees
&lt;/h2&gt;

&lt;p&gt;A reasonable internal model has three numbers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Quantity&lt;/th&gt;
&lt;th&gt;Formula&lt;/th&gt;
&lt;th&gt;Surface to user?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw CPM&lt;/td&gt;
&lt;td&gt;&lt;code&gt;total_keys / active_minutes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sometimes, as "raw"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Net WPM&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(correct_chars / 5) / active_minutes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes, primary metric&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accuracy&lt;/td&gt;
&lt;td&gt;&lt;code&gt;correct_chars / total_chars&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes, secondary metric&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trap is letting "gross WPM" leak into the UI. Gross WPM rewards sloppy typing because it includes errors in the numerator; once a user notices that fixing typos lowers their score, they stop fixing typos, and the product has taught them the wrong thing. Always display the corrected (net) WPM.&lt;/p&gt;

&lt;p&gt;For keystroke handling, the event you'll listen for is the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event" rel="noopener noreferrer"&gt;&lt;code&gt;keydown&lt;/code&gt;&lt;/a&gt; event on a focusable element. Capture &lt;code&gt;event.key&lt;/code&gt;, normalize for layout differences (&lt;code&gt;Shift&lt;/code&gt; should not count as a character, modifier-only keys should be ignored), and decide your policy on IME composition events up front. If your test supports non-Latin scripts, you'll need to read from the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event" rel="noopener noreferrer"&gt;&lt;code&gt;input&lt;/code&gt;&lt;/a&gt; event instead, because &lt;code&gt;keydown&lt;/code&gt; fires per Latin key press and double-counts for CJK input methods.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking a Corpus Without Accidentally Building a Bias Engine
&lt;/h2&gt;

&lt;p&gt;Corpus selection is the silent politics of a typing test. Two questions to answer in your design doc:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dictionary vs. prose.&lt;/strong&gt; A common-words dictionary produces higher and more consistent scores, but it trains only short, common tokens. Prose produces lower scores and more variance, but it surfaces real bigram weaknesses (more on bigrams below). Most serious tools offer both; pick a default and make the alternative one click away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain.&lt;/strong&gt; Programming-language tokens (&lt;code&gt;function&lt;/code&gt;, &lt;code&gt;return&lt;/code&gt;, &lt;code&gt;const&lt;/code&gt;) have dramatically different character distributions than English prose. If your user base is developers, a code-flavored corpus is more honest. But also consider keyboard layout: the same text typed on QWERTY vs. Dvorak vs. Colemak will produce wildly different WPM numbers, so don't let a "global leaderboard" mix layouts unless you've normalized for that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's an accessibility angle here too. For users on screen readers or alternative input devices, the "competition" framing is actively hostile. At minimum, your test should allow pausing, should not auto-fail on long pauses, and should expose a "practice mode" that reports per-character latency rather than a single summary number.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigram Heatmap: The Single Most Useful Diagnostic You'll Add
&lt;/h2&gt;

&lt;p&gt;Once the headline metric works, the highest-leverage feature you can ship next is a per-bigram latency map. A bigram is just a two-character sequence; there are ~676 of them in English lowercase plus space. Plotting mean latency per bigram reveals exactly where a user hesitates, and it's much more actionable than "your WPM is 52."&lt;/p&gt;

&lt;p&gt;The implementation is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;For each completed session, group keystrokes into pairs (&lt;code&gt;th&lt;/code&gt;, &lt;code&gt;he&lt;/code&gt;, &lt;code&gt;e&lt;/code&gt;, &lt;code&gt;s&lt;/code&gt;, ...).&lt;/li&gt;
&lt;li&gt;For each pair, record the inter-key interval in milliseconds.&lt;/li&gt;
&lt;li&gt;Render a grid where rows and columns are characters; cell color encodes mean latency.&lt;/li&gt;
&lt;li&gt;Sort the slowest bigrams to the top of a sidebar.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once a user sees that their &lt;code&gt;ed&lt;/code&gt;, &lt;code&gt;th&lt;/code&gt;, and &lt;code&gt;tion&lt;/code&gt; transitions are slow, they can drill those specifically, and your product is now a training tool, not just a measurement tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shipping Checklist for a v1 Typing Test
&lt;/h2&gt;

&lt;p&gt;Use this list verbatim in your PR template:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Time source is &lt;code&gt;performance.now()&lt;/code&gt;, not &lt;code&gt;Date.now()&lt;/code&gt;, to avoid clock-skew artifacts.&lt;/li&gt;
&lt;li&gt;WPM formula is documented in the UI tooltip and matches the code, character for character.&lt;/li&gt;
&lt;li&gt;Accuracy is reported separately from speed, and the formula is in the tooltip.&lt;/li&gt;
&lt;li&gt;Backspace behavior is explicit: errors counted in accuracy, ignored in raw WPM.&lt;/li&gt;
&lt;li&gt;The clock stops after a configurable idle threshold (3 seconds is a sensible default).&lt;/li&gt;
&lt;li&gt;Sessions under a minimum duration (15 seconds) display a confidence note.&lt;/li&gt;
&lt;li&gt;The corpus file is versioned and the version is shown in results.&lt;/li&gt;
&lt;li&gt;Layout is recorded (auto-detected when possible) and shown on the leaderboard.&lt;/li&gt;
&lt;li&gt;Pause/resume is supported and does not reset progress.&lt;/li&gt;
&lt;li&gt;The event handler correctly ignores modifier keys and handles IME composition.&lt;/li&gt;
&lt;li&gt;Paste is detected (&lt;code&gt;event.inputType === 'insertFromPaste'&lt;/code&gt;) and either rejected or flagged.&lt;/li&gt;
&lt;li&gt;Results are exportable as JSON for users who want to track their own history.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If all twelve boxes are checked, your test will survive a code review from someone who actually knows the domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What's a reasonable default for the idle-timeout threshold?
&lt;/h3&gt;

&lt;p&gt;Three seconds is the consensus in most consumer-facing typing tools. Shorter than that and thinking pauses get punished; longer and the user gets ambiguous results after walking away. Make it configurable in your advanced settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I count capitalized words differently from lowercase ones?
&lt;/h3&gt;

&lt;p&gt;No, if you're using the 5-character-word convention. Capitalization adds one keystroke (Shift + letter) but only when starting a sentence or proper noun, so it doesn't materially shift the average. If you're using token-based counting, capitalization is irrelevant because the token is the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I prevent paste-based cheating in a public leaderboard?
&lt;/h3&gt;

&lt;p&gt;Detect &lt;code&gt;inputType === 'insertFromPaste'&lt;/code&gt; on the &lt;code&gt;input&lt;/code&gt; event and disqualify the run. Be transparent about this in your rules. Also display the active-vs-elapsed time ratio; humans rarely sustain above a 1.2x ratio, while pasted runs typically show near-perfect 1.0x because they were applied in a single event.&lt;/p&gt;

&lt;h3&gt;
  
  
  My users complain that their scores dropped after I "fixed" the formula. What do I do?
&lt;/h3&gt;

&lt;p&gt;This is almost always because the old formula was gross WPM and the new one is net WPM, or because you tightened the idle timeout. Don't roll back. Instead, ship a "score history" feature so users can see their own trend line, and announce the change clearly. Honest measurement always costs a few leaderboard positions in the short term, and that's the right trade-off.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and reviewed for technical accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>lizely</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>aibotwrotethis</category>
    </item>
    <item>
      <title>Reviewing Pull Requests Without Losing Your Mind: A Text-Diff Workflow for Code Reviewers</title>
      <dc:creator>Tea-sip</dc:creator>
      <pubDate>Thu, 13 Aug 2026 19:02:28 +0000</pubDate>
      <link>https://dev.to/lizely/reviewing-pull-requests-without-losing-your-mind-a-text-diff-workflow-for-code-reviewers-18nm</link>
      <guid>https://dev.to/lizely/reviewing-pull-requests-without-losing-your-mind-a-text-diff-workflow-for-code-reviewers-18nm</guid>
      <description>&lt;p&gt;Code review fails in predictable ways. The reviewer skims, the author feels blindsided, and a missing comma in a SQL migration ships to staging. Most of those failures are not about skill — they are about &lt;em&gt;what&lt;/em&gt; the reviewer actually sees. Diff tools shape what is visible, what is hidden, and what the human eye can reasonably scan in fifteen minutes between meetings.&lt;/p&gt;

&lt;p&gt;This article is for the reviewer on the receiving end. It treats the text comparison not as a "diff" feature but as a &lt;em&gt;review surface&lt;/em&gt; with its own rules, edge cases, and pitfalls. The goal: a repeatable, defensible way to read a change so you catch semantically meaningful edits and ignore cosmetic noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Diffs Are Harder to Read Than They Look
&lt;/h2&gt;

&lt;p&gt;The Myers diff algorithm, the algorithm behind nearly every textual comparison tool in modern version control, is one of those pieces of engineering that quietly shapes daily life. It traces back to Eugene Myers's 1986 paper and is now baked into Git, Mercurial, and most standalone comparison utilities. The algorithm is correct, but correctness is not the same as clarity.&lt;/p&gt;

&lt;p&gt;A diff is a &lt;em&gt;projection&lt;/em&gt;. You are looking at the difference between two strings, but the strings represent intent, and intent lives in a layer the algorithm cannot see. A renamed function may produce a noisy hunk that hides a real bug. A reindent can grow a hunk by twenty lines without changing a single instruction. A trailing-whitespace cleanup right before a security-sensitive block can bury the security-sensitive block six lines below the visible change.&lt;/p&gt;

&lt;p&gt;Reviewers compensate by changing the &lt;em&gt;surface&lt;/em&gt;. Side-by-side viewing, collapsed unchanged regions, syntax highlighting, and consistent hunk anchoring all reframe the same algorithm output. The practices below are about choosing that frame deliberately.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Configuration That Pays Off Immediately
&lt;/h2&gt;

&lt;p&gt;Before walking through a pull request, set the comparison up so it matches the way you actually read. Three settings matter more than any plugin collection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whitespace normalization on display, strict on commit.&lt;/strong&gt; &lt;code&gt;git diff&lt;/code&gt; exposes &lt;code&gt;--ignore-cr-at-eol&lt;/code&gt;, &lt;code&gt;--ignore-trailing-cr&lt;/code&gt;, &lt;code&gt;--ignore-space-change&lt;/code&gt;, and &lt;code&gt;--ignore-all-space&lt;/code&gt; flags. In a browser-based viewer, you usually toggle the equivalent settings inline. The right pattern is: ignore &lt;em&gt;trailing-space differences&lt;/em&gt; and &lt;em&gt;end-of-line differences&lt;/em&gt; in the visual layer, but reject them in code review discussions if the file format forbids them. For example, a Markdown file rendered on the web can tolerate a stray &lt;code&gt;\r&lt;/code&gt;, but a shell script being executed on Linux cannot (&lt;a href="https://en.wikipedia.org/wiki/Newline" rel="noopener noreferrer"&gt;POSIX defines text files as sequences of lines terminated by newline&lt;/a&gt;, not carriage return + newline).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Word-level granularity for prose, line-level for code.&lt;/strong&gt; Algorithms can compare two files character by character, word by word, or line by line. Word-level is overwhelming for source files; line-level is the right default there. For copy edits, config files with long values, or translation strings, word-level reveals more. Some viewers default to one or the other — pick deliberately, do not inherit the default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anchor hunks to function or section headers.&lt;/strong&gt; A unified diff shows &lt;code&gt;@@ -120,8 +120,8 @@ optional context line&lt;/code&gt;. The optional context line is whatever happens to live nearby. In a large file, that optional context tells you almost nothing. Good viewers anchor the hunk on a label — the nearest function signature, the heading above a paragraph. If your tool does not do this automatically, scan up from the hunk header to find one before you start reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Walkthrough: Reviewing a 400-Line Backend Change
&lt;/h2&gt;

&lt;p&gt;Suppose a teammate opens a pull request touching a single file, around 400 lines, that blends three concerns: a SQL migration, a Go service layer, and a few JSON serialization helpers. You have fifteen minutes. Here is how to consume it without missing the load-bearing edits.&lt;/p&gt;

&lt;p&gt;Read the file path and the file's role first. A &lt;code&gt;migrations/0007_add_user_role.sql&lt;/code&gt; file is destructive in a different way than &lt;code&gt;internal/service/user.go&lt;/code&gt;. Treat them as different review surfaces. The SQL migration has a small surface area; read it line by line, and pay close attention to the order of statements. Some databases, including PostgreSQL, do not allow all forms of &lt;code&gt;ALTER TABLE&lt;/code&gt; inside a transaction wrapping backfill, depending on the version — confirm any backfill strategy against the version deployed in staging (&lt;a href="https://www.postgresql.org/docs/current/sql-altertable.html" rel="noopener noreferrer"&gt;PostgreSQL transactional DDL notes are documented at length on the official wiki&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Now look at the Go service layer. Scan the diff hunks in the order they appear, but read only the &lt;em&gt;new&lt;/em&gt; lines and the &lt;em&gt;removed&lt;/em&gt; lines until you have the structure. The unchanged context is for orientation, not for re-reading. As you go, mentally tag each hunk with one of three labels: &lt;em&gt;contract change&lt;/em&gt; (renames, signature changes), &lt;em&gt;behavior change&lt;/em&gt; (new branch, new state transition), or &lt;em&gt;cosmetic&lt;/em&gt; (renames of locals, comment edits). The contract and behavior tags are what you write comments about. Cosmetic edits get a thumbs-up, not an action.&lt;/p&gt;

&lt;p&gt;For the JSON helpers, the question is whether the wire format moved. Renaming a struct field is fine in isolation; renaming it without bumping the version and updating the producer breaks every downstream consumer in production. This is the moment where a side-by-side comparison earns its keep. Two panes, one anchored on the "before," one on the "after," with whitespace ignored — you can see in five seconds whether the field rename is paired with the right serialization call. For a deeper walkthrough on tuning this kind of browser-based comparison workflow, &lt;a href="https://www.lizecheng.net/dev/guides/compare-text-differences-in-vs-code-with-a-browser-diff-checker/" rel="noopener noreferrer"&gt;the Lizely guide on comparing text differences covers the configuration knobs in detail&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge Cases That Trip Up Even Senior Reviewers
&lt;/h2&gt;

&lt;p&gt;A few cases deserve explicit attention because they consistently hide defects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reformatting commits before logical commits.&lt;/strong&gt; If the diff includes a whole-file rename (a change touching almost every line because of a formatter), the hunks become unreadable. Ask the author to split the change before review. &lt;a href="https://www.conventionalcommits.org/en/v1.0.0/" rel="noopener noreferrer"&gt;The conventional commits specification&lt;/a&gt; does not require this, but a clean separation of "style:" and "feat:" commits in a single pull request is what makes a diff reviewable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cherry-picks that drop hunks.&lt;/strong&gt; A reviewer sees a patch that looks safe but was produced by cherry-picking across branches. Context lines can disagree between the two parents, which means the hunk applies cleanly but the semantics drift. The Myers algorithm reports a clean apply; the human reviewer has to know whether to ask.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Binary files reported as "text."&lt;/strong&gt; A &lt;code&gt;.sql&lt;/code&gt; file shipped with &lt;code&gt;\uFEFF&lt;/code&gt; (a BOM, byte order mark) at the top will compare as text but render oddly in some terminals (&lt;a href="https://en.wikipedia.org/wiki/Byte_order_mark" rel="noopener noreferrer"&gt;Unicode's BOM is defined in UAX #15 and surrounding areas&lt;/a&gt;). If the diff shows weird leading characters, that is the cause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three-way merges masquerading as two-way diffs.&lt;/strong&gt; A pull request updated to resolve conflicts may show only the final patch. The conflict markers themselves are gone, but the logic they resolved is what you need to see. Ask for &lt;code&gt;git log --merge&lt;/code&gt; output if anything in the diff looks suspiciously consistent across two unrelated areas.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Checklist You Can Keep Open During Review
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Set whitespace rules for the file type before opening the diff.&lt;/li&gt;
&lt;li&gt;Tag each hunk as contract, behavior, or cosmetic while reading.&lt;/li&gt;
&lt;li&gt;For SQL or schema files, re-read in source order ignoring the diff layout.&lt;/li&gt;
&lt;li&gt;Cross-check every rename against every callsite the tool shows.&lt;/li&gt;
&lt;li&gt;Confirm that any binary or BOM-prefixed files are intentional.&lt;/li&gt;
&lt;li&gt;If the patch touched merge-resolved code, request the merge's history slice.&lt;/li&gt;
&lt;li&gt;Before approving, run the test command locally and read the failing-test diff if any.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Trade-offs Worth Naming
&lt;/h2&gt;

&lt;p&gt;Browser-based comparison has limits. Large files (multi-megabyte logs, generated code) usually lag in a tab-based viewer and feel snappier in a native tool. Cross-file comparison — for example, "did this constant move?" — is harder in any single-file viewer. And you lose the integration with comment threads and CI status that a code-host-native diff page provides. The right answer in production is usually &lt;em&gt;both&lt;/em&gt;: use the host's diff for the conversation, and a side-by-side external viewer for the careful read of the largest, riskiest file in the change.&lt;/p&gt;

&lt;p&gt;No single setting, plugin, or algorithm will catch everything. The leverage comes from applying the same six-step checklist to every change, so the review surface is consistent enough that your pattern recognition actually fires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should I review a diff in a browser or in an IDE?
&lt;/h3&gt;

&lt;p&gt;Use the host's diff for the conversation and quick orientation, and an external side-by-side viewer for the largest, riskiest file. Browser tools are nearly always more responsive for cross-file comparison and stay out of the way of your editor.&lt;/p&gt;

&lt;h3&gt;
  
  
  What whitespace setting should I default to?
&lt;/h3&gt;

&lt;p&gt;Ignore trailing whitespace and end-of-line differences for display, but never for shell scripts, build files, or any file executed by a POSIX tool. Confirm against the project's editorconfig or equivalent before tightening the rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I review a reformatting commit that turned every line red?
&lt;/h3&gt;

&lt;p&gt;Do not review it as a single diff. Ask the author to split it: one commit for the formatter, one commit for the actual change. A reformat that hides a logic edit is a defect waiting to land.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the Myers algorithm and why does it matter to me?
&lt;/h3&gt;

&lt;p&gt;It is the standard approach used to compute the shortest edit script between two strings, which is what nearly every diff tool displays. Knowing it exists is useful, but knowing its limits matters more: it finds minimum edits, not minimum &lt;em&gt;intent&lt;/em&gt;, and that gap is exactly where review judgment has to step in.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and reviewed for technical accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>lizely</category>
      <category>dev</category>
      <category>webdev</category>
      <category>aibotwrotethis</category>
    </item>
    <item>
      <title>Stress-Testing Visual States in Frontend Apps With Random Color Sweeps</title>
      <dc:creator>Tea-sip</dc:creator>
      <pubDate>Wed, 12 Aug 2026 22:03:39 +0000</pubDate>
      <link>https://dev.to/lizely/stress-testing-visual-states-in-frontend-apps-with-random-color-sweeps-ed0</link>
      <guid>https://dev.to/lizely/stress-testing-visual-states-in-frontend-apps-with-random-color-sweeps-ed0</guid>
      <description>&lt;p&gt;When you build a frontend, you spend most of your time on the "happy path" — the layout where the data loads, the user is logged in, the empty state is friendly, and the error is a soft amber pill. But every UI surface also has to survive bad data, weird data, hostile data, and no data. One trick I lean on more than I'd like to admit: blast random colors through the component tree and look at what breaks.&lt;/p&gt;

&lt;p&gt;This is not about design polish. It is a debugging and QA pattern. Random color generators turn into a cheap, repeatable visual fuzz tester for state-handling code. Below is the workflow I use, the rules I hold the team to, and the trade-offs you'll hit once you try to ship this.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Color Sweep Actually Exercises
&lt;/h2&gt;

&lt;p&gt;If you swap every text color, background, border, and divider for a value pulled from a random generator, you are not just "making it ugly." You are forcing the component to render under inputs it was never hand-tuned for. That surfaces four classes of bugs fast:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Contrast regressions.&lt;/strong&gt; Hardcoded hex codes like &lt;code&gt;#cccccc&lt;/code&gt; on &lt;code&gt;#ffffff&lt;/code&gt; look fine in code review and look broken on screen under real luminance. A sweep catches the ones that slipped through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stale state leaking between renders.&lt;/strong&gt; If a component caches the "last color used" in a closure and you re-render with a different key, you'll see ghost colors or stuck backgrounds. Random sweeps make this obvious in two seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Theme provider blind spots.&lt;/strong&gt; Components that bypass the theme and reach for raw values show up immediately because their colors stay constant while everything around them changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS specificity wars.&lt;/strong&gt; A random inline color fights with a class-based rule, and the winner tells you which side of the cascade is actually in charge.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you've ever shipped a UI where "it looked fine on my machine" — this is the cure.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Reproducible Sweep Workflow
&lt;/h2&gt;

&lt;p&gt;Don't ad-hoc this. Random inputs are only useful if you can reproduce a failure. Here is the workflow I keep in a &lt;code&gt;scripts/color-sweep.ts&lt;/code&gt; and run in CI for visual diffs.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Seed the PRNG so the run is deterministic. &lt;code&gt;Math.random()&lt;/code&gt; is fine for a one-off, useless for a regression test.&lt;/li&gt;
&lt;li&gt;Pick a target node. Usually the root, or each top-level route shell.&lt;/li&gt;
&lt;li&gt;Walk the rendered tree. For every element with text, a background, a border, or an SVG fill, replace the value with a fresh random color.&lt;/li&gt;
&lt;li&gt;Capture a screenshot per route. Keep the seed in the filename.&lt;/li&gt;
&lt;li&gt;Store the seed in the test report. If a screenshot fails review, that seed reproduces it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The MDN guide on the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Performance_API" rel="noopener noreferrer"&gt;Web Performance APIs&lt;/a&gt; is the right landing page for any tooling that needs frame timing while you capture; you do not want the sweep itself to mask rendering stalls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Holding the Team to Rules
&lt;/h2&gt;

&lt;p&gt;A color sweep is a hammer; without rules it produces a mess of screenshots nobody trusts. The rules I enforce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scope first.&lt;/strong&gt; The sweep is opt-in per route. You do not run it against billing or auth screens; the data is real and the colors may persist in logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Names, not hex.&lt;/strong&gt; Every randomized color must come from a typed enum (&lt;code&gt;text&lt;/code&gt;, &lt;code&gt;bg&lt;/code&gt;, &lt;code&gt;border&lt;/code&gt;, &lt;code&gt;accent&lt;/code&gt;). No "let me just inject a string."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two runs minimum.&lt;/strong&gt; One with a fixed seed for diff, one with a different seed to confirm the bug is not a single lucky value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One bug per PR.&lt;/strong&gt; A sweep is exploratory. If you find three regressions in one run, split them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The contrast rule is the one that matters most. WCAG lays out the math for &lt;a href="https://www.w3.org/TR/WCAG21/#contrast-minimum" rel="noopener noreferrer"&gt;relative luminance and contrast ratio&lt;/a&gt; in a stable, citable form, and every "looks fine" debate ends at that document.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trade-offs Nobody Mentions
&lt;/h2&gt;

&lt;p&gt;This is where engineers lose patience. Honest list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot diffs grow.&lt;/strong&gt; A random sweep produces N images that are not stable across PRNG library upgrades. Pin the seed library, and treat the screenshot suite like a binary artifact — re-baseline deliberately, not by accident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance overhead.&lt;/strong&gt; Walking a large DOM and patching styles per node is real work. Profile once, then gate the sweep behind a flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;False confidence.&lt;/strong&gt; A green sweep does not mean the UI is correct. It means the UI tolerates arbitrary colors. The next bug class — semantic correctness — needs fixtures, not fuzz.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reviewer fatigue.&lt;/strong&gt; Five hundred random screenshots is not "exhaustively tested." It is a pile. Curate. Keep ten representative seeds, not one thousand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I have watched two teams adopt this pattern well and one team adopt it badly. The difference was almost always whether someone owned the seed corpus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrating With Existing Visual Tests
&lt;/h2&gt;

&lt;p&gt;You probably already have a visual regression runner — Percy, Chromatic, Playwright's &lt;code&gt;toHaveScreenshot&lt;/code&gt;, or a homegrown pixel-diff job. The color sweep is a &lt;em&gt;complement&lt;/em&gt;, not a replacement. Wire it like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the normal visual suite against the real theme. This catches "the new button is 2px off."&lt;/li&gt;
&lt;li&gt;Run the sweep suite against the same routes. This catches "the loading skeleton uses a hardcoded white that nobody noticed."&lt;/li&gt;
&lt;li&gt;Compare the two pass/fail sets. Bugs that appear only in the sweep pass/fail are the unique catch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keep the sweep suite on a separate CI lane with a longer timeout. Don't make every PR wait for it. Nightly is fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a Random Sweep Is the Wrong Tool
&lt;/h2&gt;

&lt;p&gt;A few honest cases where you should skip this entirely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The component is a chart with a semantic palette (red = down, green = up). Random colors destroy meaning. Use a palette fixture instead.&lt;/li&gt;
&lt;li&gt;The screen is text-heavy and the bug class is information density. Color won't help.&lt;/li&gt;
&lt;li&gt;You are debugging a single user's environment. A sweep tells you nothing about one machine.&lt;/li&gt;
&lt;li&gt;The renderer is canvas/WebGL. DOM walking is irrelevant; you need a pixel-level approach instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're staring at a bug report that says "the toast disappears when I do X," a color sweep is not the first move. A color sweep is for the bugs that only show up &lt;em&gt;because&lt;/em&gt; the state is varied.&lt;/p&gt;

&lt;p&gt;For a deeper walkthrough of generator formats and how to wire deterministic output into a script, the Lizely guide on &lt;a href="https://www.lizecheng.net/color/guides/generate-random-colors-in-any-format-with-one-click/" rel="noopener noreferrer"&gt;generating random colors in any format with one click&lt;/a&gt; is the closest fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How many seeds do I need before the sweep is "enough"?
&lt;/h3&gt;

&lt;p&gt;Ten is a practical floor for a small app; thirty is a reasonable ceiling for a medium one. Beyond thirty, you're mostly paying reviewer cost. The exact number depends on how many visually distinct states your app has — login vs. logged-out vs. error vs. empty are four, and each deserves its own seeds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I run this against a production build?
&lt;/h3&gt;

&lt;p&gt;I would not. The sweep mutates live DOM and may persist in error reports, analytics payloads, and screen recordings. Run it against a staging build with telemetry stripped, or against Storybook with a flag.&lt;/p&gt;

&lt;h3&gt;
  
  
  Won't this just find contrast bugs my linter already catches?
&lt;/h3&gt;

&lt;p&gt;Linters catch hardcoded values and obvious violations of a token. They do not catch a theme provider being bypassed in a single component, or a stale value bleeding between renders. The sweep is for the bugs the linter cannot see — runtime state-handling, not static rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should the random colors be perceptually uniform?
&lt;/h3&gt;

&lt;p&gt;Yes for sweep runs meant to find contrast regressions, because a uniform sampler across RGB leaves huge gaps in the luminance space. For most debugging sweeps, plain HSL rotation is fine and faster to reason about. The choice matters less than the discipline of keeping the seed reproducible.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and reviewed for technical accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>lizely</category>
      <category>color</category>
      <category>webdev</category>
      <category>aibotwrotethis</category>
    </item>
    <item>
      <title>Stop Reinventing the Wheel: A QA Pattern for Date Math in User-Facing Code</title>
      <dc:creator>Tea-sip</dc:creator>
      <pubDate>Tue, 11 Aug 2026 22:04:31 +0000</pubDate>
      <link>https://dev.to/lizely/stop-reinventing-the-wheel-a-qa-pattern-for-date-math-in-user-facing-code-4kb7</link>
      <guid>https://dev.to/lizely/stop-reinventing-the-wheel-a-qa-pattern-for-date-math-in-user-facing-code-4kb7</guid>
      <description>&lt;p&gt;Every backend that touches a date of birth ends up reinventing the same arithmetic. I've watched it happen in three codebases this year alone: a fintech KYC service, a healthcare intake portal, and a small HR tool for a startup. The bug shape is almost always identical — somebody computes age by subtracting birth year from current year, the QA suite passes because it only tests inputs born in 1970 and 2000, and then someone born on February 29 walks in and gets flagged as underage. Or worse, a leap-year-spanning eligibility window quietly lets the wrong person through.&lt;/p&gt;

&lt;p&gt;This article is a checklist for engineers who inherit one of these systems and need to harden it before the next incident. It assumes you've already decided you need to compute a duration between two dates correctly, and your job is to figure out how to test that computation so you stop shipping regressions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two Questions You Actually Need to Answer
&lt;/h2&gt;

&lt;p&gt;Before writing a single assertion, ask the business which of these they care about. The implementation is different for each, and conflating them is the source of most date-math bugs.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;How many full years has the person been alive?&lt;/strong&gt; This is the classic "age in years" question — what a doctor's office, a voting registration form, or an age-gated content filter wants. The answer changes only on the birthday.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Has a duration threshold been crossed?&lt;/strong&gt; This is the eligibility question — "has this user been enrolled for at least 90 days," "was this contract signed more than 30 days ago," "did the trial expire." The answer is a boolean with a specific reference date.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The age-in-years case is subtler because of the birthday boundary, and it's where most teams stumble. The duration-threshold case is just subtraction with a comparison, but the comparison must use the right granularity — comparing a millisecond timestamp to a day count is a famous source of off-by-one-day bugs when daylight saving time shifts occur. The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date" rel="noopener noreferrer"&gt;Date and Time Arithmetic section of MDN's Date reference&lt;/a&gt; is worth a read if you're implementing in JavaScript, and the &lt;a href="https://docs.python.org/3/library/datetime.html" rel="noopener noreferrer"&gt;datetime topic in the Python docs&lt;/a&gt; covers the equivalent pitfall with &lt;code&gt;timedelta&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Reference Test Matrix You Should Commit to Your Repo
&lt;/h2&gt;

&lt;p&gt;Most date-math codebases have maybe two test cases: "born today" and "born 30 years ago on the same day." That's not a test suite, that's a smoke test. For age-in-years logic, commit a fixture-driven test that covers these rows at minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Born exactly today — must return 0.&lt;/li&gt;
&lt;li&gt;Born one calendar day before today — must return 0, not 1.&lt;/li&gt;
&lt;li&gt;Born exactly one year ago to the day — must return 1.&lt;/li&gt;
&lt;li&gt;Born one year and one day ago — must return 1.&lt;/li&gt;
&lt;li&gt;Born on Feb 29 in a leap year, queried on Feb 28 of a non-leap year — must return the elapsed years, not treat the missing day as "not yet had a birthday this year."&lt;/li&gt;
&lt;li&gt;Born on Feb 29 in a leap year, queried on March 1 of a non-leap year — must return the elapsed years (birthday observed March 1).&lt;/li&gt;
&lt;li&gt;Born on Dec 31, queried on Jan 1 the next year — must return 0, not 1.&lt;/li&gt;
&lt;li&gt;Born on Jan 1, queried on Dec 31 of the previous year — must return 0.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each row, store the birthday and the "as of" date as explicit ISO-8601 strings in your fixture file, never as &lt;code&gt;new Date()&lt;/code&gt; literals in the test body, because the latter will rot the moment someone runs the suite in a different timezone. The &lt;a href="https://en.wikipedia.org/wiki/ISO_8601" rel="noopener noreferrer"&gt;ISO 8601 entry on Wikipedia&lt;/a&gt; is a reasonable anchor for the team on why the format is preferable to anything locale-driven.&lt;/p&gt;

&lt;h2&gt;
  
  
  Injecting the Clock: The Single Most Useful Refactor
&lt;/h2&gt;

&lt;p&gt;Most age-computation bugs are not in the math. They are in the fact that the math depends on &lt;code&gt;now&lt;/code&gt;, and &lt;code&gt;now&lt;/code&gt; is hidden inside the function being tested. You can't unit-test what you can't control.&lt;/p&gt;

&lt;p&gt;Refactor every age computation so the reference date is a parameter. Default it to &lt;code&gt;now()&lt;/code&gt; only at the public boundary (controller, handler, scheduled job). Internally, everything takes &lt;code&gt;(birth_date, reference_date)&lt;/code&gt; explicitly. This sounds pedantic until you see how much it simplifies the test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;age_in_years&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;birth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;years&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;year&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;birth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;year&lt;/span&gt;
    &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;birth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;birth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;years&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;years&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the test isn't testing "the current moment," it's testing "given these two inputs, does this function produce this output." That single refactor usually eliminates a third of the team's date-bug backlog.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the Rule Has a Time-of-Day Component
&lt;/h2&gt;

&lt;p&gt;This is the case that catches mature codebases. The product says "users under 18 cannot purchase," but the actual stored rule is "users whose 18th birthday has not yet passed at the moment of purchase." If your backend stores the birthday with a time component (and many do, because the database column is a &lt;code&gt;TIMESTAMP&lt;/code&gt; rather than a &lt;code&gt;DATE&lt;/code&gt;), you have to decide whether &lt;code&gt;2007-03-15 23:59:00&lt;/code&gt; plus 18 years crosses the threshold on &lt;code&gt;2025-03-15 14:00:00&lt;/code&gt; or &lt;code&gt;2025-03-16&lt;/code&gt;. The answer depends on whether your business rule operates in calendar days or in elapsed time.&lt;/p&gt;

&lt;p&gt;I've seen teams burn a week on this. The pragmatic answer is: strip the time component at the boundary, then use calendar-day comparison. Almost no product actually cares about the hour; they care about the date. If your column is &lt;code&gt;TIMESTAMP&lt;/code&gt;, cast to &lt;code&gt;DATE&lt;/code&gt; on read and the question evaporates.&lt;/p&gt;

&lt;p&gt;For teams operating across jurisdictions, remember that the rules of &lt;em&gt;whose&lt;/em&gt; calendar day matters can vary. A patient admitted at 23:50 in Tokyo is in a different calendar day than a physician reviewing the chart at 00:10 in Berlin. If your system crosses timezones, decide explicitly which anchor you use. The &lt;a href="https://en.wikipedia.org/wiki/Tz_database" rel="noopener noreferrer"&gt;IANA Time Zone Database overview&lt;/a&gt; is worth skimming so the team at least agrees on terminology before they disagree on policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Pragmatic Workflow for the Next Time This Comes Up
&lt;/h2&gt;

&lt;p&gt;When a ticket lands asking for an age check or an eligibility-window check, walk through this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Classify the question&lt;/strong&gt; as age-in-years or duration-threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Locate the existing helper.&lt;/strong&gt; Most codebases already have one, hidden in a &lt;code&gt;utils/&lt;/code&gt; folder, written by someone who left. Read it before writing a new one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the fixture file&lt;/strong&gt; with at least the eight rows above, plus any domain-specific rows (e.g. "free trial expires exactly on day 30 at midnight UTC").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inject the clock.&lt;/strong&gt; If the helper takes &lt;code&gt;now&lt;/code&gt; implicitly, refactor before adding tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the rule check&lt;/strong&gt; as a separate function that consumes the helper's output. Age computation and age policy are different concerns and should live in different files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the suite in CI with a fixed system clock&lt;/strong&gt; for at least one test case, using something like &lt;code&gt;freezegun&lt;/code&gt; or &lt;code&gt;timecop&lt;/code&gt;, to prove the clock is actually injectable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document the boundary behavior&lt;/strong&gt; in the helper's docstring, especially what happens on Feb 29 and on month-end edges.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your team has standardized on Excel for any of these calculations — which happens more often than engineers like to admit, especially in HR and operations — pair the engineering helper with a spreadsheet that uses the same logic. The &lt;a href="https://www.lizecheng.net/calculator/guides/how-to-calculate-age-between-two-dates-in-excel-instant-online-tool/" rel="noopener noreferrer"&gt;in-depth walkthrough on calculating age between two dates&lt;/a&gt; is a useful reference when the business needs a non-engineer to verify the rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What's the difference between using a &lt;code&gt;TIMESTAMP&lt;/code&gt; and a &lt;code&gt;DATE&lt;/code&gt; column for birthdays?
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;DATE&lt;/code&gt; column stores only the calendar day. A &lt;code&gt;TIMESTAMP&lt;/code&gt; stores a moment in time, down to microseconds, and is interpreted in the session timezone. For age logic, you almost always want &lt;code&gt;DATE&lt;/code&gt;, because the rule operates on calendar days and the time component is either ignored or, worse, used inconsistently across queries. If you must store &lt;code&gt;TIMESTAMP&lt;/code&gt; for legacy reasons, cast to &lt;code&gt;DATE&lt;/code&gt; at the application boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should leap-year babies be treated specially?
&lt;/h3&gt;

&lt;p&gt;No special-casing is needed if you use the standard "subtract years, decrement if birthday hasn't occurred yet" pattern. The function handles Feb 29 correctly because it compares month-and-day tuples; when the reference date is in a non-leap year and is Feb 28 or March 1, the comparison still works. What you must avoid is &lt;code&gt;birthday.addYears(n)&lt;/code&gt; style libraries that throw when the target year is not a leap year — those libraries need a policy on whether to roll forward to Feb 28 or March 1, and not all of them document it.&lt;/p&gt;

&lt;h3&gt;
  
  
  My tests pass locally but fail on the CI server once a month. What's happening?
&lt;/h3&gt;

&lt;p&gt;The CI server's clock and yours are fine. The problem is almost certainly that one of your tests uses &lt;code&gt;new Date()&lt;/code&gt; directly and runs near midnight UTC. The local machine and the CI runner cross the day boundary at different wall-clock times. This is the single most common reason date tests are flaky, and it's exactly why fixture-driven tests with explicit ISO-8601 strings are non-negotiable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there an existing standard I should be citing in my code review comments?
&lt;/h3&gt;

&lt;p&gt;Yes. If you're working in Python, &lt;a href="https://peps.python.org/pep-0008/" rel="noopener noreferrer"&gt;PEP 8&lt;/a&gt; covers naming but not date logic; the relevant authority is the &lt;a href="https://docs.python.org/3/library/datetime.html" rel="noopener noreferrer"&gt;&lt;code&gt;datetime&lt;/code&gt; module documentation&lt;/a&gt; and, for cross-system interchange, &lt;a href="https://en.wikipedia.org/wiki/ISO_8601" rel="noopener noreferrer"&gt;ISO 8601&lt;/a&gt;. Citing those in a code review makes the date-of-birth column review land faster than re-explaining the Feb 29 case each time.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and reviewed for technical accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>lizely</category>
      <category>calculator</category>
      <category>webdev</category>
      <category>aibotwrotethis</category>
    </item>
    <item>
      <title>Choosing How to Merge PDF Files: A Practical Engineer's Decision Guide</title>
      <dc:creator>Tea-sip</dc:creator>
      <pubDate>Mon, 10 Aug 2026 19:08:48 +0000</pubDate>
      <link>https://dev.to/lizely/choosing-how-to-merge-pdf-files-a-practical-engineers-decision-guide-1j1h</link>
      <guid>https://dev.to/lizely/choosing-how-to-merge-pdf-files-a-practical-engineers-decision-guide-1j1h</guid>
      <description>&lt;p&gt;Combining separate PDF documents into a single file sounds like a trivial operation until you hit your second real-world use case. A one-off personal scan is one thing; a recurring team process with naming conventions, retention rules, and audit trails is another. This guide walks through the three most common ways engineers and technical leads actually get this done — doing it manually, scripting it in code, or routing the job through a purpose-built web utility — and lays out the trade-offs honestly so you can pick the right path per situation.&lt;/p&gt;

&lt;p&gt;The advice here applies whether you are stitching together scanned invoices, assembling an evidence packet for a compliance review, or shipping a multi-appendix deliverable to a client. The decision criteria are the same: how repeatable the task is, how sensitive the contents are, and how much control you need over the resulting file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Approaches in Plain Terms
&lt;/h2&gt;

&lt;p&gt;Before going deeper, here is the short version of what each path looks like in practice.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Manual, on your desktop.&lt;/strong&gt; Open each file, copy pages, paste into a new document, save. Most operating systems now ship a stock previewer that supports page reordering and export.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Programmatic, in code.&lt;/strong&gt; Use a library in Python, Node, Go, or your language of choice. A short script reads the input list, walks each page, and writes a new file. Pair it with a spreadsheet or CSV for the input manifest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web-based utility.&lt;/strong&gt; Upload the source files, drag to reorder, click combine, download the result. No install, no code, no environment setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these is universally best. The rest of the article explains when each one earns its place.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the Manual Route Actually Makes Sense
&lt;/h2&gt;

&lt;p&gt;For a single merge involving two or three files you have already inspected, manual work is hard to beat. There is no setup overhead, no script to debug, and you can eyeball the result before saving. This is also the safest choice when the documents contain information you would rather not transmit to any external service — the operation never leaves your laptop.&lt;/p&gt;

&lt;p&gt;The cost shows up the third or fourth time you do the same thing. If you find yourself repeating the same sequence of clicks every week, you are paying a hidden tax. That tax is the right signal to graduate to a script or a shared utility.&lt;/p&gt;

&lt;p&gt;A quick mental test before going manual:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the file count below five?&lt;/li&gt;
&lt;li&gt;Are you confident in the page order without writing it down?&lt;/li&gt;
&lt;li&gt;Is this a one-time task, or at most a quarterly one?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If all three are yes, stay manual. If any is no, keep reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a Script Earns Its Keep
&lt;/h2&gt;

&lt;p&gt;Once the task repeats, or once you have more than a handful of files to combine, a short script pays for itself. Engineers usually reach for &lt;code&gt;pypdf&lt;/code&gt; in Python, &lt;code&gt;pdf-lib&lt;/code&gt; in Node, or &lt;code&gt;unipdf&lt;/code&gt; in Go. A minimal example using &lt;code&gt;pypdf&lt;/code&gt; reads like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pypdf&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PdfWriter&lt;/span&gt;

&lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PdfWriter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cover.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;appendix.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;out.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real value is not the ten lines of code — it is the input manifest sitting next to it. A spreadsheet or CSV that lists, in order, every file path and its role (&lt;code&gt;cover&lt;/code&gt;, &lt;code&gt;toc&lt;/code&gt;, &lt;code&gt;chapter-1&lt;/code&gt;, &lt;code&gt;appendix-a&lt;/code&gt;) turns the operation from a manual chore into a reproducible pipeline. Now any teammate can run the same script, and the order is reviewable in version control.&lt;/p&gt;

&lt;p&gt;This path shines when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need a deterministic, auditable order.&lt;/li&gt;
&lt;li&gt;The merge is part of a larger build or release flow.&lt;/li&gt;
&lt;li&gt;You want to attach metadata (title, author, subject) programmatically.&lt;/li&gt;
&lt;li&gt;Multiple people contribute files and the manifest is the contract.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest downsides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You own the dependency, the Python or Node version, and the runner.&lt;/li&gt;
&lt;li&gt;Output validation is on you — you need to spot-check page count, embedded fonts, and any password-protected inputs.&lt;/li&gt;
&lt;li&gt;Scanned documents that started as images need OCR handled elsewhere before this step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to dig deeper into how a small build script can sit inside a larger document pipeline, the &lt;a href="https://www.lizecheng.net/pdf/guides/combine-pdf-vs-pdf-portfolio-which-one-should-you-use/" rel="noopener noreferrer"&gt;combine PDF vs PDF Portfolio decision guide&lt;/a&gt; walks through the underlying formats and when a multi-file container is actually the better answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a Web Utility Is the Right Tool
&lt;/h2&gt;

&lt;p&gt;There is a class of job that does not justify a script and does not fit cleanly into the manual column: an ad-hoc merge performed by someone who is not a developer, using files that are not sensitive enough to require an air-gapped workflow. Think of a freelancer packaging deliverables for a client, a sales rep assembling a proposal, or a student joining lecture notes.&lt;/p&gt;

&lt;p&gt;A purpose-built online tool is the lowest-friction option for these cases. Drag the files in, reorder by drag-and-drop, click combine, download. Done in under a minute, with no install and no learning curve. For repeated personal use, bookmark the page and the next time around it takes seconds.&lt;/p&gt;

&lt;p&gt;The trade-offs to weigh honestly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy posture.&lt;/strong&gt; Files travel to a third-party server. For non-sensitive material this is fine; for medical, legal, or financial documents, it is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File size and count limits.&lt;/strong&gt; Free tiers often cap total size or the number of source files. Check before you start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No audit trail.&lt;/strong&gt; You cannot easily show a colleague which version of which input produced today's output. If that matters, a script with a manifest wins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A reasonable rule of thumb: if the inputs are public or already shared widely, and the output does not need to be reproducible from a record, a web utility is the pragmatic choice. If either of those conditions is false, fall back to manual or scripted.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Decision Checklist You Can Reuse
&lt;/h2&gt;

&lt;p&gt;Run through this list the next time you are about to combine documents. Pick the path that satisfies the most items.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Are the documents sensitive (PII, financial, legal, internal-only)? If yes, prefer local execution — manual or scripted.&lt;/li&gt;
&lt;li&gt;Will this exact sequence of files need to be re-merged later? If yes, script it with a manifest.&lt;/li&gt;
&lt;li&gt;Does the order matter and is it non-obvious from filenames? If yes, anything that lets you visually reorder before saving is worth it.&lt;/li&gt;
&lt;li&gt;Are there more than five input files? Manual starts to break down here; lean toward scripted or web utility.&lt;/li&gt;
&lt;li&gt;Does the output need to carry metadata such as title, author, or page numbers? Scripted gives the most control.&lt;/li&gt;
&lt;li&gt;Is the person doing the merge a developer? If no, the choice is between manual and web utility.&lt;/li&gt;
&lt;li&gt;Is the result subject to an audit or compliance review? If yes, scripted with a logged manifest is the safest answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If at least four of those land on "scripted," write the script. If at least four land on "manual" or "web utility," do not over-engineer it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation You Should Never Skip
&lt;/h2&gt;

&lt;p&gt;Whichever path you choose, the last step is the same: open the result and confirm three things before you send or archive it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Page count matches expectations.&lt;/strong&gt; A missing page from a bad read or a truncated download is the most common silent failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedded text is selectable, not rasterized.&lt;/strong&gt; If you need to search or copy text later, this matters. Tools like &lt;code&gt;pdftotext&lt;/code&gt; from the Poppler suite let you verify quickly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fonts render correctly.&lt;/strong&gt; A missing font on a different machine can show up as boxes or substituted glyphs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For deeper reading on the format itself, the &lt;a href="https://en.wikipedia.org/wiki/PDF" rel="noopener noreferrer"&gt;PDF specification on Wikipedia&lt;/a&gt; is a reliable entry point, and the &lt;a href="https://developer.adobe.com/document-services/docs/overview/" rel="noopener noreferrer"&gt;Adobe PDF 1.7 reference&lt;/a&gt; covers the structural details if you are working at the byte level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I know whether the merge actually preserved the original quality?
&lt;/h3&gt;

&lt;p&gt;Page count and visual inspection are the first checks. For a stronger guarantee, extract text from a few representative pages using &lt;code&gt;pdftotext&lt;/code&gt; and compare it to what you expect from the source. If the text comes out garbled or empty, the source was likely image-only and the merge is fine but the document was never searchable to begin with.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I merge password-protected files without removing the protection?
&lt;/h3&gt;

&lt;p&gt;Yes. Both &lt;code&gt;pypdf&lt;/code&gt; and &lt;code&gt;pdf-lib&lt;/code&gt; accept an empty-password option for owner-locked files that simply restrict editing. For files with a real user password, you have to provide it during the read step. In every case, the merged output should have its own protection applied if the source had any.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between merging and creating a PDF portfolio?
&lt;/h3&gt;

&lt;p&gt;A merge produces a single document with continuous page numbering. A portfolio is a container that holds separate files and presents them under one cover, but each component stays independent. The right choice depends on whether the reader needs to extract one piece later or treat the whole thing as one document.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there a size limit I should worry about?
&lt;/h3&gt;

&lt;p&gt;Practically, yes. Most viewers handle files up to a few hundred megabytes without complaint, but anything past a gigabyte starts to feel slow on lower-end hardware. If your inputs regularly push the merged result above that range, consider whether a portfolio or a download bundle would serve the reader better.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and reviewed for technical accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>lizely</category>
      <category>pdf</category>
      <category>webdev</category>
      <category>aibotwrotethis</category>
    </item>
    <item>
      <title>Resizing Images for Email Campaigns: A Frontend Engineer’s Playbook</title>
      <dc:creator>Tea-sip</dc:creator>
      <pubDate>Sun, 09 Aug 2026 19:02:20 +0000</pubDate>
      <link>https://dev.to/lizely/resizing-images-for-email-campaigns-a-frontend-engineers-playbook-28m7</link>
      <guid>https://dev.to/lizely/resizing-images-for-email-campaigns-a-frontend-engineers-playbook-28m7</guid>
      <description>&lt;p&gt;Email clients are the most hostile rendering environment most web engineers will touch this year. Outlook on Windows ignores CSS background images, Gmail strips &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; blocks from forwarded messages, Apple Mail ignores &lt;code&gt;height&lt;/code&gt; on &lt;code&gt;img&lt;/code&gt; if the source has a fixed &lt;code&gt;width&lt;/code&gt; attribute, and every mobile client downsamples differently. Resize an image the wrong way and your carefully designed newsletter lands in the inbox looking like a captcha. Resize it the right way and it survives all of it.&lt;/p&gt;

&lt;p&gt;This article is the playbook I wish I’d had when I inherited a transactional email pipeline that was shipping 1.4 MB JPEGs into a user base where 60% read on mobile networks. I’ll cover the sizing decisions, the HTML constraints, and the QA loop that catches regressions before they reach production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Email Forces Different Rules Than the Web
&lt;/h2&gt;

&lt;p&gt;The web has progressive enhancement. Email has progressive degradation: every client is the worst client. The &lt;a href="https://html.spec.whatwg.org/multipage/images.html" rel="noopener noreferrer"&gt;HTML Living Standard&lt;/a&gt; treats &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; as a replaced element with intrinsic dimensions, but most email clients pre-process the markup, rewrite the DOM, and sometimes re-encode images before display. The result is that your pixel-perfect mockup in Litmus or Email on Acid is not the same pipeline your recipient sees.&lt;/p&gt;

&lt;p&gt;A few constraints shape every decision below:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Outlook (Word rendering engine) still re-proxies images through a CDN and can recompress them. Sending a noisy JPEG gives it permission to make things worse.&lt;/li&gt;
&lt;li&gt;Gmail’s web client strips images that are not &lt;code&gt;https://&lt;/code&gt;, or that have a &lt;code&gt;src&lt;/code&gt; attribute longer than ~2,000 characters. Every query string on a resizing CDN costs you headroom.&lt;/li&gt;
&lt;li&gt;Apple Mail (macOS and iOS) respects &lt;code&gt;srcset&lt;/code&gt; and &lt;code&gt;sizes&lt;/code&gt; more reliably than Gmail does, but it caches aggressively and ignores cache-busting unless you rotate the entire &lt;code&gt;cid:&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Dark mode clients (iOS Mail, Gmail iOS) may invert background colors. Transparent PNGs of UI chrome look fine; opaque JPEGs with baked-in backgrounds look like day-glo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A safe default is a single image per visual concept, sized for the most common reading viewport, with CSS that holds it together if the image fails to load. That is the opposite of how you’d ship a hero image on a marketing site.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sizing Math That Actually Matters
&lt;/h2&gt;

&lt;p&gt;I size once, in code, and reuse the numbers. Open the analytics for the last six issues of your newsletter on whatever platform you use, and pull the distribution of email client and viewport sizes. If you don’t have that, the approximate breakdown from &lt;a href="https://www.litmus.com/blog/email-client-market-share" rel="noopener noreferrer"&gt;Litmus’s annual email client market share reports&lt;/a&gt; is a reasonable starting point until you have your own data.&lt;/p&gt;

&lt;p&gt;Three buckets cover nearly every case:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Slot&lt;/th&gt;
&lt;th&gt;Logical width&lt;/th&gt;
&lt;th&gt;Pixel width @2x&lt;/th&gt;
&lt;th&gt;Pixel width @3x&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full-bleed&lt;/td&gt;
&lt;td&gt;600 px&lt;/td&gt;
&lt;td&gt;1200 px&lt;/td&gt;
&lt;td&gt;1800 px&lt;/td&gt;
&lt;td&gt;Banded hero, broken rarely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two-column&lt;/td&gt;
&lt;td&gt;280 px&lt;/td&gt;
&lt;td&gt;560 px&lt;/td&gt;
&lt;td&gt;840 px&lt;/td&gt;
&lt;td&gt;Card images, product thumbnails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inline/icon&lt;/td&gt;
&lt;td&gt;48 px&lt;/td&gt;
&lt;td&gt;96 px&lt;/td&gt;
&lt;td&gt;144 px&lt;/td&gt;
&lt;td&gt;Icons, avatars, badges&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three questions decide which bucket an asset belongs in:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What is the &lt;strong&gt;role&lt;/strong&gt; of the image — hero, product, supporting, or functional?&lt;/li&gt;
&lt;li&gt;At what &lt;strong&gt;width fraction&lt;/strong&gt; does it sit in the 600 px content column?&lt;/li&gt;
&lt;li&gt;What is the &lt;strong&gt;worst-case device pixel ratio&lt;/strong&gt; you’ll target? iOS Safari goes to 3x on iPhone Pro Max; Android Chrome on mid-range hardware hits 2x and stops.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For 2x output I keep one master, resize to 1200 px wide, and re-encode. For 3x I generate a parallel 1800 px variant. I do not send both — I pick based on the dominant client. If your list is 70%+ iOS Mail, send the 3x master and let Android downsample. If your list is mixed Gmail and Outlook, ship the 2x master, because Outlook’s re-compression already costs you roughly 15%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing a Format That Survives the Pipeline
&lt;/h2&gt;

&lt;p&gt;I default to a short decision tree rather than a rule, because the answer changes with the image type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Photographic hero or lifestyle shot&lt;/strong&gt;: JPEG, quality 78–82, &lt;code&gt;mozjpeg&lt;/code&gt; if available. Strip the EXIF; email clients don’t display it and the metadata leaks PII.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logo, line art, badges with text&lt;/strong&gt;: PNG-8 with adaptive palette, or better, an SVG. SVG support in email is uneven — Gmail and Outlook strip it — so embed SVG only when you control both clients, and fall back to a high-DPI PNG otherwise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screenshots of UI&lt;/strong&gt;: WebP is now supported in Apple Mail and Gmail iOS/Android, but Outlook does not render it. If your recipient set includes any meaningful Outlook share, stick with PNG. The extra 30% on file size is not worth a broken screenshot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Animations or motion&lt;/strong&gt;: Short looped GIF, but cap at 200 KB. Email clients do not autoplay GIFs silently above 1 MB on mobile data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Format selection is downstream of resizing, not independent of it — a 1200 px image encoded badly in any format will fail Outlook’s re-compression tests. The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types" rel="noopener noreferrer"&gt;MDN article on image file type and format guidance&lt;/a&gt; is a useful cross-check, especially the section on browser support, which overlaps about 80% with email client support.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Workflow You Can Run From a Makefile
&lt;/h2&gt;

&lt;p&gt;Below is the loop I use, stripped of the proprietary tooling. Each step is its own command, so it slots into CI.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Audit the brief.&lt;/strong&gt; Read the design spec and write down the role, target slot, and alternate text for every image in the issue. If the brief does not include alt text, ask for it. No image ships without it. Accessibility is non-negotiable; the &lt;a href="https://www.w3.org/WAI/tutorials/images/decision-tree/" rel="noopener noreferrer"&gt;W3C alt text decision tree&lt;/a&gt; is short and concrete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate masters at 3x.&lt;/strong&gt; A single &lt;code&gt;convert&lt;/code&gt; (ImageMagick) or &lt;code&gt;magick&lt;/code&gt; command resizes from the design source to 1800 px wide for full-bleed and 840 px for two-column. Lanczos resampling is fine for photos; bicubic for screenshots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encode for the target slot.&lt;/strong&gt; Run separate passes at 1200 px and 1800 px. Strip metadata with &lt;code&gt;mogrify -strip&lt;/code&gt;. Set JPEG quality 80 for full-bleed, 78 for two-column, 85 for inline icons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure.&lt;/strong&gt; Reject any output above the slot’s budget. A 1200 px JPEG of a clean lifestyle shot should not exceed 180 KB; a screenshot PNG should not exceed 220 KB. If it does, drop the width one step and re-encode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tag the file.&lt;/strong&gt; Rename to &lt;code&gt;{slot}_{role}_{2x|3x}.{ext}&lt;/code&gt; and drop into the email build directory. The naming makes it impossible for an editor to grab the wrong variant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render in a real client.&lt;/strong&gt; Pin the campaign to Litmus or Email on Acid, or open in Mail.app and Outlook desktop, and screenshot. Compare to the design spec at 100% zoom on a 1x display and on a 3x display.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  QA: The Three Screenshots That Catch 80% of Bugs
&lt;/h2&gt;

&lt;p&gt;Most email regressions are visible in three screenshots, taken every run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Outlook desktop on Windows&lt;/strong&gt;, reading pane at default zoom. Look for stretched logos (a giveaway that the source aspect ratio didn’t match the &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; attributes), and missing alt text fallbacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gmail iOS app&lt;/strong&gt; on the latest iPhone, portrait, with the message open at the top. Scroll once and confirm that the second image renders crisp, not blurred. A blurred image means the 3x variant was uploaded to a 2x slot, or vice versa.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apple Mail macOS&lt;/strong&gt; with dark mode enabled. Confirm that any UI chrome sitting on the image has enough contrast in both modes. A common failure: a light-gray button PNG on a hero image that vanishes when the background inverts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anything that survives those three screenshots will survive the long tail. Anything that breaks one of them is fixed and re-run before the issue ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs Worth Naming Out Loud
&lt;/h2&gt;

&lt;p&gt;A few decisions that come up often and that I’ve changed my mind on over time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File size versus image count.&lt;/strong&gt; Twenty sharp small images beat eight compressed posters. Gmail’s pre-processor stalls on large combined payloads more than on small individual ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;srcset&lt;/code&gt; versus a single 3x master.&lt;/strong&gt; &lt;code&gt;srcset&lt;/code&gt; is honored by Apple Mail and recent Yahoo/AOL webmail. It is ignored by older Outlooks. If your list has more than 15% Outlook desktop, ship one 2x master and accept the soft rendering on a Pro Max display. Most readers will not notice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDN resizing versus build-time resizing.&lt;/strong&gt; CDN resizing (&lt;code&gt;?w=1200&lt;/code&gt;) is convenient, but the resulting URLs are long, brittle, and stripped or shortened unpredictably by Gmail. Build-time resizing means losing flexibility but gaining predictability. For newsletters I prefer the latter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aspect ratio fixity.&lt;/strong&gt; The image always declares its &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; attributes in the HTML, so the layout reserves space even when the image fails to load. For a deeper walkthrough on avoiding stretch and quality loss during the actual resize operation, &lt;a href="https://www.lizecheng.net/image/guides/resize-image-without-stretching/" rel="noopener noreferrer"&gt;the Lizely resize guide&lt;/a&gt; covers the resampling and aspect-ratio math in detail.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should I send retina/HiDPI images to every recipient?
&lt;/h3&gt;

&lt;p&gt;Only if your list is dominated by Apple Mail iOS/macOS. For a mixed Outlook+Gmail+Apple Mail audience, a single 2x master is more reliable than a 3x variant delivered to a recipient who will downsample it anyway. Test with your own client distribution.&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s the right JPEG quality for email?
&lt;/h3&gt;

&lt;p&gt;Around 78–82 for hero and lifestyle photos, 85 for inline icons and avatars. Going higher adds bytes without visible gain in the inbox, because Outlook and Gmail both recompress during transit. Going below 70 introduces banding that is visible in flat-colored product shots.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use WebP in transactional email?
&lt;/h3&gt;

&lt;p&gt;Only if every client on your recipient list supports it. That excludes Outlook desktop and older Outlook for Mac. For mixed lists stick with JPEG/PNG; for pure Apple Mail + Gmail iOS audiences, WebP is fine and saves 25–30% on file size.&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s the single biggest mistake engineers make resizing for email?
&lt;/h3&gt;

&lt;p&gt;Fixing the rendered width but forgetting &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; attributes on the &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag. Most clients don’t compute aspect ratio from the image itself, so a missing attribute pair causes the layout to reflow, the next image to load shifted, and the entire grid to land wrong. Always declare both attributes, with the natural pixel dimensions of the variant you’re sending.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and reviewed for technical accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>lizely</category>
      <category>image</category>
      <category>webdev</category>
      <category>aibotwrotethis</category>
    </item>
    <item>
      <title>Designing a Solvability Gate for 15 Puzzle Implementations</title>
      <dc:creator>Tea-sip</dc:creator>
      <pubDate>Sat, 08 Aug 2026 20:02:54 +0000</pubDate>
      <link>https://dev.to/lizely/designing-a-solvability-gate-for-15-puzzle-implementations-2adb</link>
      <guid>https://dev.to/lizely/designing-a-solvability-gate-for-15-puzzle-implementations-2adb</guid>
      <description>&lt;p&gt;Every 15-puzzle implementation eventually hits the same quiet question: given a starting arrangement, can the player ever reach the goal? The answer is not obvious. Random shuffles fail roughly half the time, and a permissive build that lets the player play an unsolvable board wastes the user's evening. A strict gate that blocks too many starts kills engagement. Engineers building or integrating a sliding-tile game need a clear, testable rule, plus a way to debug it when their version disagrees with a reference solver.&lt;/p&gt;

&lt;p&gt;This piece walks through the parity math behind the classic sliding puzzle, the edge cases that bite production code, and a short checklist you can drop into a code review. It is written for engineers, not for casual players — if you want the gameplay walkthrough, rules, and winning tips, the &lt;a href="https://www.lizecheng.net/games/guides/how-to-play-the-15-puzzle-rules-and-winning-tips/" rel="noopener noreferrer"&gt;15 puzzle rules and winning tips guide&lt;/a&gt; covers that side of the topic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two-State Rule Most Codebases Get Wrong
&lt;/h2&gt;

&lt;p&gt;The puzzle's state space splits cleanly into two connected components. From any reachable configuration, the parity of the permutation combined with the row of the blank tile determines which component the configuration lives in. The standard formulation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Count the number of &lt;strong&gt;inversions&lt;/strong&gt; — pairs &lt;code&gt;(i, j)&lt;/code&gt; with &lt;code&gt;i &amp;lt; j&lt;/code&gt; and &lt;code&gt;value[i] &amp;gt; value[j]&lt;/code&gt;, treating the blank as value 16 and ignoring it when it occupies a tile position.&lt;/li&gt;
&lt;li&gt;Find the &lt;strong&gt;row of the blank tile&lt;/strong&gt;, counted from the bottom (row 1 is the bottom row of the 4×4 grid).&lt;/li&gt;
&lt;li&gt;The board is solvable iff either of the following holds:

&lt;ul&gt;
&lt;li&gt;The blank is on an even row from the bottom and the inversion count is &lt;strong&gt;odd&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The blank is on an odd row from the bottom and the inversion count is &lt;strong&gt;even&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the rule. Implementations get it wrong by counting inversions with the blank included, by counting the blank's row from the top, or by flipping both branches at once. Any one of those slips makes the gate accept unsolvable boards or reject solvable ones. Treat the two checks as a single boolean with a unit test on each side.&lt;/p&gt;

&lt;p&gt;A useful reference for the underlying combinatorics is the Wikipedia entry on the &lt;a href="https://en.wikipedia.org/wiki/15_puzzle" rel="noopener noreferrer"&gt;15 puzzle&lt;/a&gt;, which traces the parity argument back to the 1870s and explains why the two-component structure exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Rule Is Not Enough in Production
&lt;/h2&gt;

&lt;p&gt;A pure parity check protects you against random shuffles, but production systems accumulate state through player actions, save/restore, and import/export. Three concrete failure modes show up repeatedly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resume from a corrupt save.&lt;/strong&gt; A user closes the tab mid-shuffle, the local storage writes a half-serialized state, and on reload the board is technically legal but unreachable from the current "shuffle seed." Reject these on load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drag-and-drop moves that bypass the grid model.&lt;/strong&gt; A naive drag handler lets the user pick up a tile and release it two cells away. From the player's view it looks like two swaps; from the solver it is an illegal transition. Gate every input through the same move primitive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hint features that propose an illegal move.&lt;/strong&gt; If your hint engine runs an A* search on a state graph but you forgot to apply the parity filter when seeding the search, you can return a move that pushes the board out of the goal's component. Pre-filter the start state before any search.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix in every case is the same: have one function &lt;code&gt;isSolvable(state) -&amp;gt; boolean&lt;/code&gt;, call it on every transition that changes state, and never expose the underlying &lt;code&gt;state&lt;/code&gt; object to other modules without that wrapper.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Minimal Implementation in TypeScript
&lt;/h2&gt;

&lt;p&gt;Here is a compact, testable version. It treats the blank as 0 internally and never lets the blank leak into inversion counting.&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;type&lt;/span&gt; &lt;span class="nx"&gt;Board&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;readonly &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[])[];&lt;/span&gt; &lt;span class="c1"&gt;// 4x4, values 1..15 plus one 0&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isSolvable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;board&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Board&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;flat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;board&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flat&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;inversions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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="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;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;flat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flat&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="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&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;j&lt;/span&gt; &lt;span class="o"&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="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;flat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;j&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flat&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flat&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;flat&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;inversions&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="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Row of the blank, 1-indexed from the bottom.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blankIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;flat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blankRowFromTop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blankIndex&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&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;blankRowFromBottom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;blankRowFromTop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 1..4&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blankRowFromBottom&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&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;inversions&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;inversions&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two tests worth pinning down before shipping:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The solved board (&lt;code&gt;[1..15, 0]&lt;/code&gt;) is solvable — inversions is 0, blank is on row 1 from the bottom (odd), even inversions satisfy the branch. Returns &lt;code&gt;true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A swap of just &lt;code&gt;14&lt;/code&gt; and &lt;code&gt;15&lt;/code&gt; in the solved state is unsolvable — inversions jumps to 1, blank is still row 1, even branch rejects. Returns &lt;code&gt;false&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If those two cases pass, you have eliminated about 90% of parity bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating Shuffles That Are Always Legal
&lt;/h2&gt;

&lt;p&gt;Once the gate exists, the natural next step is a shuffle primitive that never returns &lt;code&gt;false&lt;/code&gt;. The simplest approach is rejection sampling:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shuffleBoard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Board&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="kc"&gt;true&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;flat&lt;/span&gt; &lt;span class="o"&gt;=&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="nf"&gt;range&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="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&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;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="nx"&gt;flat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;k&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;board&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;flat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;chunk&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isSolvable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;board&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;board&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;Rejection sampling is fine for small state spaces and human-scale runs, but the expected number of retries is close to 2, so the cost is negligible. If you are seeding thousands of puzzles per minute — leaderboards, daily challenges, classroom modes — consider a constructive algorithm: start from the solved state and perform a long random walk of legal moves. Any walk from the solved state is, by construction, in the same connected component. This guarantees one valid output per walk and avoids the loop entirely. The walk length matters: fewer than ~50 moves produces recognizably ordered boards; 200+ looks random to players.&lt;/p&gt;

&lt;p&gt;For a deeper look at random walk length and perceived randomness in puzzle generation, the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Typed_arrays" rel="noopener noreferrer"&gt;JavaScript reference on typed arrays&lt;/a&gt; is a useful reminder that any RNG path you pick should produce reproducible output when seeded, so daily challenges are reproducible across users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging When Your Solver Disagrees With a Reference
&lt;/h2&gt;

&lt;p&gt;A common debugging story: a tester solves the puzzle, your completion counter does not increment, and the state you serialized looks solved. Three checks, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Re-serialize and re-run &lt;code&gt;isSolvable&lt;/code&gt; on the result.&lt;/strong&gt; If it returns &lt;code&gt;false&lt;/code&gt;, your move handler produced an illegal transition. Walk back through the last move and confirm the move primitive is the only path to mutate state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare the serialized state to the canonical solved state byte-by-byte.&lt;/strong&gt; Tile-by-tile equality is the only valid completion signal. Do not compare hashes, do not compare "all tiles in correct position except blank" — the only correct solved state has the blank in the bottom-right corner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay the user's move log against a known-good solver.&lt;/strong&gt; If your solver refuses the final move, your solver is wrong; if it accepts but your counter does not fire, your completion signal is reading stale state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A reviewer-friendly way to enforce this: keep &lt;code&gt;isSolvable&lt;/code&gt;, &lt;code&gt;applyMove&lt;/code&gt;, and &lt;code&gt;isSolved&lt;/code&gt; in the same module, expose them through a single barrel, and forbid direct state mutation outside that module. A grep for &lt;code&gt;board[...]=&lt;/code&gt; outside the file should be a code review failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist for the Code Review
&lt;/h2&gt;

&lt;p&gt;Before approving a 15-puzzle change, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Every public mutation goes through a move primitive that updates the blank position and the tile permutation atomically.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;isSolvable&lt;/code&gt; is called on every external state entry point: shuffle, save restore, URL-import, and seeded challenge load.&lt;/li&gt;
&lt;li&gt;[ ] The solved-state check is strict tile equality with the blank at index 15, not a heuristic.&lt;/li&gt;
&lt;li&gt;[ ] At least one unit test pins the parity rule with the blank on an even row from the bottom and one with an odd row.&lt;/li&gt;
&lt;li&gt;[ ] The shuffle path is either rejection sampling with a bounded retry count, or a constructive random walk of length 100+.&lt;/li&gt;
&lt;li&gt;[ ] No PDF, download, or vendor-specific link appears in error messages; failures resolve to stable docs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That checklist, plus the parity function above, will catch every solvability regression I have seen in shipped builds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does the parity rule change for 3×3 or 5×5 sliding puzzles?
&lt;/h3&gt;

&lt;p&gt;Yes — the rule generalizes to any NxN board with a single blank. For odd-width grids (3×3, 5×5) the rule collapses to a single inversion parity: solvable iff the inversion count is even. For even-width grids the blank-row term appears, exactly as in the 4×4 case. The derivation is on the Wikipedia page linked above and is worth a careful read if you ship multiple sizes.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I handle boards imported from a URL or QR code?
&lt;/h3&gt;

&lt;p&gt;Treat the import as untrusted. Run &lt;code&gt;isSolvable&lt;/code&gt; on the deserialized state and refuse to load if it fails, with a clear message. Optionally offer a "shuffle to a solvable state" button so the user is not stranded. Never silently rewrite the user's input — they may be testing your gate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can the parity rule be bypassed to add "cheat" features like swapping any two tiles?
&lt;/h3&gt;

&lt;p&gt;Technically yes — any state is reachable if you let the user perform arbitrary permutations — but doing so breaks every downstream feature that assumes the two-component invariant, including hint engines, save compatibility, and leaderboard validation. If you must ship a "free play" mode, isolate it behind a flag and never let its state cross into the main mode's persistence layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the smallest number of moves guaranteed to solve any legal state?
&lt;/h3&gt;

&lt;p&gt;The worst-case optimal move count for the 15 puzzle is 80, established by exhaustive search. If your hint engine proposes more than 80 moves to a solved state, your search is misconfigured; if it proposes fewer for a legal state, your state is misconfigured. Use 80 as a sanity bound during development.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and reviewed for technical accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>lizely</category>
      <category>games</category>
      <category>webdev</category>
      <category>aibotwrotethis</category>
    </item>
    <item>
      <title>Picking the Right Video Compression Path for Web Delivery: A Practical Decision Guide</title>
      <dc:creator>Tea-sip</dc:creator>
      <pubDate>Fri, 07 Aug 2026 19:03:07 +0000</pubDate>
      <link>https://dev.to/lizely/picking-the-right-video-compression-path-for-web-delivery-a-practical-decision-guide-3fj5</link>
      <guid>https://dev.to/lizely/picking-the-right-video-compression-path-for-web-delivery-a-practical-decision-guide-3fj5</guid>
      <description>&lt;p&gt;When a web team owns video assets, the hardest part is rarely the encoding itself — it's choosing a workflow that fits the team's constraints. Files arrive from product shoots, screen recordings, partner agencies, and customer support screen captures. Each one has a different source size, codec, and deadline. The goal here is to walk through three realistic routes engineers take to shrink those files for web distribution, lay out the honest trade-offs of each, and point to a deeper reference when the team needs more background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "Just Use HandBrake" Is Often the Wrong Starting Point
&lt;/h2&gt;

&lt;p&gt;HandBrake is excellent software, but it solves the wrong problem for most web teams. It's tuned for personal archival and one-off encodes. A team shipping updates every week runs into three recurring pain points:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Per-machine inconsistency.&lt;/strong&gt; Two engineers on macOS and Windows will produce slightly different output even with identical settings, because of the underlying toolchain versions. Reviewers end up chasing visual regressions that aren't real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preset drift.&lt;/strong&gt; Someone tweaks a preset, the change isn't documented, and six months later the original spec is gone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No audit trail.&lt;/strong&gt; There's no built-in log of which file went through which profile on which date. When QA flags a regression, the answer is usually "we think this is the May preset."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a single freelancer uploading one reel, none of this matters. For a four-person product team responsible for 200 clips, it's a recurring tax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 1: The Command-Line Pipeline (FFmpeg + a Config Repo)
&lt;/h2&gt;

&lt;p&gt;This is the route most engineering-led teams eventually settle on. The shape is consistent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Git repository holds &lt;code&gt;presets/&lt;/code&gt; (JSON or shell snippets), &lt;code&gt;inputs/&lt;/code&gt; (filenames and SHA256), and &lt;code&gt;outputs/&lt;/code&gt; (file paths and target sizes).&lt;/li&gt;
&lt;li&gt;A single shell script or Makefile wraps the &lt;code&gt;ffmpeg&lt;/code&gt; invocation.&lt;/li&gt;
&lt;li&gt;CI runs the encode on every push, so the same binary produces the same artifact across machines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A minimal example for a 720p web clip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mov &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-preset&lt;/span&gt; slow &lt;span class="nt"&gt;-crf&lt;/span&gt; 21 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"scale=-2:720"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-b&lt;/span&gt;:a 128k &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-movflags&lt;/span&gt; +faststart &lt;span class="se"&gt;\&lt;/span&gt;
  output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;+faststart&lt;/code&gt; flag matters more than people realize. It relocates the &lt;code&gt;moov&lt;/code&gt; atom to the front of the file, which lets the browser begin playback before the full download completes. Without it, the entire file must load before the first frame renders, which kills the perceived performance of any video under five seconds.&lt;/p&gt;

&lt;p&gt;The trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pros:&lt;/strong&gt; Reproducible, scriptable, CI-friendly, supports any input format FFmpeg can read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; Engineers own it. Non-technical teammates can't run an encode without help. Encoding speed on commodity laptops is slow without &lt;code&gt;libx264&lt;/code&gt; tuned flags or hardware acceleration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This path fits when the team already has a deployment pipeline and treats video assets the same way they treat JavaScript bundles — built, versioned, and deployed from a single source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 2: The Spreadsheet-of-Bitrates Workflow
&lt;/h2&gt;

&lt;p&gt;Marketing-led teams frequently land here because it lets non-engineers participate. The pattern looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A spreadsheet lists every clip, its source duration, source resolution, and target distribution context (landing page hero, blog post inline, email thumbnail).&lt;/li&gt;
&lt;li&gt;Each row has a column with a target bitrate in kilobits per second, picked from a small lookup table (e.g., 1080p = 2500 kbps, 720p = 1200 kbps, 480p = 600 kbps).&lt;/li&gt;
&lt;li&gt;A formula computes the expected output size: &lt;code&gt;bitrate_kbps × duration_seconds / 8 / 1024 = megabytes&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Someone runs the encode manually using those numbers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The bitrate math is straightforward enough that it survives in a shared doc for years. The spreadsheet is the contract between the person who records the footage and the person who encodes it.&lt;/p&gt;

&lt;p&gt;The trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pros:&lt;/strong&gt; No code to maintain. Anyone on the team can update it. The size estimates are good enough to promise delivery budgets to stakeholders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; Manual execution is slow and error-prone. There's no enforcement that the actual encode matched the plan — someone can write 1200 kbps in the sheet and ship a 4000 kbps encode by mistake.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This path fits when the team produces fewer than 20 clips a month and one person is willing to own the encode queue as a recurring chore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 3: A Browser-Based Tool for One-Off Conversions
&lt;/h2&gt;

&lt;p&gt;Not every clip needs to enter a pipeline. A customer success manager recording a 30-second screen walkthrough for a support ticket doesn't want to clone a repo, push to CI, and wait for a build. They want to drop a file in, pick a target, and move on.&lt;/p&gt;

&lt;p&gt;This is where a lightweight in-browser compressor earns its place. The key engineering concern is that the conversion must actually happen client-side. If the file uploads to a server, two things go wrong: the upload itself takes longer than the encode would have, and the file leaves the user's machine, which is a non-starter for assets under NDA.&lt;/p&gt;

&lt;p&gt;The browser tool approach has gotten more practical in recent years thanks to two browser-side capabilities that have stabilized. The first is broad codec support in the &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; element, which means most inputs can be decoded without server help. The second is &lt;code&gt;WebAssembly&lt;/code&gt; builds of common codec libraries, which let tools encode entirely in the browser without a round trip. For background on the codec landscape and which formats browsers can actually decode today, the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Containers" rel="noopener noreferrer"&gt;MDN guide to media container formats&lt;/a&gt; is the right starting point. For the bitrate math that determines output size, &lt;a href="https://en.wikipedia.org/wiki/Bitrate" rel="noopener noreferrer"&gt;Wikipedia's article on bitrate&lt;/a&gt; gives the underlying formula without vendor framing.&lt;/p&gt;

&lt;p&gt;The trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pros:&lt;/strong&gt; Zero install, works on a colleague's laptop, fast for files under 200 MB, no pipeline to babysit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; Output is harder to reproduce across runs because the browser environment varies. Not appropriate as the team's only path for high-volume work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This path fits when someone needs a single file shrunk in the next five minutes and there is no engineer available.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Decision Checklist for the Next Clip
&lt;/h2&gt;

&lt;p&gt;When a new file lands in the queue, walk through this ordered list before picking a path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Will this clip be published more than once?&lt;/strong&gt; If yes, the CLI pipeline (Path 1) is the right home — reproducibility pays off the second time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the file under 200 MB and needed in the next hour?&lt;/strong&gt; A browser-based compressor is faster than waiting on CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does the team have a single owner who will encode manually every time?&lt;/strong&gt; If yes, the spreadsheet workflow (Path 2) is sustainable. If no, it isn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the asset under NDA or contains customer data?&lt;/strong&gt; Verify the chosen tool processes locally. Server-side uploads are a hard no.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Will the result play on mobile Safari?&lt;/strong&gt; Test on a real device before shipping. Encodes that look fine on a desktop Chrome can fail on iOS due to codec profile mismatches.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I'd Actually Recommend
&lt;/h2&gt;

&lt;p&gt;For most teams starting from zero, the right move is to build Path 1 once and accept that Path 3 will still happen informally. The CLI pipeline handles the 80% of clips that go to production. The browser-based tool covers the long tail of one-off requests where the pipeline is overkill.&lt;/p&gt;

&lt;p&gt;Path 2 — the spreadsheet — is a transitional state. It works, but it tends to collapse into Path 1 within a year as the volume grows. If the team is already at scale, skip it.&lt;/p&gt;

&lt;p&gt;When the team needs a deeper walkthrough on the codec choices, target sizes, and quality trade-offs that sit underneath all three paths, the Lizely guide on &lt;a href="https://www.lizecheng.net/video/guides/compress-video-files-without-losing-quality-online/" rel="noopener noreferrer"&gt;compressing video files without losing quality online&lt;/a&gt; covers the background in more depth than fits in a decision article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does client-side compression match server-side quality?
&lt;/h3&gt;

&lt;p&gt;For most web targets, yes. Browser-side encoders running through &lt;code&gt;WebAssembly&lt;/code&gt; produce files within a few percent of server-side output at the same target bitrate. The gap shows up at very high resolutions (above 4K) or when the encoder lacks hardware acceleration.&lt;/p&gt;

&lt;h3&gt;
  
  
  How small should a web clip actually be?
&lt;/h3&gt;

&lt;p&gt;A reasonable budget for a hero video is roughly 1 MB per second of playback at 1080p, and 0.5 MB per second at 720p. Anything larger and the bandwidth cost starts to dominate the design budget.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a single team use more than one path?
&lt;/h3&gt;

&lt;p&gt;Absolutely. Most teams do. The mistake is treating the browser-based tool as a replacement for the pipeline. It complements it by handling the small, ad-hoc requests that would otherwise clog the queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about format choice — MP4 vs WebM?
&lt;/h3&gt;

&lt;p&gt;MP4 with H.264 is still the safest default because every browser, TV, and phone can play it. WebM with VP9 or AV1 delivers smaller files at equivalent quality but isn't universal yet. If the audience is known to be on modern browsers, AV1 is worth testing; otherwise, MP4 is the right baseline.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and reviewed for technical accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>lizely</category>
      <category>video</category>
      <category>webdev</category>
      <category>aibotwrotethis</category>
    </item>
    <item>
      <title>QA-Testing Audio Trimming Workflows Before You Ship a Web Editor</title>
      <dc:creator>Tea-sip</dc:creator>
      <pubDate>Fri, 07 Aug 2026 03:38:15 +0000</pubDate>
      <link>https://dev.to/lizely/qa-testing-audio-trimming-workflows-before-you-ship-a-web-editor-5fio</link>
      <guid>https://dev.to/lizely/qa-testing-audio-trimming-workflows-before-you-ship-a-web-editor-5fio</guid>
      <description>&lt;p&gt;If you're building — or integrating — a browser-based audio trimmer, the question that eventually reaches your inbox isn't "does it cut audio?" The real question is: does it cut audio &lt;em&gt;correctly across the inputs we actually receive from users?&lt;/em&gt; That shift, from feature presence to behavior under fuzzy conditions, is what turns a demo into a product. This article walks through the QA matrix I use when reviewing client-side trimmers before release, with an emphasis on the silent failures that don't show up in a happy-path recording.&lt;/p&gt;

&lt;p&gt;The tool under review for most of this article is the Lizely audio cutter (&lt;a href="https://www.lizecheng.net/audio/guides/audio-cutter-and-joiner-online-trim-in-browser/" rel="noopener noreferrer"&gt;in-depth walkthrough&lt;/a&gt;), but the principles apply to any browser trimmer that decodes via &lt;code&gt;AudioContext&lt;/code&gt; or &lt;code&gt;OfflineAudioContext&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Trim" Actually Means Once You Leave the Lab
&lt;/h2&gt;

&lt;p&gt;In the lab, you upload a 44.1 kHz stereo WAV, drag two handles, click export, and verify the output. In production, users upload M4A recordings from iPhone Voice Memos, AMR files from old Android handsets, mono 8 kHz captures from cheap conference mics, and — occasionally — files renamed from &lt;code&gt;.wav&lt;/code&gt; to &lt;code&gt;.mp3&lt;/code&gt; without re-encoding. Each of those paths stresses a different layer of the pipeline.&lt;/p&gt;

&lt;p&gt;The first thing to test, before any UI work, is the decode step. Browsers expose this through the &lt;code&gt;decodeAudioData&lt;/code&gt; method on &lt;code&gt;BaseAudioContext&lt;/code&gt;, documented on &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/decodeAudioData" rel="noopener noreferrer"&gt;MDN's BaseAudioContext page&lt;/a&gt;. MDN is explicit about something engineers often miss: &lt;code&gt;decodeAudioData&lt;/code&gt; detaches the input &lt;code&gt;ArrayBuffer&lt;/code&gt;. If your trimmer holds a reference to the original buffer for "undo" and reuses it, you'll decode an empty buffer the second time around and get a silent result. That's a real defect class, not a theoretical one.&lt;/p&gt;

&lt;p&gt;The second thing to test is what happens when decoding fails. The spec says &lt;code&gt;decodeAudioData&lt;/code&gt; invokes the error callback with a &lt;code&gt;DOMException&lt;/code&gt;, but the browser-specific error messages vary. Chrome tends to surface "Decoding error" with no detail; Firefox appends the underlying codec name. Your QA suite should assert on the &lt;em&gt;callback being invoked&lt;/em&gt;, not on a particular string — otherwise you'll chase platform-specific noise forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Input Matrix I Run Before Sign-Off
&lt;/h2&gt;

&lt;p&gt;For every trimmer I review, I keep a fixed matrix of inputs. The columns change per project, but the rows are stable. Here's the version I use for a generic web trimmer targeting consumer audio:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Container/codec combinations.&lt;/strong&gt; MP3 (CBR and VBR), WAV (PCM 16-bit and 24-bit), FLAC, M4A (AAC-LC and HE-AAC), OGG Vorbis. At minimum, three of these.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sample rates.&lt;/strong&gt; 8 kHz, 16 kHz, 22.05 kHz, 44.1 kHz, 48 kHz. The 22.05 kHz case catches tools that assume CD-quality input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Channel layouts.&lt;/strong&gt; Mono, stereo, and — if you support it — 5.1. Most consumer editors don't, but you should at least fail cleanly on multi-channel input rather than silently downmixing to mono with a phase-inverted side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File sizes.&lt;/strong&gt; Under 1 MB, around 50 MB, and the "I exported my entire podcast feed by accident" case at 500+ MB. The last one is where memory budgets explode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration edge cases.&lt;/strong&gt; Files shorter than the minimum selection window (some trimmers enforce a 1-second floor), files longer than &lt;code&gt;OfflineAudioContext&lt;/code&gt; will render without chunking, and files whose total length is not an integer number of seconds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each cell in that matrix gets two assertions: the trim completes without throwing, and the output's first/last sample timestamps match the UI's reported selection within a tolerance that depends on sample rate. For 48 kHz audio, a tolerance of one sample is roughly 20 microseconds — tight enough to catch rounding bugs but loose enough not to fail on legitimate resampling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sample-Accurate Timing vs. Frame-Accurate Timing
&lt;/h2&gt;

&lt;p&gt;A subtle source of QA failures is the difference between sample-accurate trim points and frame-accurate ones. MP3 is encoded in frames of 1152 samples (for MPEG-1 Layer 3) — see the &lt;a href="https://en.wikipedia.org/wiki/MP3#Design" rel="noopener noreferrer"&gt;MPEG-1 Layer III Wikipedia article&lt;/a&gt; for the framing details. That means you cannot start an MP3 decode at an arbitrary sample index; you have to start at a frame boundary and discard the leading samples.&lt;/p&gt;

&lt;p&gt;In practice, a browser trimmer sidesteps this by decoding the whole file into an &lt;code&gt;AudioBuffer&lt;/code&gt; and then slicing the in-memory representation. The frame boundary becomes irrelevant once you're working with raw PCM. But if your architecture ever goes near the encoded bitstream — for example, to avoid decoding a 3-hour file into memory — you need to know that any "sample 4,823,104" you report to the user is meaningless until you account for the encoder's framing.&lt;/p&gt;

&lt;p&gt;The test I run for this: pick an MP3, ask the trimmer for a selection that starts at, say, sample 1000 (not a frame boundary), and verify that the &lt;em&gt;output&lt;/em&gt; starts at exactly that offset in the resulting PCM. If the tool silently snaps to frame boundaries, the user gets a clip that's a few milliseconds earlier or later than the UI shows. That's a defect worth filing, even if most users never notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory Budgets: The Constraint That Kills Browser Editors
&lt;/h2&gt;

&lt;p&gt;The most common production failure I see isn't a logic bug — it's the tab crashing because the editor tried to hold a 600 MB decoded buffer in memory. &lt;code&gt;AudioBuffer&lt;/code&gt; stores Float32 PCM, so a stereo 48 kHz file of duration &lt;em&gt;d&lt;/em&gt; seconds consumes roughly &lt;em&gt;d × 384 KB&lt;/em&gt;. A 60-minute podcast is about 1.4 GB. Most desktop browsers will allocate that, eventually, but mobile Safari will not.&lt;/p&gt;

&lt;p&gt;The QA angle here is: at what file size does your trimmer stop working, and what does it tell the user? Acceptable answers include "we reject files over X MB with a clear message" or "we chunk-decode and never hold more than Y MB at once." Unacceptable answers include the silent hang followed by the tab being killed by the OS.&lt;/p&gt;

&lt;p&gt;A practical test: upload the largest MP3 your matrix allows and watch the browser's memory profiler. If you see peak heap usage approaching the file's decoded size * 2 (input plus output), you have a problem. If it stays flat regardless of input size, you're chunking correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The OfflineAudioContext Render Trap
&lt;/h2&gt;

&lt;p&gt;If your trimmer uses &lt;code&gt;OfflineAudioContext&lt;/code&gt; to render the trimmed region — and most do, because it's the cleanest way to apply fades, gain, or format conversion — there's a render-length limit that varies by browser. Chrome has historically capped &lt;code&gt;OfflineAudioContext&lt;/code&gt; at a total render length related to the source's duration; Safari is stricter. Long files combined with fade-out tails can exceed these limits silently.&lt;/p&gt;

&lt;p&gt;The test: take a file that's near your maximum supported duration, apply a 5-second fade-out, and render. If the result is truncated, you've hit the limit. The fix is usually to chunk the render into segments and concatenate, but that's its own QA exercise — concatenation bugs are easy to introduce and hard to spot by ear.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Pre-Ship Checklist for Browser Audio Trimmers
&lt;/h2&gt;

&lt;p&gt;Before signing off on a release, I walk through this list with the engineering owner:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Confirm &lt;code&gt;decodeAudioData&lt;/code&gt; does not retain references to the source &lt;code&gt;ArrayBuffer&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Verify the error callback is wired and surfaces a user-readable message.&lt;/li&gt;
&lt;li&gt;Run the input matrix above with at least one file per row.&lt;/li&gt;
&lt;li&gt;Assert output sample timestamps match UI-reported selection within tolerance.&lt;/li&gt;
&lt;li&gt;Profile peak heap usage on the largest supported input.&lt;/li&gt;
&lt;li&gt;Test &lt;code&gt;OfflineAudioContext&lt;/code&gt; renders at maximum supported duration with fades applied.&lt;/li&gt;
&lt;li&gt;Verify behavior on mono, stereo, and multi-channel inputs (even if multi-channel is unsupported — fail loudly, don't silently downmix).&lt;/li&gt;
&lt;li&gt;Confirm the export format's metadata (artist, title, album) is either preserved or explicitly stripped, not partially copied.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I decide whether to decode in memory or chunk-stream?
&lt;/h3&gt;

&lt;p&gt;Decode in memory when files are under ~50 MB and you need random-access trimming. Chunk-stream when you support hour-long inputs or mobile users with memory-constrained devices. The cutoff depends on your target audience; for podcast editors, in-memory is fine. For music production tools, chunk-streaming is mandatory.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the right way to test sample-accurate trim points?
&lt;/h3&gt;

&lt;p&gt;Decode a known test tone (sine wave at a specific frequency), trim a region whose boundaries fall between zero crossings, and verify the output's first and last samples match the requested indices exactly. Use a small tolerance for floating-point comparison, but the trim point itself should be exact.&lt;/p&gt;

&lt;h3&gt;
  
  
  My trimmer works on Chrome but fails on Safari. Where do I start?
&lt;/h3&gt;

&lt;p&gt;Check &lt;code&gt;OfflineAudioContext&lt;/code&gt; length limits first — Safari is stricter than Chrome. Then check &lt;code&gt;decodeAudioData&lt;/code&gt; support for your codec matrix; Safari ships fewer free codecs. Finally, inspect any uses of &lt;code&gt;AudioWorklet&lt;/code&gt; or &lt;code&gt;AudioBuffer.copyFromChannel&lt;/code&gt;, which have had varying levels of support across Safari versions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I preserve or strip metadata from the trimmed output?
&lt;/h3&gt;

&lt;p&gt;Default to preserving, but be explicit. Users who trim a podcast for a clip often want the original title and artist preserved; users who trim a voice memo for privacy often want everything stripped. The right answer is to expose the choice in the export dialog, not to guess.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and reviewed for technical accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>lizely</category>
      <category>audio</category>
      <category>webdev</category>
      <category>aibotwrotethis</category>
    </item>
    <item>
      <title>Engineering a Shared Reference Clock for Distributed Teams: Rules, Drift, and Recovery Playbooks</title>
      <dc:creator>Tea-sip</dc:creator>
      <pubDate>Sun, 02 Aug 2026 20:03:59 +0000</pubDate>
      <link>https://dev.to/lizely/engineering-a-shared-reference-clock-for-distributed-teams-rules-drift-and-recovery-playbooks-18pl</link>
      <guid>https://dev.to/lizely/engineering-a-shared-reference-clock-for-distributed-teams-rules-drift-and-recovery-playbooks-18pl</guid>
      <description>&lt;p&gt;When a backend incident fires at 02:47 UTC, a payroll export runs late in São Paulo, or a deploy window opens for a Singapore office, the question "what time is it where they are?" stops being trivia and becomes a debugging primitive. Distributed teams keep three or four reference clocks side by side, often in the same browser tab. The hard part is not the display — it is agreeing on the rules underneath.&lt;/p&gt;

&lt;p&gt;This article walks through the data structures, protocol references, and operational playbooks I lean on whenever a shared wall clock has to be trustworthy across continents. It complements the practical setup guide, which you can find here: &lt;a href="https://www.lizecheng.net/productivity/guides/change-your-world-clock-in-seconds-with-this-free-tool/" rel="noopener noreferrer"&gt;Change Your World Clock in Seconds with This Free Tool&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Data Layer Behind Every Display
&lt;/h2&gt;

&lt;p&gt;A clock widget renders one number, but the inputs to that number are layered. From the ground up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A monotonic counter.&lt;/strong&gt; Most platforms expose a millisecond- or nanosecond-resolution timestamp tied to a chosen epoch. JavaScript hands you &lt;code&gt;performance.now()&lt;/code&gt; relative to navigation start, and &lt;code&gt;Date.now()&lt;/code&gt; in epoch milliseconds; both are sourced from the same system clock but answer different questions. For wall-clock semantics you almost always want the latter, cross-checked against a network time source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A UTC offset at the current instant.&lt;/strong&gt; Time zones are not static vectors. They are functions of (UTC instant, civil location, historical database version). The IANA tz database — version 2024b at the time of writing — encodes those functions, including the transitions in 2011 when Samoa skipped an entire calendar day and the multiple DST rules that have changed in regions like Morocco and Egypt. Anyone representing local time as a fixed offset is hiding this layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A formatting rule.&lt;/strong&gt; ISO 8601 gives a deterministic string for any instant, but humans still want "Mon, 14:30". The CLDR repository, mirrored by Unicode, owns the locale-data tables behind that rendering, including whether noon is rendered as 12:00 PM or 24-hour 12:00.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your "clock" is doing all three steps inline without an explicit boundary between them, the next time someone changes a DST rule upstream you will spend a day grepping the codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Four Reference Points I Trust in Production
&lt;/h2&gt;

&lt;p&gt;When I audit a system that mishandles civil time, the fix usually comes from one of four canonical sources. Pick the one that matches your trust boundary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IANA tz database&lt;/strong&gt; (&lt;code&gt;/usr/share/zoneinfo&lt;/code&gt;, mirrored at &lt;code&gt;iana.org/time-zones&lt;/code&gt;) — the source of truth for offsets and transitions. Every operating system ships it; every language runtime forwards to it indirectly. Treat any hard-coded offset list as suspect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NTP and its successors.&lt;/strong&gt; NTP version 4, documented in &lt;a href="https://www.rfc-editor.org/rfc/rfc5905.html" rel="noopener noreferrer"&gt;RFC 5905&lt;/a&gt;, is the protocol that keeps the monotonic counter honest. For sub-second accuracy without dedicated hardware, NTP can usually hold a workstation within tens of milliseconds of a stratum-1 server. Chrony, ntpd, and systemd-timesyncd all speak it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roughtime.&lt;/strong&gt; Google, Tailscale, and others published &lt;a href="https://roughtime.googlesource.com/roughtime/" rel="noopener noreferrer"&gt;Roughtime&lt;/a&gt; as a deliberately simpler, auditable alternative — signed timestamp responses without the full NTP state machine. Useful when you want a verifiable audit trail rather than tight jitter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leap-second tables.&lt;/strong&gt; The IERS publishes Bulletin C and the leap-second table referenced by POSIX &lt;code&gt;tzdata&lt;/code&gt;. Treat 2016-12-31 23:59:60 as a reminder that civil time is not always a clean bijection with atomic time; UT1 vs TAI differences occasionally matter for systems that aggregate logs across a leap boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A robust shared clock consumes from at least two of these so that a single source's bug — for instance, a tz database update that flipped Argentina's rules mid-cycle — cannot propagate silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping Civil Location to an Offset: The Five-Step Procedure
&lt;/h2&gt;

&lt;p&gt;Given an arbitrary user-entered city or IANA zone identifier, the canonical procedure for a backend service is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Normalize.&lt;/strong&gt; Lowercase, strip punctuation, fold common aliases ("NYC" → "America/New_York"). Keep the original string in logs for audit; never trust the normalized form alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolve against the tz database.&lt;/strong&gt; If the input matches an IANA identifier, you are done. If it is a free-form city, look it up against your own curated city-to-zone table — populated from &lt;code&gt;zone.tab&lt;/code&gt; — and flag any miss for review. The &lt;a href="https://en.wikipedia.org/wiki/Tz_database" rel="noopener noreferrer"&gt;Wikipedia entry on tz database&lt;/a&gt; documents the file layout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute the offset for the target instant.&lt;/strong&gt; This is not &lt;code&gt;getOffset()&lt;/code&gt;; it is the offset function &lt;code&gt;offset(t) = local(t) − UTC(t)&lt;/code&gt; evaluated at the instant of interest. Most libraries expose this as &lt;code&gt;DateTimeFor(t).offset&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Format using CLDR locale data.&lt;/strong&gt; Pass the resolved zone plus the user's language tag into an ICU formatter. Do not hand-roll AM/PM logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persist the resolved IANA identifier, not the offset.&lt;/strong&gt; DST changes; identifiers are stable. Storing offsets is a classic postmortem root cause.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If step 2 fails, do not silently fall back to UTC. Return an explicit unresolved state and surface it in the UI. The cost of a wrong fallback during a scheduling meeting is much higher than the cost of an empty clock row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drift, Skew, and the Things That Go Bump at 02:00 Local
&lt;/h2&gt;

&lt;p&gt;Three failure patterns recur in cross-region incidents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Container clock drift.&lt;/strong&gt; A container running for months without an NTP sync can drift minutes per day if its host kernel is virtualized without &lt;code&gt;kvm-clock&lt;/code&gt;. Always run chrony or systemd-timesyncd inside long-lived containers; never assume the host is honest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DST transition windows.&lt;/strong&gt; Brazil abolished DST in 2019, Chile abolished it for most regions in 2015, and Mexico's border municipalities follow US rules while the rest of the country does not. A scheduled job that ran at "02:30 local" pre-transition can land at "03:30 local" post-transition without a code change. Encode jobs in UTC and project to local at display time only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ISO week vs fiscal week.&lt;/strong&gt; ISO 8601 defines weeks starting Monday and the first week containing a Thursday as week 1. Your finance team may use a different definition. Coordinate the conversion in one place — usually a &lt;code&gt;week_of_year(civil_date, locale)&lt;/code&gt; helper — not in thirty scattered report queries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When triaging an incident that touches civil time, the first three questions to ask are: what is the UTC instant, what is the IANA zone the operator believes they are in, and what is the IANA zone the system actually applied. Almost every confusing report becomes clear once those three values are written down explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Playbook for Cross-Region Coordination
&lt;/h2&gt;

&lt;p&gt;Here is the checklist I run through before announcing a maintenance window that touches more than one region:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;State the window in UTC first, then list local projections for every participating region.&lt;/li&gt;
&lt;li&gt;For each region, confirm whether the window crosses a DST or offset transition.&lt;/li&gt;
&lt;li&gt;Verify that the region's tz database version is current on every host that will execute work in the window.&lt;/li&gt;
&lt;li&gt;Identify the human on call in each region and the local civil time at which they pick up.&lt;/li&gt;
&lt;li&gt;Capture the IANA identifier and offset for each on-call location in the runbook, so a responder searching the document at 03:00 does not have to recompute it.&lt;/li&gt;
&lt;li&gt;After the window, archive the wall-clock times of every event in the postmortem with their UTC equivalent.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your team needs a quick reference display while executing that playbook, a lightweight world clock widget on the incident channel removes the cognitive load of converting in your head. The tool linked at the top of this article is built for that exact workflow — keep the runbook authoritative and use the widget as a verification surface, not as the source of record.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Compute, When to Trust the Wire
&lt;/h2&gt;

&lt;p&gt;Not every system should compute civil time itself. If you are displaying time that originates from an API, the safest pattern is to propagate the producer's formatted string plus the IANA zone plus the source instant, then reformat on the consumer side. This is the same pattern W3C &lt;a href="https://www.w3.org/TR/" rel="noopener noreferrer"&gt;Date and Time Formats&lt;/a&gt; recommends for &lt;code&gt;datetime&lt;/code&gt; interchange. Avoid shipping pre-formatted local strings to downstream consumers; they will silently disagree with whoever computed them.&lt;/p&gt;

&lt;p&gt;For client-only displays, prefer the platform's built-in &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt; with an explicit &lt;code&gt;timeZone&lt;/code&gt; option. MDN documents the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat" rel="noopener noreferrer"&gt;Intl.DateTimeFormat constructor&lt;/a&gt; and the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/supportedValuesOf#supported_values_of_time_zone" rel="noopener noreferrer"&gt;IANA time zone names&lt;/a&gt; the constructor accepts. This guarantees that the offset you see came from the same tz database your server uses, modulo the user's update cadence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thought
&lt;/h2&gt;

&lt;p&gt;A trustworthy shared clock is mostly a discipline problem: name your sources, version your tz data, store identifiers instead of offsets, and project to local only at the edge. Once those rules are in place, the wall-clock widget becomes what it should be — a quiet confirmation that the numbers in your runbook match the numbers in your head.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the minimum data I need to display correct local time?
&lt;/h3&gt;

&lt;p&gt;You need an IANA tz identifier for the location and the UTC instant you want to project. From those two inputs, any compliant library can derive the formatted string using the current tz database. An offset alone is not enough, because the offset can change for the same zone across the year.&lt;/p&gt;

&lt;h3&gt;
  
  
  How often should I update my tz database?
&lt;/h3&gt;

&lt;p&gt;Most operating systems ship tz database updates via standard patch channels and apply them automatically. For servers that handle scheduled jobs, I treat a tz database bump as a low-priority maintenance event and confirm it within a week of release. Critical infrastructure should pin the version it was tested against and audit changes before promotion.&lt;/p&gt;

&lt;h3&gt;
  
  
  My container's clock keeps drifting — what should I check?
&lt;/h3&gt;

&lt;p&gt;Confirm that NTP or chrony is running inside the container and can reach a time server. If your orchestration platform freezes process clocks for snapshots, that can look like drift after a resume. On most clouds the hypervisor's paravirtualized clock (&lt;code&gt;kvm-clock&lt;/code&gt;, &lt;code&gt;tsc&lt;/code&gt;) is good enough that host-side NTP is sufficient, but verify with &lt;code&gt;chronyc tracking&lt;/code&gt; rather than trusting the clock face.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is it safe to store local times in my database?
&lt;/h3&gt;

&lt;p&gt;No, unless the field is purely for display after the fact. Persist UTC instants for events and IANA identifiers for user preferences, and project to local at query time. Storing a formatted local string means every future DST or policy change misrepresents your historical data.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and reviewed for technical accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>lizely</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>aibotwrotethis</category>
    </item>
    <item>
      <title>Picking a Random Name From a List: A Practical Decision Guide for Engineers</title>
      <dc:creator>Tea-sip</dc:creator>
      <pubDate>Sun, 02 Aug 2026 02:05:11 +0000</pubDate>
      <link>https://dev.to/lizely/picking-a-random-name-from-a-list-a-practical-decision-guide-for-engineers-52gj</link>
      <guid>https://dev.to/lizely/picking-a-random-name-from-a-list-a-practical-decision-guide-for-engineers-52gj</guid>
      <description>&lt;p&gt;Rolling a name out of a hat sounds trivial until you actually have to do it in front of people. When fairness matters, when the list changes every session, and when someone will inevitably ask "but is it really random?", the method you choose quietly shapes how much credibility you have. This guide walks through three realistic approaches — pen and paper, a spreadsheet, and a purpose-built wheel — and tells you when each one earns its keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Method Matters More Than the Wheel
&lt;/h2&gt;

&lt;p&gt;A draw is only as trustworthy as the process behind it. If the audience cannot reproduce it, the result is just a coin flip someone else flipped. If the host can quietly retry until a "good" outcome comes up, the result is theatre. Psychology research on fairness perception, summarized in part by Wikipedia's &lt;a href="https://en.wikipedia.org/wiki/Random_assignment" rel="noopener noreferrer"&gt;Random assignment&lt;/a&gt; entry, suggests that visible process often matters as much as the outcome itself. That is why the choice of mechanism — not just the spinning animation — is the actual engineering decision.&lt;/p&gt;

&lt;p&gt;Concretely, the questions you should answer before picking an approach are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many entries are in the pool, and how often does it change?&lt;/li&gt;
&lt;li&gt;Does every entry need equal odds, or do some deserve more weight?&lt;/li&gt;
&lt;li&gt;Will the draw be live in front of people, or run offline?&lt;/li&gt;
&lt;li&gt;Do you need an audit trail — timestamps, the exact list used, the seed value?&lt;/li&gt;
&lt;li&gt;Will non-technical teammates be able to run it without help?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A draw for a Slack channel of twelve people bears no resemblance to one for a three-hundred-person conference, and the tooling should follow the situation, not the other way around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 1: Doing It by Hand
&lt;/h2&gt;

&lt;p&gt;Slips of paper, a deck of cards, or a hat still work. Write each item on an identical slip, fold them the same way, drop them into a container, and have someone else draw. This is the original pseudorandom device, and it has real advantages.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transparency: everyone watches the physical shuffling.&lt;/li&gt;
&lt;li&gt;Zero dependencies: no electricity, no browser, no spreadsheet license.&lt;/li&gt;
&lt;li&gt;Pedagogically rich: good for explaining probability to kids or trainees.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trade-offs are real, though. You cannot easily weight entries without writing numbers on the back of slips and doubling some, which the audience will notice and may dispute. You cannot add or remove an entry mid-session without a recount. And with fewer than roughly thirty slips, physical imperfections (size, thickness, how tightly folded) introduce subtle bias. As the pool grows, manual draws become slow and error-prone.&lt;/p&gt;

&lt;p&gt;Hand-drawn methods earn their place when the audience is small, the stakes are low, and the ritual itself is the point — a team lunch, a classroom exercise, a wedding. They do not scale, and they resist automation or audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 2: A Spreadsheet With a Random Number
&lt;/h2&gt;

&lt;p&gt;For repetitive draws, a spreadsheet is the path many engineers reach for first. One column holds the pool, a second column uses &lt;code&gt;RAND()&lt;/code&gt; or &lt;code&gt;RANDOM()&lt;/code&gt; to produce a value, sort by that column, and the top row is your winner. If you need weighting, multiply the random value by a weight column or use &lt;code&gt;INDEX&lt;/code&gt; with &lt;code&gt;MATCH&lt;/code&gt; against a cumulative distribution.&lt;/p&gt;

&lt;p&gt;This shines when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want reproducibility. Most spreadsheets expose the seed via the recalculation state, and you can save a snapshot.&lt;/li&gt;
&lt;li&gt;You need to attach metadata. A timestamp column, a session ID, and a hash of the pool turn the draw into evidence.&lt;/li&gt;
&lt;li&gt;You are integrating with other workflows. A CSV export feeds downstream reporting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The costs are subtler. Spreadsheet RNG is documented in vendor docs but rarely audited by the user — a quick read of the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random" rel="noopener noreferrer"&gt;Mozilla Developer Network entry on &lt;code&gt;Math.random&lt;/code&gt;&lt;/a&gt; shows that it is not suitable for cryptographic or fairness-critical use, and the same caveat applies to most spreadsheet implementations. Sorting also changes the visible structure, which can be confusing in a live demonstration. If a non-technical teammate will run this, count on spending twenty minutes documenting the steps.&lt;/p&gt;

&lt;p&gt;A spreadsheet is the sweet spot when you are running dozens of draws a week, need an audit trail, and are comfortable owning the procedure yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 3: A Purpose-Built Online Wheel
&lt;/h2&gt;

&lt;p&gt;When the draw is public and the audience expects something to spin, a dedicated tool reduces friction. The core requirements are simple: paste a list, spin, and show a result. The interesting engineering questions are the ones the tool abstracts away — how the underlying selection is implemented, what happens when the list contains duplicates, and whether a screenshot is enough to back the claim.&lt;/p&gt;

&lt;p&gt;For a one-off session or a recurring public draw, a web-based picker handles the parts that slow down hand-built solutions. A walkthrough of the fair-spin method, including how to scrub the list, what to record, and how to spot a rigged animation, is laid out in the &lt;a href="https://www.lizecheng.net/generators/guides/pick-random-name-from-list-wheel-the-fair-spin-method/" rel="noopener noreferrer"&gt;fair-spin guide on Lizely&lt;/a&gt;, which is the most thorough reference I have found on the procedural side.&lt;/p&gt;

&lt;p&gt;What a wheel does not solve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It does not generate your list. You still need to decide who is eligible.&lt;/li&gt;
&lt;li&gt;It does not store history unless you do. Save the screenshot and the input list yourself.&lt;/li&gt;
&lt;li&gt;It does not replace the social contract. If your audience does not trust the host, a fancier animation will not save you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where it wins is in the mundane middle: a team standup, a giveaway in a streamed session, a classroom lottery that runs every Friday. The overhead is roughly a minute of setup, and the result is something people can watch without explanation.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Checklist for Choosing Your Method
&lt;/h2&gt;

&lt;p&gt;Before reaching for any tool, walk through this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;List the pool size and the expected variance week to week.&lt;/li&gt;
&lt;li&gt;Decide whether equal odds are required, or if some entries should be more likely.&lt;/li&gt;
&lt;li&gt;Identify who runs the draw and whether they can be trained.&lt;/li&gt;
&lt;li&gt;Define what proof of fairness you need to keep — a screenshot, a log row, a witness signature.&lt;/li&gt;
&lt;li&gt;Pick the lightest method that satisfies all four constraints.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you can answer "manual, equal odds, I run it, a witness is enough", then slips of paper are correct. If you need "automated, weighted, anyone on the team, plus a log", the spreadsheet is right. If you need "live, public, light-touch, reversible within one session", use a wheel.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Worked Example
&lt;/h2&gt;

&lt;p&gt;Consider a fifty-person engineering org running a monthly "demo lottery" where one slot is drawn to present at the all-hands. The pool is built from a query against the HR database, so it is fresh every month. The host rotates among managers, most of whom do not write code. Auditors will ask, twice a year, whether a given draw was fair.&lt;/p&gt;

&lt;p&gt;A spreadsheet is appealing but creates a support burden: every new manager needs the template explained, and the file lives in someone's drive. Hand-drawing is too tempting to cheat with when the prize is visibility. A wheel, run from a shared link with a standardized list, keeps the procedure identical regardless of who is hosting. The screenshot plus the exported list is the audit trail.&lt;/p&gt;

&lt;p&gt;That is the pattern worth internalizing: when the host changes and the audience persists, standardize the tool first and the conversation second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I know the draw is actually random if I use a tool I cannot inspect?
&lt;/h3&gt;

&lt;p&gt;You cannot fully. What you can do is run the same tool many times in private, compare against a known-good source like a spreadsheet, and confirm that the distribution matches. Treat the tool as a black box you have validated, not as an oracle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I include duplicates or aliases in the list?
&lt;/h3&gt;

&lt;p&gt;No. Build a canonical list first with one row per real entity. If "Alex" appears twice, you have already biased the draw before the tool even runs. Normalize the input, then put the list through whichever picker you are using.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the smallest defensible audit trail?
&lt;/h3&gt;

&lt;p&gt;A timestamp, the exact list used, a screenshot of the result, and the name of the host. If your process is challenged, those four artifacts answer ninety percent of the questions a reasonable observer will ask.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I reuse one list for every draw in a series?
&lt;/h3&gt;

&lt;p&gt;You can, but it is rarely wise. Pools shift — people leave projects, new joiners appear, eligibility rules change. A draw that used a stale list is not unfair, but it is unauditable, and that is almost the same problem.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and reviewed for technical accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>lizely</category>
      <category>generators</category>
      <category>webdev</category>
      <category>aibotwrotethis</category>
    </item>
    <item>
      <title>Designing QR Codes That Survive Real-World Scanning: A Debugging Playbook</title>
      <dc:creator>Tea-sip</dc:creator>
      <pubDate>Fri, 31 Jul 2026 19:02:30 +0000</pubDate>
      <link>https://dev.to/lizely/designing-qr-codes-that-survive-real-world-scanning-a-debugging-playbook-1f9l</link>
      <guid>https://dev.to/lizely/designing-qr-codes-that-survive-real-world-scanning-a-debugging-playbook-1f9l</guid>
      <description>&lt;p&gt;A QR code can look perfect in your browser and still fail on a printed flyer behind glass, on a wrinkled shipping label, or under a flickering streetlight at dusk. When you treat generation as a one-step drop into a designer, you skip the constraints that decide whether the code is actually scannable in the field. This article walks through the engineering decisions that show up after you hit "download" — the ones that decide whether your audience scans cleanly or gets a "Could not read code" error.&lt;/p&gt;

&lt;p&gt;We'll treat the QR code as an input/output system: inputs are the data payload, version, error correction level, and module size; outputs are reliability across distance, lighting, surface, and reader hardware. Most production failures trace back to a mismatch between those inputs and the physical medium.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understand the Encoding Budget Before You Pick a Version
&lt;/h2&gt;

&lt;p&gt;Every QR code carries a fixed payload capacity per version and error-correction level. Version 1 holds up to 25 alphanumeric characters at level L (low correction), but the same symbol only holds 14 characters at level H (high correction). Version 10 jumps to 271 alphanumeric characters at L but drops to 213 at H. The QR specification defines 40 versions, and the structural choices ripple outward into module density and quiet-zone requirements.&lt;/p&gt;

&lt;p&gt;This matters because the code's module count drives the physical size you need. A Version 10 symbol at the same module pitch as a Version 2 symbol needs more space per side to stay scannable. The International Organization for Standardization's &lt;a href="https://en.wikipedia.org/wiki/QR_code" rel="noopener noreferrer"&gt;ISO/IEC 18004 standard&lt;/a&gt; is the canonical reference for these tables, and the &lt;a href="https://en.wikipedia.org/wiki/QR_code" rel="noopener noreferrer"&gt;QR Code Wikipedia overview&lt;/a&gt; summarizes version/capacity trade-offs in plain language. If your payload sits near a version boundary, a single extra URL parameter can bump you up a version, which then changes the minimum print size.&lt;/p&gt;

&lt;p&gt;Practical rule: before generating, compute the version your payload needs at your chosen ECC level. If you can drop to L for a URL under 60 characters, you'll get a less dense code that scans from farther away at the same physical size. If you must use H because you're overlaying a logo on the center, accept the density penalty and size up accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick an Error Correction Level That Matches the Surface
&lt;/h2&gt;

&lt;p&gt;Error correction is not a "more is better" knob. The four levels — L (~7%), M (~15%), Q (~25%), H (~30%) — describe how much of the code can be destroyed before the payload is unrecoverable. A clean digital display can use L. A printed sticker on a courier's package that gets scuffed in transit wants Q or H. A code printed on a curved bottle cap that gets partially occluded by a logo needs H and a generous quiet zone.&lt;/p&gt;

&lt;p&gt;Trade-off: each step up in error correction costs you capacity and increases module density. A code that fits at version 5 / L might require version 7 / H for the same payload. Higher density means each module is smaller at a fixed print size, which reduces the working distance and demands better focus from the camera.&lt;/p&gt;

&lt;p&gt;If you need to drop a branded icon into the center, H gives you headroom — up to 30% of the modules can be obscured before the payload fails. For reference, the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img" rel="noopener noreferrer"&gt;MDN documentation on image error correction&lt;/a&gt; is not the right source here, but the Wikipedia page on Reed–Solomon error correction explains the underlying math if you want to understand why H is more expensive than Q.&lt;/p&gt;

&lt;h2&gt;
  
  
  Size and Quiet Zone: The Two Numbers Most People Get Wrong
&lt;/h2&gt;

&lt;p&gt;The most common field failure is a code that technically decodes in a lab and refuses to decode in a café window. Two physical parameters dominate: the module pitch (the size of one black square, measured in mm or inches) and the quiet zone (the white border required around the symbol).&lt;/p&gt;

&lt;p&gt;Module pitch drives working distance. A general guideline from print-production references is that the module pitch should be at least 0.4 mm for reliable scanning at hand-held distance, and larger if the code will be read from across a room. Tiny codes look elegant on a business card and frustrate users whose phone cameras can't resolve the modules.&lt;/p&gt;

&lt;p&gt;Quiet zone is non-negotiable. The QR specification requires a margin of at least four modules' width on all four sides. Printers, designers, and CMS templates routinely crop this. If your generator produces an image with no margin, you must add it yourself, or expect failures on first-encounter scans. When in doubt, treat the quiet zone as part of the artifact, not as decorative padding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Match the Output Format to the Distribution Channel
&lt;/h2&gt;

&lt;p&gt;A PNG with a transparent background is not the same artifact as an SVG. The format you choose changes how the code behaves through the rest of your pipeline.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Raster (PNG, JPEG)&lt;/strong&gt;: Fixed resolution. If you upscale beyond the native pixel grid, you get blur, which destroys module edges. Use these when the final display size is known and won't change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vector (SVG, EPS, PDF)&lt;/strong&gt;: Scales without quality loss. Use these for print pipelines, where the artwork may be resized, embedded in larger layouts, or sent to a commercial printer at arbitrary DPI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High-contrast B/W only&lt;/strong&gt;: Cameras use the contrast boundary to lock onto the finder patterns. If you tint the code with brand colors, keep contrast high — dark modules against a light background, with sufficient luminance difference. Avoid gradients on the modules themselves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For printed materials, SVG or PDF keeps the modules crisp at any scale. For a web page or app where the code is rendered at one fixed pixel size, a high-resolution PNG is fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a Pre-Print QA Checklist
&lt;/h2&gt;

&lt;p&gt;Before sending any QR code to print, run it through this ordered list. Catching issues at this stage costs minutes; catching them after 10,000 flyers have shipped costs the campaign.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decode the generated image with at least two independent scanners (your phone, a colleague's phone, a desktop tool). Confirm the payload matches what you intended, character for character.&lt;/li&gt;
&lt;li&gt;Print a proof at the final intended physical size, on the actual substrate (matte vs. glossy paper, fabric, plastic). Substrate reflectance changes effective contrast.&lt;/li&gt;
&lt;li&gt;Scan the proof under three lighting conditions: bright direct light, dim indoor light, and shadow. A code that fails under any of these will fail for someone in the field.&lt;/li&gt;
&lt;li&gt;Verify the quiet zone visually — measure four modules' width of clear space on every side. Crop tools and template overlays routinely remove it.&lt;/li&gt;
&lt;li&gt;If a logo or icon overlays the center, confirm the obscured area stays below 30% of the modules, and that the error correction level is H.&lt;/li&gt;
&lt;li&gt;Re-decode the printed proof. Do not assume the digital version is the artifact being shipped.&lt;/li&gt;
&lt;li&gt;Archive the final file alongside the source URL, version number, ECC level, and proof photo. Future you will need to audit this.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Common Debugging Scenarios
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom: Code scans on some phones, fails on others.&lt;/strong&gt; Usually a quiet-zone violation or a contrast issue. Older or budget cameras have less margin for error. Enlarge the code or restore the quiet zone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom: Code scans at arm's length but fails across a room.&lt;/strong&gt; Module pitch is too small. Increase the physical size of the printed symbol or reduce the version by shortening the payload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom: Code scans once, then stops working after a few weeks.&lt;/strong&gt; Surface degradation. UV, abrasion, or moisture is destroying modules. Switch to a more durable substrate, increase ECC to H, or add a laminate that doesn't reflect IR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom: Code redirects to the wrong URL.&lt;/strong&gt; Payload mismatch. The URL in your campaign tracker doesn't match the URL encoded in the file. Re-encode from the canonical URL and replace every instance.&lt;/p&gt;

&lt;p&gt;When you need a walkthrough of the generation step itself — version selection, ECC choice, and download formats — the &lt;a href="https://www.lizecheng.net/seo/guides/how-to-generate-a-qr-code-a-complete-guide-lizely/" rel="noopener noreferrer"&gt;Lizely guide on generating QR codes&lt;/a&gt; covers the mechanics in depth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Constraints Engineers Hit in Production
&lt;/h2&gt;

&lt;p&gt;Three constraints bite hardest when QR codes move from prototype to production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tracking attribution across physical surfaces.&lt;/strong&gt; A code on a poster and a code on a business card may encode different UTM-tagged URLs even though they lead to the same landing page. Decide your tagging scheme before you generate, and store the mapping between artifact and URL in a versioned log.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Print pipeline vs. web pipeline.&lt;/strong&gt; Print and web want different file formats, and going from one to the other often introduces subtle quality loss. Generate once at the highest fidelity you need, and derive smaller versions rather than re-encoding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reader diversity.&lt;/strong&gt; Not every scanner uses the same detection algorithm. Apple's Camera app, Google's Lens, and dedicated industrial readers handle damaged codes differently. Test with the readers your audience actually uses, not just the one on your desk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How small can a QR code be before it stops scanning?
&lt;/h3&gt;

&lt;p&gt;There is no universal minimum, because it depends on the camera, the version, and the ECC level. A practical floor for hand-held scanning at arm's length is a module pitch around 0.4 mm and a total size of roughly 2 cm per side for a low-version code. Larger versions need larger physical sizes to keep the module pitch above that threshold.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I use a dynamic QR code or a static one?
&lt;/h3&gt;

&lt;p&gt;Dynamic codes point to a redirect URL that you control, so you can change the destination after printing. Static codes encode the final URL directly and cannot be changed. Use dynamic when the destination might change, when you need scan analytics, or when you want to A/B test landing pages without reprinting. Use static when the URL is permanent, you want zero redirect latency, and you don't need analytics.&lt;/p&gt;

&lt;h3&gt;
  
  
  What error correction level should I choose?
&lt;/h3&gt;

&lt;p&gt;L (low) for clean digital displays with no logo overlay. M (medium) as a safe default for most printed materials. Q (quartile) when the surface may be lightly scuffed or the code will live outdoors. H (high) when you must overlay a center logo, the surface is curved, or the code may be partially occluded.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I verify a QR code before printing?
&lt;/h3&gt;

&lt;p&gt;Print a physical proof at the intended final size, on the actual substrate, and scan it with multiple devices under varied lighting. Decode the result and confirm it matches the intended URL. Check that the quiet zone — four modules of clear space on every side — is intact. Do not skip this step.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and reviewed for technical accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>lizely</category>
      <category>seo</category>
      <category>webdev</category>
      <category>aibotwrotethis</category>
    </item>
  </channel>
</rss>
