<?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: Rahib Ali</title>
    <description>The latest articles on DEV Community by Rahib Ali (@rahibaly).</description>
    <link>https://dev.to/rahibaly</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%2F4161638%2F48bc8b44-436e-4936-86c5-2a5781509765.png</url>
      <title>DEV Community: Rahib Ali</title>
      <link>https://dev.to/rahibaly</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rahibaly"/>
    <language>en</language>
    <item>
      <title>Compress JPG Images: How I Cut 60% of the Weight</title>
      <dc:creator>Rahib Ali</dc:creator>
      <pubDate>Mon, 09 Nov 2026 08:28:52 +0000</pubDate>
      <link>https://dev.to/rahibaly/compress-jpg-images-how-i-cut-60-of-the-weight-2jmj</link>
      <guid>https://dev.to/rahibaly/compress-jpg-images-how-i-cut-60-of-the-weight-2jmj</guid>
      <description>&lt;p&gt;Yes — you can cut most of a JPG's weight without any visible quality loss. I tested it on three real photos: a single image dropped from 606 KB to 241 KB, a second squeezed to an exact 100 KB target came out at 79 KB, and a batch of four fell from 5.60 MB to 2.47 MB.&lt;/p&gt;

&lt;p&gt;That is 60%, 87% and 56% saved — on the same photos, with no change you can see at normal viewing size. The trick is not a secret setting. It is checking the result at full zoom instead of trusting a number on a slider.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three tests, with real numbers
&lt;/h2&gt;

&lt;p&gt;I ran the tests on ordinary photos, not special test images. Each one went through the same tool, in the browser, with nothing uploaded. The point was to see what normal settings actually do, not what a marketing page claims.&lt;/p&gt;

&lt;h3&gt;
  
  
  One photo, quality mode
&lt;/h3&gt;

&lt;p&gt;The first photo started at 606 KB. I compressed it once, at the default quality setting, and it came out at 241 KB. That is 60% of the file gone, and at normal viewing size the two versions look the same. I only saw a difference when I zoomed in close and hunted for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  One photo, exact target size
&lt;/h3&gt;

&lt;p&gt;The second test used target-size mode. I set the target to 100 KB because that was the budget for the page. The tool returned a file of 79 KB, which is 87% smaller than the original. Hitting an exact size matters when a platform caps uploads or a page has a strict weight budget.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four photos, bulk mode
&lt;/h3&gt;

&lt;p&gt;The third test was a batch of four photos, compressed together. They started at 5.60 MB combined and finished at 2.47 MB, saving 56%. Bulk mode matters because nobody compresses one image at a time on a real site. A product page or a blog post has a dozen images, and doing them one by one is how the job never gets done.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually loses quality
&lt;/h2&gt;

&lt;p&gt;Three things change a JPG: quality, dimensions and format. Quality is the slider most tools show you. Dimensions are the pixel width and height. Format is JPG versus PNG versus WebP. Most of the damage people blame on compression actually comes from resizing badly or saving in the wrong format.&lt;/p&gt;

&lt;p&gt;Lowering quality removes detail your eyes cannot see at normal size. That is why a file can shrink by more than half and still look identical. The loss only shows up when you push the slider too far, or when you zoom in past 100% and look for it. At the size readers actually see, it is not there.&lt;/p&gt;

&lt;p&gt;Google's &lt;a href="https://web.dev/articles/compress-images" rel="noopener noreferrer"&gt;guide to compressing images&lt;/a&gt; makes the same point: the goal is the smallest file that still looks right, not the lowest number on a slider.&lt;/p&gt;

&lt;p&gt;Dimensions are the bigger lever when a photo is displayed small. A photo far wider than its display size carries far more data than the page needs. Resizing it first, then compressing, beats compressing alone every time. Check the display size before you touch the quality slider.&lt;/p&gt;

&lt;p&gt;Format is the third choice, and it is simple. Use JPG for photos. Use PNG only when you need transparency. WebP gives smaller files than JPG at the same visible quality, and most browsers support it now. If your audience includes very old browsers, keep a JPG as the fallback. MDN's &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types" rel="noopener noreferrer"&gt;image format guide&lt;/a&gt; lists which browsers support WebP and which still need a fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow I use now
&lt;/h2&gt;

&lt;p&gt;After those three tests, I settled on a short workflow. It takes a few minutes for a batch, and it works for blog images, product photos and portfolio shots alike. Here it is, in order.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check the display size first. If the image shows smaller than its real size, resize it to the display width before anything else.&lt;/li&gt;
&lt;li&gt;Compress once, at a middle quality setting, and check the result at 100% zoom. If it looks clean, stop there.&lt;/li&gt;
&lt;li&gt;If you have a hard budget — say 100 KB for a page-speed target — use target-size mode and let the tool hit it exactly.&lt;/li&gt;
&lt;li&gt;Do the whole batch at once instead of one file at a time. That is the only way a backlog of images actually gets finished.&lt;/li&gt;
&lt;li&gt;Convert to WebP when the platform accepts it, and keep the JPG for anywhere that does not.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I ran all three tests with a free tool that can &lt;a href="https://frostrank.com/tools/image-compressor" rel="noopener noreferrer"&gt;compress jpg&lt;/a&gt; images in the browser, with bulk mode and an exact target-size mode. Nothing is uploaded, so the photos never leave your device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistakes that cost quality
&lt;/h2&gt;

&lt;p&gt;The most common mistake is compressing the same file twice. Each pass throws away more detail, and the second pass has nothing good left to keep. Always compress from the original, never from an already compressed copy.&lt;/p&gt;

&lt;p&gt;The second mistake is saving a photo as PNG because it feels safer. PNG is lossless, so it sounds better. For a photo, it usually means a file several times larger than the same image as a JPG, with no visible benefit. PNG is for graphics and transparency, not photos.&lt;/p&gt;

&lt;p&gt;The third mistake is upscaling. If a photo is small, enlarging it will not add detail — it only makes the blur bigger, and then compression makes it worse. Use the photo at its real size, or pick a larger original.&lt;/p&gt;

