<?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: Ameen Usman</title>
    <description>The latest articles on DEV Community by Ameen Usman (@ameenusman).</description>
    <link>https://dev.to/ameenusman</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%2F4181012%2Fe1324f6d-6551-4ecc-ab3f-784cfd012669.png</url>
      <title>DEV Community: Ameen Usman</title>
      <link>https://dev.to/ameenusman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ameenusman"/>
    <language>en</language>
    <item>
      <title>0 9 1 * 5 is the 1st or Friday</title>
      <dc:creator>Ameen Usman</dc:creator>
      <pubDate>Tue, 03 Nov 2026 06:32:27 +0000</pubDate>
      <link>https://dev.to/ameenusman/0-9-1-5-is-the-1st-or-friday-2d0a</link>
      <guid>https://dev.to/ameenusman/0-9-1-5-is-the-1st-or-friday-2d0a</guid>
      <description>&lt;p&gt;The report was supposed to run only when the 1st landed on a Friday. The crontab said &lt;code&gt;0 9 1 * 5&lt;/code&gt;, and it also ran on a Friday that was not the 1st. On Vixie cron and cronie, both day fields being set means the job runs when either field matches. It does not wait for the 1st to be a Friday. That line is 09:00 on the 1st, and 09:00 on every Friday. A star in one of those fields turns the choice off. The &lt;a href="https://kepttools.com/cron-generator" rel="noopener noreferrer"&gt;cron generator&lt;/a&gt; can fill the five fields, and it cannot tell you which daemon will read them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the five fields actually select
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;0 9 1 * *&lt;/code&gt; is 09:00 on the 1st, and on no other day. The weekday is a star, so Friday never enters the decision.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;0 9 * * 5&lt;/code&gt; is 09:00 every Friday. The day of the month is a star, so the 1st matters only when that date is itself a Friday.&lt;/p&gt;

&lt;p&gt;The either-day rule starts when both day fields are restricted. A number, a list, a range, or a step such as &lt;code&gt;1-31/2&lt;/code&gt; counts as restricted. A day field that starts with &lt;code&gt;*&lt;/code&gt; does not, and &lt;code&gt;*/2&lt;/code&gt; is in that group. The step still picks days. Cron then joins that field to the other day field with AND.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;30 4 1,15 * 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that as 04:30 on the 1st, 04:30 on the 15th, and 04:30 every Friday. When the 1st is a Friday, it is still one line, so the command runs once that morning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Friday the 1st, on Vixie cron
&lt;/h2&gt;