&lt;p&gt;The last mistake is skipping the zoom check. A thumbnail hides everything. Open the compressed file at 100% and look at faces, text and edges. If those are clean, the file is good. If they are not, raise the quality and try again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers mean for a real page
&lt;/h2&gt;

&lt;p&gt;A single photo at 241 KB instead of 606 KB loads faster on a slow connection, and it costs the reader less data. A batch that drops from 5.60 MB to 2.47 MB is the difference between a page that feels heavy and one that feels instant. The savings add up because a page rarely has one image.&lt;/p&gt;

&lt;p&gt;The 79 KB result matters most for strict budgets. Some platforms cap uploads, and some pages have a weight target to meet. Target-size mode takes the guessing out of it: set the number, get a file under it, and move on. That is the feature I now reach for first when a limit is involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How much can I compress a JPG without losing quality?
&lt;/h3&gt;

&lt;p&gt;In my tests, between 56% and 87% came off with no visible difference at normal viewing size. The exact amount depends on the photo — busy, detailed photos compress less than simple ones — so always check the result at full zoom.&lt;/p&gt;

&lt;h3&gt;
  
  
  What quality setting should I use?
&lt;/h3&gt;

&lt;p&gt;Start in the middle, around 80, and check the result at 100% zoom. If it looks clean, stop. If you can see damage, raise the setting and try again. There is no single right number — the photo decides.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is 100 KB enough for a photo on the web?
&lt;/h3&gt;

&lt;p&gt;Yes, for most web photos. My target-size test produced a 79 KB file that looked clean at full zoom. Large hero images shown full-screen may need more room, but for blog and product photos, 100 KB is a sensible budget.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to upload my photos to compress them?
&lt;/h3&gt;

&lt;p&gt;No — not with a tool that runs in the browser. The photos I compressed never left my device, because the work happens locally. That also means there is no queue, no account and no waiting for an upload.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does compressing a JPG twice make it smaller?
&lt;/h3&gt;

&lt;p&gt;Yes, but it costs quality each time. Each pass throws away detail, and the second pass has less good data to work with. Compress once, from the original file, and keep the original safe in case you need it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;The short version is simple: check the display size, resize first, compress once, and look at the result at full zoom. My three tests saved 60%, 87% and 56% — and none of the photos looked worse. Start with the biggest image on your slowest page. That is where the win is.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>tools</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your JSON Formatter Says "Valid" — Here's What It's Not Telling You</title>
      <dc:creator>Rahib Ali</dc:creator>
      <pubDate>Mon, 09 Nov 2026 05:20:19 +0000</pubDate>
      <link>https://dev.to/rahibaly/your-json-formatter-says-valid-heres-what-its-not-telling-you-ki5</link>
      <guid>https://dev.to/rahibaly/your-json-formatter-says-valid-heres-what-its-not-telling-you-ki5</guid>
      <description>&lt;p&gt;Last month I spent forty minutes staring at a perfectly good API response. The endpoint returned 200. The payload looked right. My app still crashed on the third record, every single time.&lt;/p&gt;

&lt;p&gt;I pasted the response into a formatter. Big green checkmark: valid JSON. I pasted it again, slower, as if that would help. Still valid. The bug was not in the syntax at all, and that is exactly the problem with how most of us use formatters.&lt;/p&gt;

&lt;p&gt;A "valid" verdict answers one narrow question: does this text follow the grammar of JSON? It says nothing about whether the data means what you think it means. The gap between valid and correct is where the nastiest production bugs live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Valid is a low bar
&lt;/h2&gt;

&lt;p&gt;JSON has a tiny grammar. Objects, arrays, strings, numbers, true, false, null. That is the whole language, and the formal definition fits on a single page at json.org.&lt;/p&gt;

&lt;p&gt;A formatter that only checks grammar will happily bless a payload that loses data, misleads your code, or behaves differently in every language that touches it. The spec itself, RFC 8259, warns that object member names SHOULD be unique — and then admits that parsers disagree about what to do when they are not.&lt;/p&gt;

&lt;p&gt;So the green checkmark is the beginning of the investigation, not the end of it. Here are the four traps I now check for every time a payload misbehaves, in the order I check them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Duplicate keys silently eat your data
&lt;/h2&gt;

&lt;p&gt;This is the one that bit me last month. The response contained something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4821&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Aisha Khan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"starter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pro"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two &lt;code&gt;plan&lt;/code&gt; keys. Perfectly valid JSON grammar. Most parsers keep the last value and throw the first one away without a word. Some keep the first. A few raise an error. Your formatter probably did not even blink.&lt;/p&gt;

&lt;p&gt;In my case, the API had a merge bug upstream. The first &lt;code&gt;plan&lt;/code&gt; came from the billing service, the second from a stale cache. My app read "pro", the billing page read "starter", and the customer got charged for a plan the dashboard said she did not have.&lt;/p&gt;

&lt;p&gt;A good formatter flags duplicate keys loudly, because the JSON specification says names SHOULD be unique and duplicate names make behavior unpredictable. If yours does not, you are flying blind on the most common silent-data-loss bug in JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Trailing commas: valid in your editor, invalid in JSON
&lt;/h2&gt;

&lt;p&gt;If you write JavaScript all day, this one will get you eventually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rahib"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"developer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That trailing comma after "developer" is fine in JavaScript and Python. It is a syntax error in JSON. Strictly speaking, your formatter should reject it — and a strict one will.&lt;/p&gt;

&lt;p&gt;The confusion runs the other way too. Some lenient tools quietly accept trailing commas, so you ship a config file that works in your editor and fails in production, where the real parser is strict. I have seen this take down a deploy: the config validated in the developer's tool, then the server's JSON parser rejected it at startup.&lt;/p&gt;

&lt;p&gt;When a formatter accepts something, ask whether it is accepting real JSON or being polite. Politeness is not a feature here. You want the tool that fails exactly the way your production parser fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Numbers lose precision without warning
&lt;/h2&gt;

&lt;p&gt;This is the trap that costs real money, and it is the one almost nobody checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"order_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9007199254740993&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That number is valid JSON. It is also larger than 2^53, which means JavaScript cannot represent it exactly. Parse it with &lt;code&gt;JSON.parse&lt;/code&gt; and you get 9007199254740992. One digit off. Your order lookup now points at a different order — or at none at all.&lt;/p&gt;