&lt;p&gt;One standard five-field line cannot say “Friday the 1st only.” Schedule the date, and test the weekday in the command. On GNU date, and in POSIX, &lt;code&gt;%u&lt;/code&gt; is 1 for Monday through 7 for Sunday, so Friday is 5. Cron runs the command with &lt;code&gt;sh&lt;/code&gt;. The &lt;code&gt;[&lt;/code&gt; test is a POSIX builtin, and the job needs an absolute path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 9 1 * * [ "$(date +\%u)" -eq 5 ] &amp;amp;&amp;amp; /usr/local/bin/job
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the crontab, write &lt;code&gt;\%&lt;/code&gt;. A bare &lt;code&gt;%&lt;/code&gt; is a newline. A terminal keeps &lt;code&gt;%&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this rule stops
&lt;/h2&gt;

&lt;p&gt;The rule above is Vixie cron and cronie. This guide does not claim it for every Kubernetes CronJob, for GitHub Actions, or for BusyBox crond.&lt;/p&gt;

&lt;p&gt;Quartz, Spring, and EventBridge use a different field shape, with &lt;code&gt;?&lt;/code&gt; in a day field, so a five-field line matches none of them. systemd &lt;code&gt;OnCalendar&lt;/code&gt; joins a weekday and a day of the month with AND.&lt;/p&gt;

&lt;p&gt;The generator’s sentence for &lt;code&gt;0 9 1 * 5&lt;/code&gt; is “at minute 0, at hour 9, on day-of-month 1, on Friday.” That list is not the either-day rule. Test the line on the host that will run it.&lt;/p&gt;

&lt;p&gt;The full guide, with the one-minute test and the FAQ: &lt;a href="https://kepttools.com/guides/cron-both-day-fields" rel="noopener noreferrer"&gt;https://kepttools.com/guides/cron-both-day-fields&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cronjob</category>
      <category>linux</category>
      <category>devops</category>
      <category>kepttools</category>
    </item>
    <item>
      <title>What a JSON converter drafts from the first object</title>
      <dc:creator>Ameen Usman</dc:creator>
      <pubDate>Mon, 02 Nov 2026 11:53:20 +0000</pubDate>
      <link>https://dev.to/ameenusman/what-a-json-converter-drafts-from-the-first-object-2n0l</link>
      <guid>https://dev.to/ameenusman/what-a-json-converter-drafts-from-the-first-object-2n0l</guid>
      <description>&lt;p&gt;I copied an API response into a blank file and started a TypeScript interface by hand. The payload was a list of two people. Each object had an id, a name, a role, and a meta object with an active flag. I typed the fields from the first person and assumed the second person used the same keys. The JSON converter at &lt;a href="https://kepttools.com/json-converter" rel="noopener noreferrer"&gt;https://kepttools.com/json-converter&lt;/a&gt; makes that rule visible. Paste the text. It runs in the browser and drafts the interface from the first object only. The same paste can also become a PHP DTO, a Laravel array, or CSV. The sample below is the array the page fills, and the TypeScript is the converter's output for that text.&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="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"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;1&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;"Ada"&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;"engineer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"meta"&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="nl"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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="nl"&gt;"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;2&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;"Linus"&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;"founder"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"meta"&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="nl"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;TypeScript stops at Ada. Linus does not add a field. meta becomes its own interface because the value is an object. The root name is Root.&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Meta&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Meta&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;active&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PHP DTO is drafted from that same first object. The class is always named DataTransferObject. There is no child class. meta is &lt;code&gt;public array $meta&lt;/code&gt;, and the constructor copies &lt;code&gt;$data['meta']&lt;/code&gt; through as an array. Laravel still walks the whole array, so both people remain and meta stays nested. CSV keeps the top-level keys and writes meta as one cell, with the inner quotes doubled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id,name,role,meta
1,Ada,engineer,"{""active"":true}"
2,Linus,founder,"{""active"":false}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TypeScript and the PHP DTO are drafted from the first object in an array. CSV keeps top-level keys only, and a nested object becomes one cell. Invalid JSON shows the parse error and leaves the output empty. &lt;code&gt;{&lt;/code&gt; produces &lt;code&gt;Expected property name or '}' in JSON at position 1 (line 1 column 2)&lt;/code&gt;. The output box is cleared.&lt;/p&gt;

&lt;p&gt;Before you save the draft, read the second object. A key that exists only there will not appear on the interface or the PHP class. Put that key on the object you convert, or add the property yourself. Rename DataTransferObject before you ship the class. A missing key is assigned with &lt;code&gt;?? null&lt;/code&gt;, including on a property declared &lt;code&gt;public int $id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The full guide, with the FAQ and the converter: &lt;a href="https://kepttools.com/guides/json-to-typescript-php-dto-laravel" rel="noopener noreferrer"&gt;https://kepttools.com/guides/json-to-typescript-php-dto-laravel&lt;/a&gt;&lt;/p&gt;

</description>
      <category>json</category>
      <category>typescript</category>
      <category>php</category>
      <category>laravel</category>
    </item>
    <item>
      <title>What a JSON converter drafts from the first object</title>
      <dc:creator>Ameen Usman</dc:creator>
      <pubDate>Sun, 01 Nov 2026 05:55:29 +0000</pubDate>
      <link>https://dev.to/ameenusman/what-a-json-converter-drafts-from-the-first-object-24b6</link>
      <guid>https://dev.to/ameenusman/what-a-json-converter-drafts-from-the-first-object-24b6</guid>
      <description>&lt;p&gt;I had an API payload in one window and a blank TypeScript file in the other. The payload was a list of people. Each object carried an id, a name, a role, and a meta object with an active flag. I was ready to type the interface by hand and assume the second person would have the same keys as the first. That assumption is the useful part to check. The JSON converter at &lt;a href="https://kepttools.com/json-converter" rel="noopener noreferrer"&gt;https://kepttools.com/json-converter&lt;/a&gt; reads the paste in the browser and drafts the interface from the first object only. The same text can also become a Laravel array, a PHP DTO, or CSV. The blocks below are that sample, run through the converter, not a tidied rewrite of it.&lt;/p&gt;

&lt;p&gt;The Sample button uses this array:&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="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"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;1&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;"Ada"&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;"engineer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"meta"&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="nl"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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="nl"&gt;"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;2&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;"Linus"&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;"founder"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"meta"&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="nl"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;Laravel keeps both people, and it keeps meta nested as a PHP array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'id'&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="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Ada'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'role'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'engineer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'meta'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'active'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Linus'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'role'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'founder'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'meta'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'active'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&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="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TypeScript stops at Ada. Linus never adds a field. Meta becomes its own interface because the value is an object:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Meta&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Meta&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;active&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PHP DTO is one class, always named DataTransferObject, and it is also Ada only. Meta is array, not a nested class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DataTransferObject&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$meta&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$data&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;]&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;]&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'role'&lt;/span&gt;&lt;span class="p"&gt;]&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'meta'&lt;/span&gt;&lt;span class="p"&gt;]&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="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;CSV keeps the top-level keys and puts meta in a single cell. The nested object is JSON text, with quotes doubled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id,name,role,meta
1,Ada,engineer,"{""active"":true}"
2,Linus,founder,"{""active"":false}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A shorter array makes the same cut obvious. &lt;code&gt;[{"id":1,"name":"Ada"},{"id":2,"email":"linus@example.com"}]&lt;/code&gt; still becomes two Laravel rows, so &lt;code&gt;email&lt;/code&gt; survives there. The interface and the PHP class keep &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;name&lt;/code&gt; only. CSV's header is &lt;code&gt;id,name,email&lt;/code&gt;: Ada's row ends on an empty cell, and the next row leaves &lt;code&gt;name&lt;/code&gt; empty.&lt;/p&gt;

&lt;p&gt;Two limits sit on top of that sample. If the root is an array, TypeScript and the PHP DTO never look past the first object, so a key that exists only later is missing from the draft. CSV does look at later rows when it builds the header, but a nested object still occupies one cell, not a new column per inner key. Invalid JSON clears the output and shows the parse error. &lt;code&gt;{&lt;/code&gt; leaves the box empty with &lt;code&gt;Expected property name or '}' in JSON at position 1 (line 1 column 2)&lt;/code&gt;. An empty array is valid JSON and still not CSV: the output is cleared and the message is &lt;code&gt;CSV conversion expects an array of objects.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;One tip before you paste the draft into a project: open the second object and look for a key the first object does not have. Root will not list it. Add that field yourself, or convert a single object that already contains every key you mean to keep. The PHP constructor also assigns null when a key is absent, including into &lt;code&gt;public int $id&lt;/code&gt;, so treat the class as a draft and rename DataTransferObject.&lt;/p&gt;

&lt;p&gt;If the paste is hard to read, indent it first at &lt;a href="https://kepttools.com/json-formatter" rel="noopener noreferrer"&gt;https://kepttools.com/json-formatter&lt;/a&gt; and then convert. A trailing comma is enough to wipe the output.&lt;/p&gt;

&lt;p&gt;Full guide with the FAQ and the converter: &lt;a href="https://kepttools.com/guides/json-to-typescript-php-dto-laravel" rel="noopener noreferrer"&gt;https://kepttools.com/guides/json-to-typescript-php-dto-laravel&lt;/a&gt;&lt;/p&gt;

</description>
      <category>json</category>
      <category>typescript</category>
      <category>php</category>
      <category>laravel</category>
    </item>
    <item>
      <title>What cron expression runs every 5 minutes?</title>
      <dc:creator>Ameen Usman</dc:creator>
      <pubDate>Sat, 31 Oct 2026 05:18:12 +0000</pubDate>
      <link>https://dev.to/ameenusman/what-cron-expression-runs-every-5-minutes-1aa</link>
      <guid>https://dev.to/ameenusman/what-cron-expression-runs-every-5-minutes-1aa</guid>
      <description>&lt;p&gt;A cleanup job was supposed to run every five minutes. The crontab was saved at 10:03. The log showed 10:05, then 10:10. That is not a late start. The expression is tied to the clock, at minutes 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, and 55. Saving the file does not start a countdown. Classic cron also does not hold the next start while the previous process is still running, so a six-minute command overlaps itself. What follows is the five-field form used by Vixie cron and cronie, not a systemd timer and not Quartz.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five fields, then the command
&lt;/h2&gt;

&lt;p&gt;A standard line is five time fields and then the command. None of them is a seconds field.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Range&lt;/th&gt;
&lt;th&gt;A star means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Minute&lt;/td&gt;
&lt;td&gt;0–59&lt;/td&gt;
&lt;td&gt;Every minute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hour&lt;/td&gt;
&lt;td&gt;0–23&lt;/td&gt;
&lt;td&gt;Every hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Day of month&lt;/td&gt;
&lt;td&gt;1–31&lt;/td&gt;
&lt;td&gt;Every day of the month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Month&lt;/td&gt;
&lt;td&gt;1–12&lt;/td&gt;
&lt;td&gt;Every month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Day of week&lt;/td&gt;
&lt;td&gt;0–7&lt;/td&gt;
&lt;td&gt;Every day of the week&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On Vixie cron and on cronie, Sunday is both 0 and 7. Monday is 1 and Friday is 5, so &lt;code&gt;1-5&lt;/code&gt; is Monday through Friday. Some other schedulers accept only 0 for Sunday. Read crontab(5) on the host before using 7. Prefer numbers to names such as MON or JAN.&lt;/p&gt;

&lt;p&gt;The fields use the daemon’s timezone, so &lt;code&gt;0 0 * * *&lt;/code&gt; is midnight there, not necessarily UTC. Honor &lt;code&gt;CRON_TZ&lt;/code&gt; only if that host’s crontab(5) says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every 5, 10, 15, and 30 minutes
&lt;/h2&gt;

&lt;p&gt;Each line below is aligned to the clock, and each one includes minute 0.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*/5 * * * *     # 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55
*/10 * * * *    # 0, 10, 20, 30, 40, 50
*/15 * * * *    # 0, 15, 30, 45
*/30 * * * *    # 0 and 30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The edit time is not stored. A line installed at 10:03 still waits for 10:05, then 10:10.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;5-59/5 * * * *&lt;/code&gt; starts the range at 5, so minute 0 is left out. Use it only when you mean to skip the top of the hour. For a plain every-five-minutes job, keep &lt;code&gt;*/5 * * * *&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Drop the step when you want an hour or a day instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 * * * *       # every hour, at minute 0
0 0 * * *       # every day at 00:00
0 9 * * *       # every day at 09:00
0 9 * * 1-5     # Monday through Friday at 09:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;0 * * * *&lt;/code&gt; fires 24 times a day, &lt;code&gt;0 0 * * *&lt;/code&gt; once at midnight, and &lt;code&gt;0 9 * * 1-5&lt;/code&gt; on weekdays at 09:00 because day-of-month stays a star. The either-day rule starts only when both day fields are restricted. A day field that starts with &lt;code&gt;*&lt;/code&gt;, including &lt;code&gt;*/2&lt;/code&gt;, is not restricted. See &lt;a href="https://kepttools.com/guides/cron-both-day-fields" rel="noopener noreferrer"&gt;why cron runs on the wrong day&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a step is counted
&lt;/h2&gt;

&lt;p&gt;A step is a range plus &lt;code&gt;/n&lt;/code&gt;. In the minute field, &lt;code&gt;*&lt;/code&gt; means &lt;code&gt;0-59&lt;/code&gt;, so &lt;code&gt;*/5&lt;/code&gt; is 0, then every fifth minute through 55. Twelve runs an hour.&lt;/p&gt;

&lt;p&gt;In the hour field, &lt;code&gt;0 */2 * * *&lt;/code&gt; is minute 0 of every second hour: 00:00, 02:00, 04:00, and so on through 22:00.&lt;/p&gt;

&lt;p&gt;Day of month starts at 1, not 0. On Vixie cron and cronie, &lt;code&gt;*&lt;/code&gt; in that field is &lt;code&gt;1-31&lt;/code&gt;, so &lt;code&gt;0 0 */5 * *&lt;/code&gt; is midnight on the 1st, 6th, 11th, 16th, 21st, 26th, and 31st. It is not the dates divisible by 5 (the 5th, 10th, 15th, 20th, 25th, and 30th). If the daemon is not cronie, test that list. A &lt;a href="https://kepttools.com/cron-generator" rel="noopener noreferrer"&gt;cron expression generator&lt;/a&gt; shows the five fields. It does not simulate one daemon’s calendar, and it does not install the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistakes that move the run
&lt;/h2&gt;

&lt;p&gt;Copy the five fields, not the sentence beside them. Crontab does not read “every five minutes.”&lt;/p&gt;

&lt;p&gt;Do not add a seconds field in front. The other fields shift right. Quartz uses six fields, or seven with a year, and seconds come first. Spring uses that six-field form. EventBridge’s six fields are minutes, hours, day-of-month, month, day-of-week, and year, with no seconds field. Quartz and EventBridge both use &lt;code&gt;?&lt;/code&gt; in a day field. A five-field line matches neither shape.&lt;/p&gt;

&lt;p&gt;Traditional Vixie cron does not skip a run because the previous process is still going. If the work must not overlap, take a lock in the command, for example with &lt;code&gt;flock&lt;/code&gt;, and leave the schedule alone. systemd timers and queue workers can differ. This warning is about classic cron.&lt;/p&gt;

&lt;p&gt;Cron’s PATH is often shorter than a login shell. Call the program by its absolute path. A stored “next run” in an API is often a Unix timestamp, not an expression. Read that number on the &lt;a href="https://kepttools.com/timestamp" rel="noopener noreferrer"&gt;Unix timestamp converter&lt;/a&gt;. Do not paste it into the minute field.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;*/5&lt;/code&gt; cannot mean “five minutes from now.” Sleep in a process, or use a queue delay. The daemon still starts the command once, when the minute begins, not again at second 10 or 20.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it for one cycle
&lt;/h2&gt;

&lt;p&gt;Point a throwaway line at a file you can delete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;*&lt;/span&gt;/5 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /tmp/cron-every-5.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait until two lines are written. The minutes should be five apart, and each minute should be divisible by 5. Then delete the line. Some hosts log the start with &lt;code&gt;journalctl -u cron&lt;/code&gt;. Others still use syslog. Use the log that machine keeps.&lt;/p&gt;

&lt;p&gt;Full guide with the FAQ and a cron generator: &lt;a href="https://kepttools.com/guides/cron-every-5-minutes" rel="noopener noreferrer"&gt;https://kepttools.com/guides/cron-every-5-minutes&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cron</category>
      <category>linux</category>
      <category>devops</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How to encode a URL and escape HTML in the browser</title>
      <dc:creator>Ameen Usman</dc:creator>
      <pubDate>Thu, 29 Oct 2026 05:13:54 +0000</pubDate>
      <link>https://dev.to/ameenusman/how-to-encode-a-url-and-escape-html-in-the-browser-1nf1</link>
      <guid>https://dev.to/ameenusman/how-to-encode-a-url-and-escape-html-in-the-browser-1nf1</guid>
      <description>&lt;p&gt;A query string and an HTML page do not escape text the same way.&lt;/p&gt;

&lt;p&gt;This string is fine in a sentence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;Hello World&amp;gt; &amp;amp; "KeptTools"?q=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put it in a URL without encoding, and &lt;code&gt;&amp;amp;&lt;/code&gt; starts a new parameter. Put it in HTML without escaping, and &lt;code&gt;&amp;lt;&lt;/code&gt; starts a tag. Those are two different jobs.&lt;/p&gt;

&lt;p&gt;I use a small encoder for both. It runs in the browser, so the string is not uploaded.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kepttools.com/url-html-encoder" rel="noopener noreferrer"&gt;https://kepttools.com/url-html-encoder&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  URL encoding
&lt;/h2&gt;

&lt;p&gt;URL encoding turns characters that have a special meaning in a URL into &lt;code&gt;%&lt;/code&gt; sequences.&lt;/p&gt;

&lt;p&gt;The tool uses &lt;code&gt;encodeURIComponent&lt;/code&gt;, which is the right function for a query &lt;strong&gt;value&lt;/strong&gt; or a path segment. The sample becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%3CHello%20World%3E%20%26%20%22KeptTools%22%3Fq%3D1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;&lt;/code&gt; is &lt;code&gt;%3C&lt;/code&gt;, a space is &lt;code&gt;%20&lt;/code&gt;, &lt;code&gt;&amp;amp;&lt;/code&gt; is &lt;code&gt;%26&lt;/code&gt;, &lt;code&gt;"&lt;/code&gt; is &lt;code&gt;%22&lt;/code&gt;, and &lt;code&gt;?&lt;/code&gt; is &lt;code&gt;%3F&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;encodeURI&lt;/code&gt; is the other common function, and it is not the same. It leaves &lt;code&gt;&amp;amp;&lt;/code&gt;, &lt;code&gt;?&lt;/code&gt;, and &lt;code&gt;=&lt;/code&gt; alone, because those characters structure a full URL. Use that when you already have a complete address. Use &lt;code&gt;encodeURIComponent&lt;/code&gt; when the text is one value you are about to drop into a query string.&lt;/p&gt;

&lt;p&gt;Do not run &lt;code&gt;encodeURIComponent&lt;/code&gt; on a whole URL. It will also encode &lt;code&gt;https://&lt;/code&gt; and the &lt;code&gt;?&lt;/code&gt;, and the link will break.&lt;/p&gt;

&lt;h2&gt;
  
  
  HTML escaping
&lt;/h2&gt;

&lt;p&gt;HTML escaping is for text that should be shown, not parsed.&lt;/p&gt;

&lt;p&gt;The same sample, on HTML Encode, becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;amp;lt;Hello World&amp;amp;gt; &amp;amp;amp; &amp;amp;quot;KeptTools&amp;amp;quot;?q=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;&lt;/code&gt; becomes &lt;code&gt;&amp;amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt; becomes &lt;code&gt;&amp;amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;amp;&lt;/code&gt; becomes &lt;code&gt;&amp;amp;amp;&lt;/code&gt;, and &lt;code&gt;"&lt;/code&gt; becomes &lt;code&gt;&amp;amp;quot;&lt;/code&gt;. A browser then displays the characters instead of treating them as markup. An apostrophe is escaped as &lt;code&gt;&amp;amp;#39;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the step you want before you insert user text into HTML. URL encoding does not do it. &lt;code&gt;%3C&lt;/code&gt; is not the same thing as &lt;code&gt;&amp;amp;lt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decoding
&lt;/h2&gt;

&lt;p&gt;Each mode has a reverse.&lt;/p&gt;

&lt;p&gt;URL Decode turns the &lt;code&gt;%&lt;/code&gt; sequences back into characters. A broken sequence, such as a &lt;code&gt;%&lt;/code&gt; that is not followed by two hex digits, does not decode. The page reports that the encoding is invalid.&lt;/p&gt;

&lt;p&gt;HTML Decode turns the entities back into &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;amp;&lt;/code&gt;, and quotes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What stays on the machine
&lt;/h2&gt;

&lt;p&gt;Paste the text, or load the sample, pick URL Encode, URL Decode, HTML Encode, or HTML Decode, and copy the result. Nothing is sent to a server.&lt;/p&gt;

&lt;p&gt;If you are building a link, encode the value. If you are showing the value on a page, escape it for HTML. Do one, then the other, when you need both.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>html</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How to decode a JWT in the browser</title>
      <dc:creator>Ameen Usman</dc:creator>
      <pubDate>Wed, 28 Oct 2026 04:33:04 +0000</pubDate>
      <link>https://dev.to/ameenusman/how-to-decode-a-jwt-in-the-browser-10li</link>
      <guid>https://dev.to/ameenusman/how-to-decode-a-jwt-in-the-browser-10li</guid>
      <description>&lt;p&gt;A JWT looks secret. It is not. The header and the payload are only Base64url text. Anyone who has the token can read the claims. The signature is the part that proves the token was issued by someone who holds the key, and that check does not happen by decoding it.&lt;/p&gt;

&lt;p&gt;This is the distinction I keep having to make while debugging login. A readable &lt;code&gt;exp&lt;/code&gt; or &lt;code&gt;sub&lt;/code&gt; is useful. It is not proof the token is valid.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the three parts are
&lt;/h2&gt;

&lt;p&gt;A JWT is three pieces separated by dots:&lt;/p&gt;

&lt;p&gt;header.payload.signature&lt;/p&gt;

&lt;p&gt;The header names the algorithm, usually &lt;code&gt;HS256&lt;/code&gt; or &lt;code&gt;RS256&lt;/code&gt;. The payload holds the claims: &lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;role&lt;/code&gt;, &lt;code&gt;iat&lt;/code&gt;, &lt;code&gt;exp&lt;/code&gt;, and whatever else the issuer put there. The signature is the third part. A decoder shows it. It does not check it.&lt;/p&gt;

&lt;p&gt;Base64url is not encryption. Changing &lt;code&gt;-&lt;/code&gt; and &lt;code&gt;_&lt;/code&gt; back to &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt;, padding with &lt;code&gt;=&lt;/code&gt;, and running a Base64 decode gives you JSON. No secret is required for that step.&lt;/p&gt;

&lt;h2&gt;
  
  
  A token you can read by eye
&lt;/h2&gt;

&lt;p&gt;The sample on the KeptTools JWT decoder is shaped like a real token, with a placeholder in the signature slot so you can see the three parts:&lt;/p&gt;

&lt;p&gt;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IktlcHRUb29scyBVc2VyIiwiaWF0IjoxNzA5ODAwMDAwLCJleHAiOjE3Mzk4MDAwMDAsInJvbGUiOiJkZXZlbG9wZXIifQ.signature-placeholder&lt;/p&gt;

&lt;p&gt;The header decodes to:&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
  "alg": "HS256",&lt;br&gt;
  "typ": "JWT"&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;The payload decodes to:&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
  "sub": "1234567890",&lt;br&gt;
  "name": "KeptTools User",&lt;br&gt;
  "iat": 1709800000,&lt;br&gt;
  "exp": 1739800000,&lt;br&gt;
  "role": "developer"&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;&lt;code&gt;iat&lt;/code&gt; and &lt;code&gt;exp&lt;/code&gt; are Unix timestamps in seconds. &lt;code&gt;1709800000&lt;/code&gt; is the issued-at time. &lt;code&gt;1739800000&lt;/code&gt; is the expiry. Compare &lt;code&gt;exp&lt;/code&gt; with the current time before you treat the session as active. An expired token still decodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What decoding does not tell you
&lt;/h2&gt;

&lt;p&gt;A decoded payload does not mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the signature matches the header and payload&lt;/li&gt;
&lt;li&gt;the token came from your server&lt;/li&gt;
&lt;li&gt;the token has not been edited&lt;/li&gt;
&lt;li&gt;the session is still valid&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Signature checks belong on the server that holds the HMAC secret or the public key. For &lt;code&gt;HS256&lt;/code&gt;, that secret must never be in frontend code. For &lt;code&gt;RS256&lt;/code&gt;, the browser can hold a public key, but verification is still a separate step from reading the JSON.&lt;/p&gt;

&lt;p&gt;If you only need to see why a request is failing, decoding is enough. If you are deciding whether to trust the token, it is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do it without uploading the token
&lt;/h2&gt;

&lt;p&gt;Paste the token into a page that decodes it locally, or do the same two Base64url decodes in your own console. Either way, a production access token or a refresh token should not be sent to a server you do not control.&lt;/p&gt;

&lt;p&gt;I use the decoder at &lt;a href="https://kepttools.com/jwt-decoder" rel="noopener noreferrer"&gt;https://kepttools.com/jwt-decoder&lt;/a&gt; for this. The token stays in the tab. The page shows the header, the payload, and the signature string. It does not verify the signature, and it does not load analytics on that page.&lt;/p&gt;

&lt;p&gt;Paste a token, or load the sample. Read &lt;code&gt;alg&lt;/code&gt; in the header and &lt;code&gt;exp&lt;/code&gt; in the payload. Then verify the signature in the app that issued the token.&lt;/p&gt;

</description>
      <category>jwt</category>
      <category>webdev</category>
      <category>security</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Turn PDF pages into JPG images in your browser</title>
      <dc:creator>Ameen Usman</dc:creator>
      <pubDate>Mon, 26 Oct 2026 10:26:08 +0000</pubDate>
      <link>https://dev.to/ameenusman/turn-pdf-pages-into-jpg-images-in-your-browser-mei</link>
      <guid>https://dev.to/ameenusman/turn-pdf-pages-into-jpg-images-in-your-browser-mei</guid>
      <description>&lt;p&gt;A PDF page is often the wrong format for a slide, a chat message, or a blog image. Uploading the whole document just to export one page is a poor trade.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kepttools.com/pdf-to-jpg" rel="noopener noreferrer"&gt;PDF to JPG&lt;/a&gt; does that conversion in the browser. The file stays on your device. Nothing is uploaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to export the pages
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open the PDF.&lt;/li&gt;
&lt;li&gt;Choose all pages, or enter a range such as &lt;code&gt;1-3, 5&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Pick High, Medium, or Low quality.&lt;/li&gt;
&lt;li&gt;Convert, preview the pages, then download one JPG or a ZIP of all of them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A 5-page deck on High comes back as five images. Preview them before you download the ZIP.&lt;/p&gt;

&lt;p&gt;The limit shown on the page is 40 pages and 25 MB. A password-protected PDF has to be unlocked first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the JPG is
&lt;/h2&gt;

&lt;p&gt;Each page becomes a flat image. Text that you could select in the PDF cannot be selected in the JPG. Use this when you need a picture. Keep the original PDF when you still need selectable text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it runs
&lt;/h2&gt;

&lt;p&gt;The page is drawn in the browser and saved as a JPG from there. I do not receive the file.&lt;/p&gt;

&lt;p&gt;Tool: &lt;a href="https://kepttools.com/pdf-to-jpg" rel="noopener noreferrer"&gt;https://kepttools.com/pdf-to-jpg&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Merge, split, and compress PDFs in your browser</title>
      <dc:creator>Ameen Usman</dc:creator>
      <pubDate>Sun, 25 Oct 2026 07:55:38 +0000</pubDate>
      <link>https://dev.to/ameenusman/merge-split-and-compress-pdfs-in-your-browser-54gg</link>
      <guid>https://dev.to/ameenusman/merge-split-and-compress-pdfs-in-your-browser-54gg</guid>
      <description>&lt;p&gt;I kept avoiding random PDF websites for contracts, invoices, and reports. Those files should not leave the laptop just to merge two exports or pull out one page.&lt;br&gt;
So I added PDF tools to &lt;a href="https://kepttools.com" rel="noopener noreferrer"&gt;KeptTools&lt;/a&gt;. They run in the browser. Nothing is uploaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merge a few PDFs into one
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="https://kepttools.com/pdf-merge" rel="noopener noreferrer"&gt;Merge PDF&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add two or more PDFs.&lt;/li&gt;
&lt;li&gt;Put them in the order you want. Remove any you do not need.&lt;/li&gt;
&lt;li&gt;Merge and download one PDF.
Example: add &lt;code&gt;report-a.pdf&lt;/code&gt; and &lt;code&gt;report-b.pdf&lt;/code&gt;, put A first, then download &lt;code&gt;kepttools-merged.pdf&lt;/code&gt; with every page in that order.
You can merge up to 20 files, 25 MB each. Password-protected PDFs have to be unlocked first.
## Pull pages out of a long PDF
Open &lt;a href="https://kepttools.com/pdf-split" rel="noopener noreferrer"&gt;Split PDF&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Choose one PDF.&lt;/li&gt;
&lt;li&gt;Enter ranges, or split every page.&lt;/li&gt;
&lt;li&gt;Download one PDF, or a ZIP when there is more than one result.
Ranges look like &lt;code&gt;1-3, 5&lt;/code&gt; or &lt;code&gt;1-3, 7-10&lt;/code&gt;. Page numbers start at 1.
Example: a 10-page PDF with &lt;code&gt;1-3, 7-10&lt;/code&gt; comes back as two files in a ZIP. Pages 1–3 in one, pages 7–10 in the other.
## Shrink the file, keep the text
&lt;a href="https://kepttools.com/pdf-compress" rel="noopener noreferrer"&gt;Compress PDF&lt;/a&gt; recompresses images inside the PDF. Light, Medium, and Strong change image quality. The text stays real text, so you can still select it.
A text-only PDF may barely get smaller. That is an honest result, not a broken tool. Use it when the PDF has photos or scans and you need a smaller file for email.
## Turn pages into images, or images into a PDF
&lt;a href="https://kepttools.com/pdf-to-jpg" rel="noopener noreferrer"&gt;PDF to JPG&lt;/a&gt; exports pages as JPG. Pick all pages or a range, choose quality, preview, then download one image or a ZIP. After this step the page is a picture, so the text is no longer selectable.
&lt;a href="https://kepttools.com/jpg-to-pdf" rel="noopener noreferrer"&gt;JPG to PDF&lt;/a&gt; does the other direction. Add JPG, PNG, or WebP images, reorder them, then set page size (A4, Letter, or fit), orientation, and margins. Fit inside keeps the photo proportions. Stretch can distort them.
## What stays on your machine
The file is read in the browser and the result is downloaded from there. I do not receive the PDF.
If you try one, tell me whether you needed merge, split, or compress first.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>webdev</category>
      <category>kepttools</category>
      <category>javascript</category>
      <category>pdf</category>
    </item>
    <item>
      <title>Unix Timestamp Converter That Runs in Your Browser</title>
      <dc:creator>Ameen Usman</dc:creator>
      <pubDate>Sat, 24 Oct 2026 07:48:52 +0000</pubDate>
      <link>https://dev.to/ameenusman/unix-timestamp-converter-that-runs-in-your-browser-47i9</link>
      <guid>https://dev.to/ameenusman/unix-timestamp-converter-that-runs-in-your-browser-47i9</guid>
      <description>&lt;p&gt;I still paste Unix timestamps into random converters more often than I want to admit.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;created_at: 1729000000&lt;/code&gt;&lt;br&gt;&lt;br&gt;
&lt;code&gt;exp: 1735689600&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The number is fine. The worry is the same every time: does this page upload what I paste?&lt;/p&gt;

&lt;h2&gt;
  
  
  What I needed
&lt;/h2&gt;

&lt;p&gt;A tiny converter that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;accepts epoch &lt;strong&gt;seconds&lt;/strong&gt; or &lt;strong&gt;milliseconds&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;shows &lt;strong&gt;UTC (ISO)&lt;/strong&gt; and &lt;strong&gt;local&lt;/strong&gt; time&lt;/li&gt;
&lt;li&gt;lets me copy the ISO string&lt;/li&gt;
&lt;li&gt;has a &lt;strong&gt;Use now&lt;/strong&gt; button for the current timestamp&lt;/li&gt;
&lt;li&gt;does the work &lt;strong&gt;in the browser&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I added one to KeptTools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;👉 &lt;a href="https://kepttools.com/timestamp" rel="noopener noreferrer"&gt;Unix Timestamp Converter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
text
1729000000
→ 2024-10-15T13:46:40.000Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>productivity</category>
      <category>kepttools</category>
    </item>
    <item>
      <title>Cron Generator Online: Build Expressions in Your Browser</title>
      <dc:creator>Ameen Usman</dc:creator>
      <pubDate>Tue, 20 Oct 2026 15:45:13 +0000</pubDate>
      <link>https://dev.to/ameenusman/cron-generator-online-build-expressions-in-your-browser-239n</link>
      <guid>https://dev.to/ameenusman/cron-generator-online-build-expressions-in-your-browser-239n</guid>
      <description>&lt;h1&gt;
  
  
  Cron Generator Online: Build Expressions in Your Browser
&lt;/h1&gt;

&lt;p&gt;Cron schedules are simple… until you’re staring at five fields and guessing.&lt;/p&gt;

&lt;p&gt;I built a &lt;strong&gt;Cron Expression Generator&lt;/strong&gt; inside &lt;a href="https://kepttools.com" rel="noopener noreferrer"&gt;KeptTools&lt;/a&gt; so you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pick a preset (hourly, daily, weekdays 9am, etc.)&lt;/li&gt;
&lt;li&gt;edit minute / hour / day / month / weekday&lt;/li&gt;
&lt;li&gt;read a plain-English summary&lt;/li&gt;
&lt;li&gt;copy the expression into crontab&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Preset: &lt;strong&gt;Weekdays 9am&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Expression: &lt;code&gt;0 9 * * 1-5&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Browser-only
&lt;/h2&gt;

&lt;p&gt;Processing stays in your browser. Nothing is uploaded.&lt;/p&gt;

&lt;p&gt;Try it: &lt;a href="https://kepttools.com/cron-generator" rel="noopener noreferrer"&gt;https://kepttools.com/cron-generator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Full toolkit (JSON, JWT, regex, and more): &lt;a href="https://kepttools.com" rel="noopener noreferrer"&gt;https://kepttools.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you use cron a lot, what preset do you want added next?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>devops</category>
      <category>linux</category>
      <category>kepttools</category>
    </item>
    <item>
      <title>I built KeptTools so your JWT, SQL, and Base64 never leave the browser</title>
      <dc:creator>Ameen Usman</dc:creator>
      <pubDate>Mon, 19 Oct 2026 15:50:32 +0000</pubDate>
      <link>https://dev.to/ameenusman/i-built-kepttools-so-your-jwt-sql-and-base64-never-leave-the-browser-5cho</link>
      <guid>https://dev.to/ameenusman/i-built-kepttools-so-your-jwt-sql-and-base64-never-leave-the-browser-5cho</guid>
      <description>&lt;h1&gt;
  
  
  I built KeptTools so your JWT, SQL, and Base64 never leave the browser
&lt;/h1&gt;

&lt;p&gt;Most “free online tools” are convenient until you paste something you shouldn’t.&lt;/p&gt;

&lt;p&gt;A JWT from staging.&lt;br&gt;&lt;br&gt;
A SQL dump with real emails.&lt;br&gt;&lt;br&gt;
A Base64 string that still holds a secret.&lt;/p&gt;

&lt;p&gt;If that page uploads your paste, you’ve already shared more than you meant to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;I kept needing small utilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;format / validate JSON
&lt;/li&gt;
&lt;li&gt;format SQL
&lt;/li&gt;
&lt;li&gt;decode a JWT
&lt;/li&gt;
&lt;li&gt;encode / decode Base64
&lt;/li&gt;
&lt;li&gt;generate hashes
&lt;/li&gt;
&lt;li&gt;tweak images
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…but I didn’t want to trust random sites with the paste.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://kepttools.com" rel="noopener noreferrer"&gt;KeptTools&lt;/a&gt;&lt;/strong&gt; — a set of browser-only developer tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Processing runs in your browser.&lt;/p&gt;

&lt;p&gt;Nothing you paste is uploaded for processing.&lt;/p&gt;

&lt;p&gt;On the more sensitive pages (JWT, Base64, hash, password generator):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no analytics
&lt;/li&gt;
&lt;li&gt;no ads
&lt;/li&gt;
&lt;li&gt;no third-party scripts
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tip: open &lt;strong&gt;DevTools → Network&lt;/strong&gt; while you use those pages. You should not see a request carrying your paste.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s included
&lt;/h2&gt;

&lt;p&gt;KeptTools currently covers everyday work like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON formatter / converter
&lt;/li&gt;
&lt;li&gt;SQL formatter
&lt;/li&gt;
&lt;li&gt;JWT decoder
&lt;/li&gt;
&lt;li&gt;Base64 encode / decode
&lt;/li&gt;
&lt;li&gt;Hash generator (MD5, SHA-1, SHA-256, SHA-512)
&lt;/li&gt;
&lt;li&gt;URL &amp;amp; HTML encode / decode
&lt;/li&gt;
&lt;li&gt;Regex tester
&lt;/li&gt;
&lt;li&gt;Image convert / compress / crop / background tools
&lt;/li&gt;
&lt;li&gt;and more
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All free to use: &lt;a href="https://kepttools.com" rel="noopener noreferrer"&gt;https://kepttools.com&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I’m sharing this
&lt;/h2&gt;

&lt;p&gt;I’m not claiming this replaces your local CLI or IDE.&lt;/p&gt;

&lt;p&gt;I’m claiming something simpler:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If a tool can run in the browser, it shouldn’t need your paste on a server.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you try it, I’d love blunt feedback:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is the privacy angle clear on first visit?
&lt;/li&gt;
&lt;li&gt;Which tool would you use weekly?
&lt;/li&gt;
&lt;li&gt;What’s missing?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thanks for reading — and if you try it, open Network and verify it yourself.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>privacy</category>
      <category>javascript</category>
      <category>kepttools</category>
    </item>
    <item>
      <title>How to prove an online JWT tool isn’t stealing your token</title>
      <dc:creator>Ameen Usman</dc:creator>
      <pubDate>Sun, 18 Oct 2026 15:28:56 +0000</pubDate>
      <link>https://dev.to/ameenusman/how-to-prove-an-online-jwt-tool-isnt-stealing-your-token-72g</link>
      <guid>https://dev.to/ameenusman/how-to-prove-an-online-jwt-tool-isnt-stealing-your-token-72g</guid>
      <description>&lt;p&gt;Most free JWT decoders ask you to paste a token.&lt;/p&gt;

&lt;p&gt;That’s a trust problem. Tokens can include emails, roles, and session data. If the page uploads it, you just gave secrets to someone else’s server.&lt;/p&gt;

&lt;p&gt;Here’s the fastest way to check any tool:&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the Network tab
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open DevTools (F12)&lt;/li&gt;
&lt;li&gt;Open &lt;strong&gt;Network&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Paste a &lt;strong&gt;sample&lt;/strong&gt; token&lt;/li&gt;
&lt;li&gt;Watch for requests&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If decoding fires a request with your token, leave.&lt;/p&gt;

&lt;p&gt;A privacy claim is marketing. An empty Network tab is evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try a local decoder
&lt;/h2&gt;

&lt;p&gt;I built a browser-only JWT decoder here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kepttools.com/jwt-decoder" rel="noopener noreferrer"&gt;https://kepttools.com/jwt-decoder&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No account. Nothing uploaded. Open Network while you decode and confirm it yourself.&lt;/p&gt;

&lt;p&gt;If you try it, tell me bluntly: is the privacy story clear in 5 seconds?&lt;/p&gt;

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