&lt;p&gt;The same hazard hits any language with double-precision floats, which is most of them. The JSON spec places no limit on number precision, but your runtime does, and the two limits are not the same.&lt;/p&gt;

&lt;p&gt;APIs that return large IDs as numbers are a known footgun. The safe pattern is to send IDs as strings. But you cannot fix the API from your side of the screen. What you can do is notice: a formatter that shows you the parsed value next to the raw text makes the precision loss visible before it reaches your database.&lt;/p&gt;

&lt;p&gt;If you work with IDs, timestamps in milliseconds, or money in minor units, check what your formatter does with a 16-digit number. If it silently rounds, it is hiding exactly the bug you are trying to find.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Strings that look like numbers (and booleans that aren't)
&lt;/h2&gt;

&lt;p&gt;JSON has exactly six value types, and the boundaries between them are where bugs breed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"is_active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retry_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"19.99"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every value here is a string. &lt;code&gt;"true"&lt;/code&gt; is not the boolean &lt;code&gt;true&lt;/code&gt;. &lt;code&gt;"3"&lt;/code&gt; is not the number 3. In a loosely typed language, &lt;code&gt;"3" + 1&lt;/code&gt; gives you &lt;code&gt;"31"&lt;/code&gt;, not 4. In a strictly typed one, it gives you a type error at 2 a.m.&lt;/p&gt;

&lt;p&gt;This usually happens when a backend serializes everything as strings — a database driver returning text columns, or a CSV import that never converted types. The JSON is valid. The types are wrong. And the failure shows up three services downstream, far from the cause.&lt;/p&gt;

&lt;p&gt;A formatter that shows types explicitly — string vs number vs boolean, ideally with colour — turns this from a guessing game into a glance. When I am debugging a payload now, the first thing I look at is not the values. It is the types.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a good formatter actually tells you
&lt;/h2&gt;

&lt;p&gt;After a few rounds of this, my checklist for a JSON tool changed completely. When you paste a payload into an online JSON formatter, you are really asking it three questions at once: is the grammar right, are the types what I expect, and is anything being silently lost? Syntax validation is table stakes. What I actually need is a tool that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flags duplicate keys instead of silently accepting them.&lt;/li&gt;
&lt;li&gt;Rejects trailing commas the way a strict parser would.&lt;/li&gt;
&lt;li&gt;Shows parsed numbers next to raw text so precision loss is visible.&lt;/li&gt;
&lt;li&gt;Makes types obvious at a glance, with strings, numbers, and booleans visually distinct.&lt;/li&gt;
&lt;li&gt;Formats and minifies without ever changing the data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a short list, and most tools fail at least one item on it. The one I reach for now is the free &lt;a href="https://frostrank.com/tools/json-formatter" rel="noopener noreferrer"&gt;json formatter&lt;/a&gt; from FrostRank — it validates strictly, flags duplicate keys, and shows the parsed structure with types, which is exactly the set of checks this article is about.&lt;/p&gt;

&lt;p&gt;The point is not which tool you use. The point is knowing what to demand from it. A formatter that only says "valid" is a spell-checker that never checks meaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  A debugging checklist for the next bad payload
&lt;/h2&gt;

&lt;p&gt;The next time a payload looks right and behaves wrong, work through these in order before you blame the API:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check for duplicate keys. Search the raw text for repeated member names inside the same object.&lt;/li&gt;
&lt;li&gt;Check the types. Is anything that should be a number or boolean actually a string?&lt;/li&gt;
&lt;li&gt;Check large numbers. Any integer above 9,007,199,254,740,991 may have lost precision in transit.&lt;/li&gt;
&lt;li&gt;Check for trailing commas and comments. Both are common in hand-written config and both are invalid JSON.&lt;/li&gt;
&lt;li&gt;Parse it in the same language your app uses. A payload that is valid in Python and valid in JavaScript can still behave differently in each.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most "the API is broken" bugs I have investigated turned out to be one of these five. The API was fine. The JSON was valid. And the data was wrong anyway.&lt;/p&gt;

&lt;p&gt;That is the lesson the green checkmark never teaches: valid JSON is a promise about grammar, not about meaning. The sooner you stop treating the two as the same thing, the sooner the forty-minute mysteries become five-minute fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The JSON grammar, in one page: &lt;a href="https://www.json.org/json-en.html" rel="noopener noreferrer"&gt;json.org&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The formal specification, RFC 8259: &lt;a href="https://datatracker.ietf.org/doc/html/rfc8259" rel="noopener noreferrer"&gt;RFC 8259&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;How JavaScript parses JSON, and where numbers lose precision: &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse" rel="noopener noreferrer"&gt;MDN — JSON.parse&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have your own JSON horror story — a duplicate key, a rounded ID, a string that should have been a number — I would genuinely like to hear it. The best debugging checklists are built from other people's production incidents, and this one is still growing.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Your barcode scans on screen but not in print? Here's the debugging order</title>
      <dc:creator>Rahib Ali</dc:creator>
      <pubDate>Mon, 02 Nov 2026 05:26:53 +0000</pubDate>
      <link>https://dev.to/rahibaly/your-barcode-scans-on-screen-but-not-in-print-heres-the-debugging-order-13k3</link>
      <guid>https://dev.to/rahibaly/your-barcode-scans-on-screen-but-not-in-print-heres-the-debugging-order-13k3</guid>
      <description>&lt;p&gt;If the barcode scans from your screen but dies the moment it's printed, the data is innocent and the print is the crime scene. Nearly every "won't scan" failure I have seen comes down to six causes: a wrong number or check digit, a cropped quiet zone, bad colors or contrast, wrong sizing, printer DPI and ink spread, or the scanner itself. Check them in that order and you will find the culprit in minutes instead of reprinting a thousand labels on a guess.&lt;/p&gt;

&lt;p&gt;The screen test is genuinely useful, by the way. If a code decodes on a monitor, the symbology, the encoded data, and the check digit are all structurally fine — which eliminates the data half of the problem and leaves only the physical half. That split is the whole debugging strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Start with the number, not the printer
&lt;/h2&gt;

&lt;p&gt;Retail barcodes carry a built-in lie detector: the check digit. The last digit of an EAN-13, EAN-8, UPC-A, or ITF-14 is computed from the other digits with the GS1 mod-10 formula, and a scanner uses it to catch misreads. A wrong check digit is the most expensive failure on this list, because nothing looks wrong until thousands of labels are already on shelves — the code either fails outright or, worse, decodes as a different product.&lt;/p&gt;

&lt;p&gt;The math is simple enough to check by hand. Take the first 12 digits of an EAN-13, say &lt;code&gt;590123412345&lt;/code&gt;: digits in odd positions count once, digits in even positions count three times.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Odd positions:   5 + 0 + 2 + 4 + 2 + 4       = 17
Even positions: (9 + 1 + 3 + 1 + 3 + 5) × 3  = 66
Total: 17 + 66 = 83 → check digit = (10 − (83 mod 10)) mod 10 = 7
Full EAN-13: 5901234123457
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type one digit wrong and the final digit changes — that is the entire point. If you are hand-rolling numbers for internal labels, validate before you print; and for anything sold in stores, remember the image is only half the job — the number inside a retail UPC or EAN has to be a GTIN licensed from &lt;a href="https://www.gs1us.org" rel="noopener noreferrer"&gt;GS1&lt;/a&gt;, or it can collide with another company's product.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The quiet zone is the number-one print failure
&lt;/h2&gt;

&lt;p&gt;The blank margin on each side of the bars is not decoration. Scanners use the quiet zone to find where the code starts and ends, and anything printed inside it — text, borders, background graphics, a die-cut line — can kill decoding outright. GS1 New Zealand, which verifies barcodes for companies supplying retailers, reports that symbols most often fail verification because the quiet zone is too narrow or the bars are too short.&lt;/p&gt;

&lt;p&gt;The spec is written in units of X, the width of the narrowest bar or space: an EAN-13 needs 11X on the left and 7X on the right, a UPC-A needs 9X on each side. As a rule of thumb, keep about ten times the narrow-bar width of clear space on both sides. The trap is that quiet zones die quietly in layout work — a barcode dropped into a label design, nudged against a border, or cropped to "save space" loses its margin, and the code that scanned perfectly in the generator stops scanning on the shelf. When you place a barcode, the margin is part of the barcode. The size numbers behind this are worth knowing: GS1 allows EAN-13 and UPC-A anywhere from 80% to 200% of nominal size, and the quiet zone scales with it, so a code shrunk for a small label has a proportionally smaller margin to protect.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Colors: contrast is physics, not taste
&lt;/h2&gt;

&lt;p&gt;Most laser scanners read with red light at around 660 nm. That single fact explains the most common color mistake: red or orange bars are nearly invisible to them, because the scanner's own light reflects off red ink almost as strongly as off the white background. Inverted colors — light bars on a dark background — fail the same way, since scanners are built to find dark bars against light spaces.&lt;/p&gt;

&lt;p&gt;The working rule is print contrast signal: PCS = (space reflectance − bar reflectance) ÷ space reflectance. Most 1D symbologies want at least 50% — black bars on a white background is the safest combination there is. Watch out for the sneaky contrast killers: glossy laminate that throws specular reflections into the scanner, yellowed thermal paper, and brand colors that look dark to your eye but reflect plenty of red light. If marketing insists on a colored barcode, test it under the actual store lighting with the actual scanner model before you commit — not on your desk, not on your phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Size: the 80–200% rule and the PNG trap
&lt;/h2&gt;

&lt;p&gt;An EAN-13 at full size is 37.29 × 25.93 mm including its quiet zones, with the narrowest bar (the X-dimension) at 0.33 mm. GS1 permits 80% to 200% of that for retail — at 80% the X-dimension drops to 0.264 mm, which is the floor for reliable checkout scanning. Below that, you have built a symbol no scanner can decode reliably, no matter how clean the artwork is.&lt;/p&gt;

&lt;p&gt;Two sizing mistakes show up constantly. The first is truncation: shortening the bar height to fit a cramped label. GS1 takes a zero-tolerance position on this — a truncated code only reads when the product crosses the beam in one particular direction, while a checkout lane scans omni-directionally. The second is the PNG trap: downloading a small raster barcode and stretching it unevenly in a design tool. Uneven scaling distorts the bar-to-space ratios the decoder depends on. For print work, generate a vector SVG and scale it proportionally, or download the PNG at the highest resolution and place it at exactly 100%.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Your printer is part of the barcode
&lt;/h2&gt;

&lt;p&gt;Here is the failure the screen test specifically isolates. A barcode is a precision optical target, and every printer distorts it a little — the question is whether the distortion stays inside the decoder's tolerance.&lt;/p&gt;

&lt;p&gt;Do the DPI math before you blame the artwork. A 203 dpi thermal printer lays down dots 0.125 mm across (25.4 ÷ 203). At 80% magnification your narrowest bar is 0.264 mm — barely two dots wide. Now add ink spread: on direct thermal and thermal transfer, and worse on inkjet, bars print slightly wider than specified and spaces slightly narrower. Spaces carry data too, so a narrow space that fills in is a dead code — and it looks perfectly fine to your eye, because human vision forgives what a decoder cannot. This is also why bar width reduction exists as a prepress technique: artwork is deliberately drawn with slightly narrower bars so the printed result lands on spec.&lt;/p&gt;

&lt;p&gt;The practical rule: test on the real printer, the real stock, at the real size. The office laser that produced a beautiful proof tells you nothing about what the warehouse thermal printer will do to the same file. When volume or retail is involved, print a size ladder — the same code at 80%, 100%, and 120% — and scan each rung with the actual scanner.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Suspect the scanner last
&lt;/h2&gt;

&lt;p&gt;If a clean, correctly sized, high-contrast code with a valid check digit still fails, interrogate the reader. Laser scanners and camera-based imagers behave differently: imagers (including phone cameras) are far more forgiving of contrast and damage, which is exactly why "scans on my phone but not at checkout" is such a common report — the phone proved the data is fine, the checkout laser is telling you the print is marginal.&lt;/p&gt;

&lt;p&gt;Check the mundane things first: a scratched or dirty scan window, the wrong scanning distance, and — the classic — the symbology disabled in the scanner's configuration. Many handhelds ship with Code 128 subsets or less common formats switched off; the scanner will stare straight through a perfect code it has been told not to read. A phone camera app makes a fine second opinion, but remember which direction the evidence points: the forgiving reader passing and the strict reader failing means the print is borderline, not that the strict reader is broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  The debugging order, as a checklist
&lt;/h2&gt;

&lt;p&gt;Work top to bottom; stop at the first failure you find:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Verify the number.&lt;/strong&gt; Recompute the check digit, and confirm the digits are the ones your system expects — not last month's SKU, not a truncated import.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scan a clean reference on screen.&lt;/strong&gt; Generate the same number fresh with a &lt;a href="https://frostrank.com/tools/barcode-generator" rel="noopener noreferrer"&gt;free barcode generator&lt;/a&gt; and decode it straight from your monitor. If the screen version scans and the printed one doesn't, the data is innocent and the print is the crime scene. (Generate it as SVG while you're there — you'll want the vector for the reprint.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swap the scanner.&lt;/strong&gt; Try a second reader, ideally a different technology — phone camera versus laser. A pass on one and fail on the other isolates print quality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure the quiet zone.&lt;/strong&gt; Put a ruler on the print: roughly ten narrow-bar widths of clear space on each side, no text or borders inside it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check colors and contrast under real light.&lt;/strong&gt; Dark bars, light background, no red or orange, no gloss throwing reflections at the scanner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Print a size ladder on the real printer.&lt;/strong&gt; Same code at 80%, 100%, and 120% on the actual stock, scanned with the actual reader.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For retail packaging, get a verification report.&lt;/strong&gt; A scanner beep is not a quality grade.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What "good enough" actually means
&lt;/h2&gt;

&lt;p&gt;Print quality for 1D barcodes is graded under ISO/IEC 15416 on an A–F scale (4.0 to 0), across parameters like symbol contrast, modulation, defects, and decodability — ten scan lines are measured, and the worst parameter sets the grade. GS1 requires retail symbols like EAN-13 and UPC-A to reach at least grade C (1.5), usually written on a verification report as 1.5/06/660: grade 1.5, measured with a 6-mil (0.15 mm) aperture under 660 nm light. A verifier is the only instrument that tells you the grade; a scanner only tells you pass or fail, today, on that reader. Most national GS1 offices offer verification services, and re-verifying once a year — or whenever you change printers, label stock, or packaging — is cheap insurance against a silent drift into unscannable.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why does my barcode scan on my phone but not at the store checkout?&lt;/strong&gt;&lt;br&gt;
Phone cameras are imagers: they photograph the whole code and are forgiving of marginal contrast, small sizes, and minor damage. Checkout lasers are strict — they need the full spec: intact quiet zones, strong contrast, and at least 80% magnification. A pass on the phone and a fail at the laser means the print is borderline, and the fix is in sections 2 through 5 above, not in the scanner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I print barcodes on a normal office printer?&lt;/strong&gt;&lt;br&gt;
For internal labels scanned by your own devices, yes — print at 100% or larger from a vector SVG, keep dark bars on white, protect the quiet zone, and test with the scanner that will actually read them. For retail packaging or high volume, use a thermal transfer printer and get a verification report before the print run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to buy a barcode verifier?&lt;/strong&gt;&lt;br&gt;
Only if you print retail packaging or ship to retailers who require a minimum grade. For internal inventory, asset tags, and event labels, the debugging order in this article plus a phone-camera second opinion gets you to reliable. The verifier earns its keep the moment someone else's scanner is the acceptance test.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;"Won't scan" is never one problem — it's a short list, and the list has an order. Validate the number, isolate print from data with a screen scan, then walk the physical causes: quiet zone, contrast, size, printer. The barcode that survives that order will scan on the cheapest reader in the building, which is the only test that matters.&lt;/p&gt;

&lt;p&gt;Further reading: &lt;a href="https://boxhero-en.ghost.io/barcode-label-size-placement/" rel="noopener noreferrer"&gt;BoxHero's barcode size and placement guide&lt;/a&gt; covers the GS1 magnification table this article summarizes, along with GS1 New Zealand's verification findings, and &lt;a href="https://www.gzljpt.com/en/guide-barcode-print-quality.html" rel="noopener noreferrer"&gt;this print-quality guide&lt;/a&gt; walks through all seven ISO/IEC 15416 grading parameters.&lt;/p&gt;

</description>
      <category>barcode</category>
      <category>gs1</category>
      <category>retail</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Your JWT decodes fine — so why does the API still return 401?</title>
      <dc:creator>Rahib Ali</dc:creator>
      <pubDate>Mon, 26 Oct 2026 05:24:22 +0000</pubDate>
      <link>https://dev.to/rahibaly/your-jwt-decodes-fine-so-why-does-the-api-still-return-401-4jd9</link>
      <guid>https://dev.to/rahibaly/your-jwt-decodes-fine-so-why-does-the-api-still-return-401-4jd9</guid>
      <description>&lt;p&gt;You paste the token into a decoder, the header and payload look perfect, and the API still answers 401. The token is not lying to you. You are asking it the wrong question.&lt;/p&gt;

&lt;p&gt;Decoding reverses Base64URL encoding: it needs no key and proves nothing. Verifying recomputes the signature with the signing secret or public key, then checks the claims against what the API expects. The decoder tells you what the token &lt;em&gt;says&lt;/em&gt;; only the verifier tells you whether the API should &lt;em&gt;believe&lt;/em&gt; it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The expiry bug that looks like a valid token
&lt;/h2&gt;

&lt;p&gt;JWT timestamps (&lt;code&gt;exp&lt;/code&gt;, &lt;code&gt;nbf&lt;/code&gt;, &lt;code&gt;iat&lt;/code&gt;) are NumericDate values: whole seconds since 1 January 1970 UTC (RFC 7519). JavaScript's &lt;code&gt;Date.now()&lt;/code&gt; returns milliseconds, so an issuer storing it directly as &lt;code&gt;exp&lt;/code&gt; gets the lifetime wrong by a factor of a thousand — while the payload still looks normal.&lt;/p&gt;

&lt;p&gt;The test is trivial: a 13-digit &lt;code&gt;exp&lt;/code&gt; is milliseconds — divide by 1000. The same bug in &lt;code&gt;nbf&lt;/code&gt; or &lt;code&gt;iat&lt;/code&gt; makes a token look "not yet valid" or impossibly old.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Clock skew, not a broken token
&lt;/h2&gt;

&lt;p&gt;A token issued by one server and verified by another lives or dies by their clocks: a verifier five minutes ahead expires a short-lived token before it arrives.&lt;/p&gt;

&lt;p&gt;Sync both clocks with NTP and allow a 30–60 second leeway in the verifier. A token failing on only one server in a fleet is almost always a clock problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The signature is fine, but the token is for someone else
&lt;/h2&gt;

&lt;p&gt;A valid signature only proves whoever holds the key signed the token. Two checks libraries do &lt;em&gt;not&lt;/em&gt; do for you:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audience (&lt;code&gt;aud&lt;/code&gt;).&lt;/strong&gt; The token must name this API; a staging token sent to production has the right signature and the wrong audience. &lt;code&gt;aud&lt;/code&gt; can be a string or an array.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Issuer (&lt;code&gt;iss&lt;/code&gt;).&lt;/strong&gt; Compare it against the exact issuer URL you trust, character for character, including trailing slashes. Most such rejections are staging-versus-production mix-ups.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The key rotated and nobody told the verifier
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;kid&lt;/code&gt; (key ID) header tells the verifier which key signed the token. After a rotation, tokens signed with the new key arrive while the verifier still holds the old one, or before the new key propagates — a sudden wave of "invalid signature" errors with no code change.&lt;/p&gt;

&lt;p&gt;If the failure correlates with a deploy or rotation, check &lt;code&gt;kid&lt;/code&gt; first, then which keys the verifier has loaded. The same symptom appears when issuer and verifier use different secrets.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The algorithm check you must never skip
&lt;/h2&gt;

&lt;p&gt;Two algorithm mistakes are security holes, not just bugs. First, never accept &lt;code&gt;alg: none&lt;/code&gt;: a token with no signature decodes perfectly and must be rejected outright. Second, pin the algorithm your verifier accepts — never let the token's header choose between an HMAC secret and a public key. An attacker who flips an RS256 token to HS256 can get a verifier using the public key as the HMAC secret to "verify" a token they signed themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automate the five checks
&lt;/h2&gt;

&lt;p&gt;This zero-dependency script runs all five checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// jwt-debug.mjs — usage: node jwt-debug.mjs &amp;lt;token&amp;gt; [secret]&lt;/span&gt;
&lt;span class="c1"&gt;// Decodes a JWT, reports the checks APIs actually fail on, and&lt;/span&gt;
&lt;span class="c1"&gt;// optionally verifies an HS256 signature. No dependencies.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timingSafeEqual&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;usage: node jwt-debug.mjs &amp;lt;token&amp;gt; [secret]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decodePart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^Bearer&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&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;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`parts: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;parts&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="s2"&gt; (expected 3; 5 parts means a JWE, not a JWT)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="mi"&gt;2&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;decodePart&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alg:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;| typ:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;typ&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(absent)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;| kid:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kid&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(absent)&lt;/span&gt;&lt;span class="dl"&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;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alg&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WARNING: alg=none — no signature; a correct API must reject this&lt;/span&gt;&lt;span class="dl"&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;now&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="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&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;const&lt;/span&gt; &lt;span class="nx"&gt;claim&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;exp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nbf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;claim&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;v&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: (absent)`&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="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;v&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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="s2"&gt; — looks like MILLISECONDS, not seconds; divide by 1000`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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="s2"&gt; = &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;claim&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;exp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EXPIRED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;claim&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nbf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NOT YET VALID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iss:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;iss&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(absent)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;| aud:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aud&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;| sub:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(absent)&lt;/span&gt;&lt;span class="dl"&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;secret&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alg&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HS256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;parts&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="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;parts&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64url&lt;/span&gt;&lt;span class="dl"&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;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HS256 signature:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;VALID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INVALID (wrong secret, rotated key, or edited token)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verified against a freshly generated HS256 token: correct secret reports VALID, a wrong secret INVALID, a millisecond &lt;code&gt;exp&lt;/code&gt; gets flagged, and a &lt;code&gt;Bearer&lt;/code&gt; prefix is stripped. Note it decodes only the first two parts — mapping over all three crashes trying to JSON-parse the signature, which is raw bytes, not JSON — and it uses &lt;code&gt;timingSafeEqual&lt;/code&gt;, not &lt;code&gt;===&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to paste a token safely
&lt;/h2&gt;

&lt;p&gt;One last rule: a live production token is a bearer credential, usable by anyone holding it until it expires, and a JWT is only encoded, not encrypted — anyone who gets it can read every claim without a key. Never put secrets in a payload; debug with a test token or an expired one.&lt;/p&gt;

&lt;p&gt;For a quick read without a script, I use &lt;a href="https://frostrank.com/tools/jwt-decoder" rel="noopener noreferrer"&gt;this free JWT decoder&lt;/a&gt; — it runs entirely in the browser, so the token never leaves my device, and converts &lt;code&gt;exp&lt;/code&gt;, &lt;code&gt;nbf&lt;/code&gt;, and &lt;code&gt;iat&lt;/code&gt; to readable times with an expiry status. That client-side property is what to check in any decoder: if a tool asks for a signing secret, close the tab. Verification belongs in your API, with a maintained library, on every request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;"Decodes fine" is the beginning of the investigation, not the end. Convert the timestamps, check the clock, compare issuer and audience, confirm the key behind &lt;code&gt;kid&lt;/code&gt;, and pin the algorithm — one of those five explains almost every 401 a well-formed token produces.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Redirect chains are silently slowing your site — here's how to find and fix them</title>
      <dc:creator>Rahib Ali</dc:creator>
      <pubDate>Fri, 23 Oct 2026 05:18:00 +0000</pubDate>
      <link>https://dev.to/rahibaly/redirect-chains-are-silently-slowing-your-site-heres-how-to-find-and-fix-them-13je</link>
      <guid>https://dev.to/rahibaly/redirect-chains-are-silently-slowing-your-site-heres-how-to-find-and-fix-them-13je</guid>
      <description>&lt;p&gt;You paste a URL into your browser. The page loads. Everything looks fine — but between your click and that final render, the browser may have silently hopped through two, three, or five redirects: &lt;code&gt;http&lt;/code&gt; to &lt;code&gt;https&lt;/code&gt;, bare domain to &lt;code&gt;www&lt;/code&gt;, old path to new path, a trailing slash added along the way.&lt;/p&gt;

&lt;p&gt;Browsers never show you this. They follow every hop automatically and render the destination. Redirect chains are invisible by design — and that is exactly why they quietly grow into a real problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a chain of redirects hurts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Latency.&lt;/strong&gt; Every 301 or 302 is a full extra HTTP round trip: DNS, TLS handshake, request, response. One hop is noise. Four hops on a mobile connection is a noticeable delay before the first byte of the real page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Crawl efficiency.&lt;/strong&gt; Googlebot follows redirect chains, but it gives up after about 10 hops. A page that is only reachable through a long chain gets crawled less often — and &lt;a href="https://developers.google.com/search/docs/crawling-indexing/301-redirects" rel="noopener noreferrer"&gt;Google's own documentation&lt;/a&gt; warns against chaining redirects where you can avoid it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Link equity and canonicalization.&lt;/strong&gt; A permanent move should be one clean 301 straight to the final URL. When old backlinks point at URL A, which 301s to B, which 302s to C, you are leaking both speed and signal at every step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Search Console noise.&lt;/strong&gt; "Page with redirect" and "Redirect error" reports are usually a chain problem, not a single-redirect problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  See the chain: curl
&lt;/h2&gt;

&lt;p&gt;The fastest honest view of a chain is in your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-IL&lt;/span&gt; https://example.com/old-page
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-I&lt;/code&gt; fetches headers only, &lt;code&gt;-L&lt;/code&gt; follows redirects. The output shows every hop — status code plus the &lt;code&gt;Location&lt;/code&gt; header it points to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="m"&gt;301&lt;/span&gt;
&lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://example.com/new-page&lt;/span&gt;

HTTP/2 302
location: https://example.com/new-page/

HTTP/2 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second hop is a 302 sending a permanent move to a trailing-slash variant — two separate mistakes visible in ten seconds of terminal output.&lt;/p&gt;

&lt;h2&gt;
  
  
  See the chain: DevTools
&lt;/h2&gt;

&lt;p&gt;In Chrome or Firefox DevTools, open the Network tab, tick "Preserve log", and reload. Every request in the chain appears with its status code. This is the best option when you suspect client-side (meta refresh or JavaScript) redirects, which &lt;code&gt;curl&lt;/code&gt; won't reveal — but it's tedious if you need to check more than a couple of URLs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trace chains in bulk: a tiny Node script
&lt;/h2&gt;

&lt;p&gt;When a migration leaves you with hundreds of legacy URLs, clicking through each one isn't an option. This script follows a chain one hop at a time and prints it — using &lt;code&gt;node:http&lt;/code&gt;/&lt;code&gt;node:https&lt;/code&gt; directly, because &lt;code&gt;fetch&lt;/code&gt; can't do this job: even with &lt;code&gt;redirect: 'manual'&lt;/code&gt; it hands back an opaque redirect response (status &lt;code&gt;0&lt;/code&gt;, headers unreadable). Raw HTTP requests have no such filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// trace.mjs — usage: node trace.mjs https://example.com/old-page&lt;/span&gt;
&lt;span class="c1"&gt;// Zero dependencies. http.get / https.get never follow redirects,&lt;/span&gt;
&lt;span class="c1"&gt;// so each hop's real status code and Location header stay visible.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;https&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:https&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;https&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;mod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User-Agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redirect-tracer/1.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// discard the body, we only need headers&lt;/span&gt;
    &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;);&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;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&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;hop&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;hop&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;hop&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&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;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`hop &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hop&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`\n      → &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;399&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&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;hop&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chain too long — stopping&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;next&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;Run it against a list of URLs from your sitemap or backlink export and you'll have a full chain audit in minutes. (&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Redirections" rel="noopener noreferrer"&gt;MDN's guide to HTTP redirections&lt;/a&gt; has the full reference on status-code semantics if you want the details.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The chains I keep finding in the wild
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The protocol + host stack.&lt;/strong&gt; &lt;code&gt;http://example.com&lt;/code&gt; → &lt;code&gt;https://example.com&lt;/code&gt; → &lt;code&gt;https://www.example.com&lt;/code&gt;. Two hops before the site even starts loading. Fix: every variant should 301 straight to the canonical URL in a single hop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Trailing-slash ping-pong.&lt;/strong&gt; &lt;code&gt;/page&lt;/code&gt; → &lt;code&gt;/page/&lt;/code&gt; → &lt;code&gt;/page&lt;/code&gt; — usually two CMS rules fighting each other. Pick one canonical form and enforce it once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Migration leftovers.&lt;/strong&gt; &lt;code&gt;old-domain.com/page&lt;/code&gt; → &lt;code&gt;new-domain.com/page&lt;/code&gt; → &lt;code&gt;new-domain.com/renamed-page&lt;/code&gt; → 200. Every legacy backlink pays three hops forever. Point the old URLs directly at their final destinations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The accidental 302.&lt;/strong&gt; A permanent move shipped as a 302 or 307 because it was the framework's default. Search engines keep indexing the old URL and never fully transfer the signal. If the move is permanent, it must be a 301.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Redirect loops.&lt;/strong&gt; A → B → A, and the browser gives up with &lt;code&gt;ERR_TOO_MANY_REDIRECTS&lt;/code&gt;. Almost always two conflicting rules — a www-forcing rule plus a non-www-forcing rule, or an http→https rule fighting an https→http one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Chains ending in errors.&lt;/strong&gt; The chain "works" in the browser but lands on a 404 or 500. Always check what the final hop returns, not just that the chain resolves.&lt;/p&gt;

&lt;h2&gt;
  
  
  A debugging workflow that actually works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Trace the full chain&lt;/strong&gt; — &lt;code&gt;curl -IL&lt;/code&gt; for one-offs, the script above for bulk. When I want a quick visual answer without the terminal, I paste the URL into &lt;a href="https://frostrank.com/tools/redirect-chain-checker" rel="noopener noreferrer"&gt;this free redirect chain checker&lt;/a&gt;, which follows every hop server-side and lists each status code and destination, with loop detection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count the hops.&lt;/strong&gt; More than two to reach the final page is worth flattening.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the status codes.&lt;/strong&gt; Permanent moves must be 301 — a 302/307 in the middle is a bug, not a preference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify the destination.&lt;/strong&gt; Confirm the final URL returns 200 and isn't carrying a surprise &lt;code&gt;noindex&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix at the source.&lt;/strong&gt; Point the original URL straight at the final destination, and update internal links, canonical tags, and sitemaps to skip the chain entirely. Redirecting to a URL that already redirects is how chains grow — one "quick fix" at a time.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Redirect chains accumulate because nothing complains about them: the browser hides them, users tolerate one extra hop, and each individual redirect looks harmless in isolation. Audit them after every migration, and spot-check your most-linked pages once in a while. The fix is almost always the same — one clean 301, straight to the destination.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>debugging</category>
      <category>http</category>
    </item>
    <item>
      <title>Stop Hand-Writing JSON-LD: Generate Valid Schema Markup in Seconds</title>
      <dc:creator>Rahib Ali</dc:creator>
      <pubDate>Sun, 04 Oct 2026 13:06:31 +0000</pubDate>
      <link>https://dev.to/rahibaly/stop-hand-writing-json-ld-generate-valid-schema-markup-in-seconds-4pe6</link>
      <guid>https://dev.to/rahibaly/stop-hand-writing-json-ld-generate-valid-schema-markup-in-seconds-4pe6</guid>
      <description>&lt;p&gt;If you've ever hand-written JSON-LD structured data, you know the drill: one missing comma, one wrong &lt;code&gt;@type&lt;/code&gt;, and Google silently ignores your rich results. I build SEO tools for a living, and I still don't write schema by hand anymore. Here's why — and the workflow I use instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why hand-written JSON-LD breaks
&lt;/h2&gt;

&lt;p&gt;JSON-LD looks simple until it isn't. The classic failure modes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Trailing commas&lt;/strong&gt; — valid in JS, fatal in JSON.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong nesting&lt;/strong&gt; — &lt;code&gt;geo&lt;/code&gt; inside &lt;code&gt;location&lt;/code&gt; vs. parallel to it for LocalBusiness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing required fields&lt;/strong&gt; — Google needs &lt;code&gt;headline&lt;/code&gt;, &lt;code&gt;author&lt;/code&gt;, &lt;code&gt;datePublished&lt;/code&gt;, and &lt;code&gt;image&lt;/code&gt; for Article. Miss one and you get no rich result, no error message, just silence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unescaped characters&lt;/strong&gt; — quotes inside descriptions breaking the whole block.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The 30-second workflow
&lt;/h2&gt;

&lt;p&gt;For standard schema types, a generator beats hand-writing every time. I built a &lt;a href="https://frostrank.com/tools/json-ld-schema-generator" rel="noopener noreferrer"&gt;free JSON-LD schema generator&lt;/a&gt; at FrostRank — no signup, runs entirely in your browser.&lt;/p&gt;

&lt;p&gt;Pick the type (Article, FAQPage, Product, LocalBusiness, Organization, BreadcrumbList, Person…), fill in the fields, copy the output. It comes wrapped in &lt;code&gt;&amp;lt;script type="application/ld+json"&amp;gt;&lt;/code&gt;, ready to paste into your page.&lt;/p&gt;

&lt;p&gt;Example output for an Organization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Organization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Inc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"logo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/logo.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sameAs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"https://twitter.com/example"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"https://linkedin.com/company/example"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop it in your &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@context&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://schema.org&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Organization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Example Inc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Always validate before you ship
&lt;/h2&gt;

&lt;p&gt;Generated or not, validate everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google's Rich Results Test&lt;/strong&gt; (&lt;a href="https://search.google.com/test/rich-results" rel="noopener noreferrer"&gt;https://search.google.com/test/rich-results&lt;/a&gt;) — paste the URL or code snippet; it flags errors and confirms rich-result eligibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema Markup Validator&lt;/strong&gt; (&lt;a href="https://validator.schema.org/" rel="noopener noreferrer"&gt;https://validator.schema.org/&lt;/a&gt;) — checks your markup against the schema.org spec itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the Rich Results Test shows errors, fix the fields and regenerate — don't patch the JSON by hand and reintroduce the exact mistakes the generator saved you from.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you SHOULD write it by hand
&lt;/h2&gt;

&lt;p&gt;Generators cover the standard types. Reach for hand-written JSON-LD when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're combining multiple entities with &lt;code&gt;@graph&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You need dynamic schema injected at runtime (e.g., a product price pulled from your DB)&lt;/li&gt;
&lt;li&gt;You're doing something exotic, like &lt;code&gt;HowTo&lt;/code&gt; with video clips&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even then, generate the skeleton first and extend it. Starting from valid markup beats starting from a blank file.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Don't hand-write boilerplate schema — generate it.&lt;/li&gt;
&lt;li&gt;Validate with Google's Rich Results Test before deploying.&lt;/li&gt;
&lt;li&gt;Keep a generator bookmarked for the next time someone says "can you add FAQ schema to this page?"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Try the &lt;a href="https://frostrank.com/tools/json-ld-schema-generator" rel="noopener noreferrer"&gt;free JSON-LD schema generator&lt;/a&gt; — it's client-side, so nothing you paste ever leaves your tab. What's your schema workflow — generator, plugin, or hand-rolled?&lt;/p&gt;

</description>
      <category>seo</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
