<?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: Zhang Enquan</title>
    <description>The latest articles on DEV Community by Zhang Enquan (@devenquan).</description>
    <link>https://dev.to/devenquan</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%2F4106337%2F2ae76bea-33aa-47de-a025-1235317f1001.png</url>
      <title>DEV Community: Zhang Enquan</title>
      <link>https://dev.to/devenquan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devenquan"/>
    <language>en</language>
    <item>
      <title>Cloudflare Just Shipped Post-Quantum at Internet Scale. Do You Need to Change Your Tools?</title>
      <dc:creator>Zhang Enquan</dc:creator>
      <pubDate>Mon, 14 Sep 2026 13:19:23 +0000</pubDate>
      <link>https://dev.to/devenquan/cloudflare-just-shipped-post-quantum-at-internet-scale-do-you-need-to-change-your-tools-p38</link>
      <guid>https://dev.to/devenquan/cloudflare-just-shipped-post-quantum-at-internet-scale-do-you-need-to-change-your-tools-p38</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fidv050zl6y95mcq7a49p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fidv050zl6y95mcq7a49p.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;br&gt;
On September 8, 2026, &lt;a href="https://blog.cloudflare.com/automatic-key-exchange-for-origins/" rel="noopener noreferrer"&gt;Cloudflare flipped the switch on post-quantum TLS origin handshakes&lt;/a&gt; across its edge. Two days later, on September 10, &lt;a href="https://blog.cloudflare.com/post-quantum-dnssec-1111/" rel="noopener noreferrer"&gt;1.1.1.1 enabled post-quantum DNSSEC&lt;/a&gt; powered by &lt;a href="https://csrc.nist.gov/pubs/fips/204/final" rel="noopener noreferrer"&gt;NIST's ML-DSA-44 algorithm&lt;/a&gt;. The numbers are not small: the TLS deployment alone now covers roughly &lt;strong&gt;45 billion daily connections&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you saw that headline and immediately opened your code editor to grep for &lt;code&gt;md5&lt;/code&gt; and &lt;code&gt;sha256&lt;/code&gt; — this post is for you. If you saw it and shrugged because you only use hashing for "checksumming" — this post is &lt;em&gt;also&lt;/em&gt; for you.&lt;/p&gt;

&lt;p&gt;If that's you — this is roughly what your terminal looks like right now:&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="nv"&gt;$ &lt;/span&gt;rg &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"md5|sha256"&lt;/span&gt; src/ &lt;span class="nt"&gt;--color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;always | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
src/main.c:1:    fprintf&lt;span class="o"&gt;(&lt;/span&gt;stderr, &lt;span class="s2"&gt;"ERROR: Unsupported hash function: %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;, hash_type&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
src/CMakeLists.txt:21:    target_compile_options&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; PRIVATE
                                                     &lt;span class="nt"&gt;-Werror&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deprecated-declarations&lt;span class="o"&gt;)&lt;/span&gt;
src/Rust/Cargo.toml:45:    version &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.1.1"&lt;/span&gt;
src/Rust/Cargo.toml:46:    md5    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.10.0"&lt;/span&gt;
src/Rust/Cargo.toml:47:    sha256 &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.9.0"&lt;/span&gt;
src/Rust/src/lib.rs:120:   &lt;span class="nb"&gt;let hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; md5::compute&lt;span class="o"&gt;(&lt;/span&gt;data&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
src/Rust/src/lib.rs:11:    &lt;span class="nb"&gt;let hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; sha2::Sha256::digest&lt;span class="o"&gt;(&lt;/span&gt;data&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
src/scripts/build.sh:15:    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; openssl   &amp;amp;&amp;gt; /dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
&lt;/span&gt;src/scripts/build.sh:15:    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nb"&gt;sha256sum&lt;/span&gt; &amp;amp;&amp;gt; /dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
&lt;/span&gt;src/scripts/build.sh:12:        &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ERROR: Neither openssl nor sha256sum found!"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
src/scripts/util.sh:35:    &lt;span class="nb"&gt;md5sum&lt;/span&gt;    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt;
src/scripts/util.sh:36:    &lt;span class="nb"&gt;sha256sum&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(That's not a snippet from a real repo, but the panic it captures is.)&lt;/p&gt;

&lt;p&gt;The short version: most of the tools you already use are fine. The panic comes from confusing &lt;strong&gt;hashes&lt;/strong&gt; with &lt;strong&gt;encryption&lt;/strong&gt;, and from using &lt;strong&gt;encryption&lt;/strong&gt; when you actually needed a &lt;strong&gt;checksum&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Let's untangle that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The panic that doesn't help
&lt;/h2&gt;

&lt;p&gt;Here's what happened in a few Discord servers I read this week:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Should I migrate from MD5 to SHA-256? Grover's algorithm makes SHA-256 only 128-bit secure anyway. Should I just go to SHA-3?"&lt;/p&gt;

&lt;p&gt;"Should I rotate all my UUIDs to UUIDv7?"&lt;/p&gt;

&lt;p&gt;"Is base64 still safe?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These are reasonable questions if you don't know the answer. They are also three different categories of mistake.&lt;/p&gt;

&lt;p&gt;MD5, SHA-1, and SHA-256 are &lt;strong&gt;hash functions&lt;/strong&gt;. They produce a fixed-size fingerprint from any input. The reason they exist is not to keep secrets — it's to verify that data didn't change in transit. A 1GB ISO file you downloaded can be checksummed with any of them; the question is "did the bytes arrive intact", not "can someone read the bytes".&lt;/p&gt;

&lt;p&gt;Base64 is &lt;strong&gt;an encoding&lt;/strong&gt;, not encryption. It exists so that binary data can travel through channels (JSON, URL paths, email) that only support text. There is no key. There is no "secure" mode. Anyone who tells you base64 is "encrypted" is selling you snake oil.&lt;/p&gt;

&lt;p&gt;UUID v4 is a &lt;strong&gt;random identifier&lt;/strong&gt;, 122 bits of randomness. It's not a hash, not an encryption — it's a label. Whether quantum computers threaten it depends on whether you used a cryptographic random number generator (good) or &lt;code&gt;Math.random()&lt;/code&gt; (you have bigger problems than Grover).&lt;/p&gt;

&lt;p&gt;So when Cloudflare ships post-quantum TLS, here's what's actually happening at the protocol level:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The TLS handshake negotiates session keys using a key exchange algorithm that a quantum computer cannot efficiently break.&lt;/li&gt;
&lt;li&gt;Everything &lt;strong&gt;inside&lt;/strong&gt; that TLS tunnel — your HTTP traffic, your JSON, your base64-encoded blobs — is still protected by classical symmetric ciphers (AES-256-GCM, ChaCha20) and authenticated by classical hashes (SHA-256, SHA-384).&lt;/li&gt;
&lt;li&gt;The risk model is "harvest now, decrypt later": an adversary records encrypted traffic today, waits for a cryptographically-relevant quantum computer (CRQC), then decrypts it. That's the threat post-quantum key exchange is closing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your MD5 checksum tool does not enter this picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each tool is actually for
&lt;/h2&gt;

&lt;p&gt;Since I built &lt;a href="https://aisubtools.xyz/" rel="noopener noreferrer"&gt;AI Subtools&lt;/a&gt; — a no-fluff browser-side toolbox — here's the honest guide. Same tool, different intent:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Threat model&lt;/th&gt;
&lt;th&gt;Replace with PQC?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CRC32 Checksum Generator&lt;/td&gt;
&lt;td&gt;Fast integrity check on small files / packets&lt;/td&gt;
&lt;td&gt;Accidental corruption&lt;/td&gt;
&lt;td&gt;No — error-detection, not security&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MD5 Hash Generator&lt;/td&gt;
&lt;td&gt;Legacy checksum, fingerprint for deduplication&lt;/td&gt;
&lt;td&gt;Accidental corruption&lt;/td&gt;
&lt;td&gt;No, for the same reason&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHA256 Hash Calculator&lt;/td&gt;
&lt;td&gt;Strong fingerprint for file integrity / commit hashes&lt;/td&gt;
&lt;td&gt;Accidental corruption&lt;/td&gt;
&lt;td&gt;Optional — SHA-256 is fine for non-adversarial checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Base64 Encoder/Decoder&lt;/td&gt;
&lt;td&gt;Binary ↔ text transport&lt;/td&gt;
&lt;td&gt;None — it's encoding&lt;/td&gt;
&lt;td&gt;No — never was security&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URL Encoder/Decoder&lt;/td&gt;
&lt;td&gt;Make strings safe for URLs&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UUID v4 Generator&lt;/td&gt;
&lt;td&gt;Random 122-bit ID&lt;/td&gt;
&lt;td&gt;Collision, predictability&lt;/td&gt;
&lt;td&gt;No, if you use &lt;code&gt;crypto.getRandomValues&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Password Strength Check&lt;/td&gt;
&lt;td&gt;Estimate entropy of a passphrase&lt;/td&gt;
&lt;td&gt;Dictionary attack&lt;/td&gt;
&lt;td&gt;The &lt;em&gt;length&lt;/em&gt; matters, not the hash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Random Password Generator&lt;/td&gt;
&lt;td&gt;High-entropy secrets&lt;/td&gt;
&lt;td&gt;Predictability&lt;/td&gt;
&lt;td&gt;No, if CSPRNG-backed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encrypt/Decrypt (AES-256)&lt;/td&gt;
&lt;td&gt;Symmetric encryption, browser-side&lt;/td&gt;
&lt;td&gt;Adversary steals ciphertext&lt;/td&gt;
&lt;td&gt;No — AES-256 is post-quantum-safe&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row is the only one where a quantum computer would matter at all, and even then: &lt;a href="https://csrc.nist.gov/Pubs/ir/8105/Final" rel="noopener noreferrer"&gt;Grover's algorithm gives a quadratic speedup, which means AES-256 effectively becomes AES-128&lt;/a&gt; — still considered computationally infeasible to break. NIST standardized it for exactly that reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  So when &lt;em&gt;do&lt;/em&gt; you actually need post-quantum?
&lt;/h2&gt;

&lt;p&gt;There are exactly three situations where PQC migration matters for the average developer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You run a TLS server.&lt;/strong&gt; Migrate your server library to support hybrid Kyber/X25519 key exchange (OpenSSL 3.5+, BoringSSL, rustls 0.23+). Cloudflare already did this for you if your users connect via their edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You sign software or firmware updates.&lt;/strong&gt; Classic ECDSA signatures are quantum-broken. Migrate to ML-DSA-44 / Dilithium for new releases; consider re-signing historical binaries if your threat model includes harvest-now-decrypt-later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You embed long-lived secrets in firmware or code signing certificates.&lt;/strong&gt; Anything with a 10+ year shelf life needs a PQC plan now.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything else — your file checksums, your base64-encoded JSON payloads, your UUIDs, your SHA-256 commit hashes — is &lt;strong&gt;already fine&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple decision tree
&lt;/h2&gt;

&lt;p&gt;When you reach for one of these tools, ask one question: &lt;strong&gt;am I protecting against bit-flips, or against an adversary?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bit-flips only&lt;/strong&gt; (corrupted download, broken pipe, accidental edit) → CRC32, MD5, SHA-256. Any of them. CRC32 is fastest; SHA-256 is most defensible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adversary who can read but not modify&lt;/strong&gt; → AES-256 encryption. Done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adversary who can read and modify&lt;/strong&gt; → authenticated encryption (AES-GCM) or HMAC over the ciphertext.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adversary with a quantum computer who recorded your traffic years ago&lt;/strong&gt; → PQC key exchange at the protocol layer (already handled by Cloudflare, AWS, GCP, Fastly, Akamai in 2026).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. No framework. No vendor lock-in.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Q: Is MD5 broken?&lt;/strong&gt;&lt;br&gt;
Yes, but not for checksumming. Collision attacks against MD5 mean you can craft two different files with the same MD5 — useful for forging signatures, useless for catching accidental corruption. Use MD5 for cache keys, deduplication, and legacy file verification. Don't use it for digital signatures — that hasn't been safe since 2008.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Should I switch to SHA-3 for file integrity?&lt;/strong&gt;&lt;br&gt;
No reason to. SHA-256 is fine for non-adversarial integrity. The actual quantum risk lands on &lt;strong&gt;digital signatures&lt;/strong&gt; and &lt;strong&gt;key exchange&lt;/strong&gt;, not on file checksums.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is base64 encryption?&lt;/strong&gt;&lt;br&gt;
No. It is an encoding. There is no key. Anyone — including this page's CSS — can decode it. If someone tells you they "encrypted it with base64", they did not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is UUID v4 quantum-safe?&lt;/strong&gt;&lt;br&gt;
UUID v4 is 122 bits from a CSPRNG. Brute-forcing 122 bits is computationally infeasible even with Grover's algorithm — you'd need ~2^61 operations, which is more atoms than in a kilogram of lead. As long as your UUID generator uses a cryptographic random source, you're fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What about AES-256?&lt;/strong&gt;&lt;br&gt;
Post-quantum-safe by design. Grover halves effective key length to 128 bits. NIST confirmed AES-256 is acceptable for top-secret data through the post-quantum era.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;Cloudflare's PQC deployment this week was an infrastructure milestone, not a consumer panic trigger. The right response is to make sure your &lt;strong&gt;TLS library&lt;/strong&gt; supports hybrid post-quantum key exchange — not to throw out your hash functions.&lt;/p&gt;

&lt;p&gt;If you're picking a tool for a one-off task today, pick the one that matches the threat model, not the trend.&lt;/p&gt;

&lt;p&gt;— &lt;em&gt;Built and maintained at &lt;a href="https://aisubtools.xyz/" rel="noopener noreferrer"&gt;aisubtools.xyz&lt;/a&gt; — 40+ free browser-side tools for developers and creators.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>postquantum</category>
      <category>cloudflare</category>
      <category>security</category>
    </item>
    <item>
      <title>Open-sourcing my dev tools as plain HTML files (no npm, no build)</title>
      <dc:creator>Zhang Enquan</dc:creator>
      <pubDate>Thu, 10 Sep 2026 07:48:49 +0000</pubDate>
      <link>https://dev.to/devenquan/open-sourcing-my-dev-tools-as-plain-html-files-no-npm-no-build-3ga7</link>
      <guid>https://dev.to/devenquan/open-sourcing-my-dev-tools-as-plain-html-files-no-npm-no-build-3ga7</guid>
      <description>&lt;p&gt;&lt;br&gt;
```Last month I shipped three developer utilities — a Base64 encoder, a Unix&lt;br&gt;
timestamp converter, and a CSS minifier. Each one runs100% in your browser.&lt;br&gt;
No servers, no uploads, no analytics, no tracking.&lt;br&gt;
\&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxz3a4k0dhyu11dp9yxrn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxz3a4k0dhyu11dp9yxrn.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Yesterday I open-sourced them on GitHub. This post is the story of &lt;em&gt;why&lt;/em&gt; I&lt;br&gt;
built them this way and &lt;em&gt;why&lt;/em&gt; I open-sourced them at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with "free online tools"
&lt;/h2&gt;

&lt;p&gt;Most "free online tools" secretly upload your input to a server, process it&lt;br&gt;
there, and hope nobody keeps a copy. That is a terrible default.&lt;/p&gt;

&lt;p&gt;Try this experiment: paste &lt;code&gt;你好世界 🌍&lt;/code&gt; into the most popular online Base64&lt;br&gt;
encoder. About half of them will return &lt;code&gt;å¥½ä¸ç•Œ&lt;/code&gt; — broken output that no&lt;br&gt;
longer decodes back to what you typed. Why? Because most sites treat text as&lt;br&gt;
Latin-1 by accident.&lt;/p&gt;

&lt;p&gt;Even when they work correctly, you're trusting that the site:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;won't log your input&lt;/li&gt;
&lt;li&gt;won't sell it to a third party&lt;/li&gt;
&lt;li&gt;won't get breached next year&lt;/li&gt;
&lt;li&gt;won't disappear and take your workflow with it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open source fixes all four.&lt;/p&gt;

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

&lt;p&gt;I run &lt;a href="https://aisubtools.xyz" rel="noopener noreferrer"&gt;aisubtools.xyz&lt;/a&gt; — a collection of free online&lt;br&gt;
utilities where nothing ever leaves your browser. Yesterday I extracted&lt;br&gt;
three of them into a standalone open-source repo:&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;&lt;a href="https://github.com/enquannacc/aisubtools-browser-tools" rel="noopener noreferrer"&gt;github.com/enquannacc/aisubtools-browser-tools&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Base64 encoder/decoder&lt;/td&gt;
&lt;td&gt;Handles UTF-8 correctly via &lt;code&gt;TextEncoder&lt;/code&gt; / &lt;code&gt;TextDecoder&lt;/code&gt;. No mojibake.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unix timestamp converter&lt;/td&gt;
&lt;td&gt;Bidirectional conversion with local and UTC time, plus a live clock.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS minifier&lt;/td&gt;
&lt;td&gt;Regex-based whitespace stripper with before/after size savings.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;MIT licensed. No &lt;code&gt;npm install&lt;/code&gt;. No build step. No dependencies. Open the file,&lt;br&gt;
use it, close the tab. Done.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture: one HTML file per tool
&lt;/h2&gt;

&lt;p&gt;Every tool in the repo is a single self-contained HTML file. Here's the&lt;br&gt;
pattern I used across all three:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
html
&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang="en"&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;meta charset="utf-8"&amp;gt;
  &amp;lt;title&amp;gt;Base64 Encode / Decode — Browser Tool&amp;lt;/title&amp;gt;
  &amp;lt;style&amp;gt;
    /* ~100 lines of CSS — nothing fancy, system fonts, CSS variables */
 &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;h1&amp;gt;Base64 Encode / Decode&amp;lt;/h1&amp;gt;
  &amp;lt;p&amp;gt;Runs 100% in your browser — nothing is uploaded anywhere.&amp;lt;/p&amp;gt;
  &amp;lt;textarea id="input" placeholder="Type or paste here"&amp;gt;&amp;lt;/textarea&amp;gt;
  &amp;lt;button id="convert"&amp;gt;Convert&amp;lt;/button&amp;gt;
  &amp;lt;textarea id="output" readonly&amp;gt;&amp;lt;/textarea&amp;gt;
  &amp;lt;script&amp;gt;
    const input = document.getElementById('input');
    const output = document.getElementById('output');
    const convert = document.getElementById('convert');

    let mode = 'encode';

    function utf8ToBase64(str) {
      return btoa(String.fromCharCode(
        ...new TextEncoder().encode(str)
      ));
    }

    function base64ToUtf8(b64) {
      const bytes = Uint8Array.from(atob(b64), c =&amp;gt; c.charCodeAt(0));
      return new TextDecoder().decode(bytes);
    }

    convert.addEventListener('click', () =&amp;gt; {
      try {
        output.value = mode === 'encode'
          ? utf8ToBase64(input.value)
          : base64ToUtf8(input.value.trim());
      } catch (e) {
        output.value = 'Error: ' + e.message;
      }
    });
  &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;




&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Why %20 Keeps Appearing in Your URLs: A Practical Guide to URL Encoding</title>
      <dc:creator>Zhang Enquan</dc:creator>
      <pubDate>Sun, 06 Sep 2026 14:19:43 +0000</pubDate>
      <link>https://dev.to/devenquan/why-20-keeps-appearing-in-your-urls-a-practical-guide-to-url-encoding-4b4l</link>
      <guid>https://dev.to/devenquan/why-20-keeps-appearing-in-your-urls-a-practical-guide-to-url-encoding-4b4l</guid>
      <description>&lt;p&gt;You paste a URL into your browser, hit Enter, and suddenly it's full of &lt;code&gt;%20&lt;/code&gt;, &lt;code&gt;%3F&lt;/code&gt;, and &lt;code&gt;%26&lt;/code&gt;. Or worse — your API call fails because a query parameter contains a space or an &lt;code&gt;&amp;amp;&lt;/code&gt; you didn't encode.&lt;/p&gt;

&lt;p&gt;URL encoding trips up everyone eventually. The good news: it's much simpler than it looks. This is a practical guide to reading percent-encoded URLs, knowing when encoding happens automatically, and fixing the bugs it causes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why URL encoding exists at all
&lt;/h2&gt;

&lt;p&gt;A URL has to be plain ASCII — but the real world isn't. Spaces, emoji, Chinese characters, ampersands... all of them need to travel through a URL &lt;strong&gt;without breaking its structure&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The solution: any character that isn't URL-safe gets replaced with a &lt;code&gt;%&lt;/code&gt; followed by its hexadecimal byte value.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A space becomes &lt;code&gt;%20&lt;/code&gt; (hex 20 = 32 = space in ASCII)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;amp;&lt;/code&gt; becomes &lt;code&gt;%26&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;?&lt;/code&gt; becomes &lt;code&gt;%3F&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;?&lt;/code&gt; and &lt;code&gt;&amp;amp;&lt;/code&gt; are the interesting ones — because those characters &lt;em&gt;do&lt;/em&gt; have a special job in URLs. That's the key to understanding the whole system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two categories you must not confuse
&lt;/h2&gt;

&lt;p&gt;Every character in a URL falls into one of two buckets:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Reserved characters&lt;/strong&gt; — they are part of the URL's &lt;em&gt;structure&lt;/em&gt;:&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These have meaning: &lt;code&gt;?&lt;/code&gt; starts the query string, &lt;code&gt;&amp;amp;&lt;/code&gt; separates parameters, &lt;code&gt;#&lt;/code&gt; starts the fragment. If your &lt;em&gt;data&lt;/em&gt; contains one of these characters, it &lt;strong&gt;must&lt;/strong&gt; be encoded — otherwise the URL's structure gets corrupted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Unsafe / non-ASCII characters&lt;/strong&gt; — spaces, control characters, non-Latin text, emoji. They must also be encoded because URLs are transmitted as ASCII.&lt;/p&gt;

&lt;p&gt;Everything else (letters, digits, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;_&lt;/code&gt;, &lt;code&gt;.&lt;/code&gt;, &lt;code&gt;~&lt;/code&gt;) passes through untouched.&lt;/p&gt;

&lt;p&gt;That's the entire mental model. When you see &lt;code&gt;%26&lt;/code&gt; in a URL, it doesn't mean "the website hates you" — it means the original data contained a literal &lt;code&gt;&amp;amp;&lt;/code&gt; that had to be protected so it wouldn't be mistaken for a parameter separator.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cheat sheet you'll actually use
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Encoded&lt;/th&gt;
&lt;th&gt;Character&lt;/th&gt;
&lt;th&gt;Where you'll meet it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;space&lt;/td&gt;
&lt;td&gt;Search queries, file names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%2F&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Paths inside query params&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%3F&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A literal &lt;code&gt;?&lt;/code&gt; in data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%26&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Values containing &lt;code&gt;&amp;amp;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%3D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Values containing &lt;code&gt;=&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%25&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The classic double-encode bug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%2B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Legacy form encoding of spaces&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;About &lt;code&gt;%25&lt;/code&gt;:&lt;/strong&gt; this is my favorite interview question. If you see &lt;code&gt;%2520&lt;/code&gt; in a URL, the original data was &lt;code&gt;%20&lt;/code&gt; — someone encoded an &lt;em&gt;already encoded&lt;/em&gt; string. The &lt;code&gt;%&lt;/code&gt; became &lt;code&gt;%25&lt;/code&gt;, leaving &lt;code&gt;20&lt;/code&gt; intact. Double-encoding bugs almost always trace back to a URL being encoded twice by two different layers (your code + a library).&lt;/p&gt;

&lt;h2&gt;
  
  
  When encoding happens automatically (and when it doesn't)
&lt;/h2&gt;

&lt;p&gt;This is where bugs sneak in. The browser encodes some things for you, but not everything, and not everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The browser address bar is forgiving.&lt;/strong&gt; Type &lt;code&gt;https://example.com/search?q=hello world&lt;/code&gt; and the browser will quietly encode the space for you. This hides bugs during manual testing that blow up when the same URL is requested programmatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;fetch()&lt;/code&gt; and &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt; are NOT forgiving in the same way.&lt;/strong&gt; Spaces and non-ASCII characters in a query string can produce malformed requests or, worse, silently work in one browser and fail in another.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Form submissions (&lt;code&gt;GET&lt;/code&gt; forms) encode spaces as &lt;code&gt;+&lt;/code&gt;.&lt;/strong&gt; That's why &lt;code&gt;hello+world&lt;/code&gt; and &lt;code&gt;hello%20world&lt;/code&gt; can both mean "hello world", depending on which layer produced them. This is also why decoding a form-encoded string with the wrong decoder gives you literal &lt;code&gt;+&lt;/code&gt; signs scattered through your data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The JavaScript functions, once and for all
&lt;/h2&gt;

&lt;p&gt;In JavaScript you have four tools, and picking the wrong one is a rite of passage:&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;// encodeURIComponent — for a VALUE going into a query string&lt;/span&gt;
&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fish &amp;amp; chips? yes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// "fish%20%26%20chips%3F%20yes"&lt;/span&gt;
&lt;span class="c1"&gt;// ✅ Encodes &amp;amp; and ? — the structure is protected&lt;/span&gt;

&lt;span class="c1"&gt;// encodeURI — for a WHOLE URL that's already assembled&lt;/span&gt;
&lt;span class="nf"&gt;encodeURI&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://example.com/search?q=hello world&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// "https://example.com/search?q=hello%20world"&lt;/span&gt;
&lt;span class="c1"&gt;// ✅ Leaves :// and ? alone, encodes the space&lt;/span&gt;

&lt;span class="c1"&gt;// decodeURIComponent / decodeURI — the reversals&lt;/span&gt;
&lt;span class="nf"&gt;decodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fish%20%26%20chips&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// "fish &amp;amp; chips"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule of thumb:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Encoding one value&lt;/strong&gt; (about to insert it into a query string)? → &lt;code&gt;encodeURIComponent&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encoding a complete URL&lt;/strong&gt; that's already well-formed except for stray spaces/unicode? → &lt;code&gt;encodeURI&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mistake I see most often: using &lt;code&gt;encodeURI&lt;/code&gt; on a value. It leaves &lt;code&gt;&amp;amp;&lt;/code&gt; and &lt;code&gt;?&lt;/code&gt; untouched, so &lt;code&gt;fish &amp;amp; chips&lt;/code&gt; slips into the URL as-is and silently truncates your query string at the &lt;code&gt;&amp;amp;&lt;/code&gt;.&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;// ❌ Broken: value contains &amp;amp; and ?&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/q?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;encodeURI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fish &amp;amp; chips?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Correct&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/q?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fish &amp;amp; chips?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Three real bugs you'll eventually meet
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The truncated query string.&lt;/strong&gt; A search for &lt;code&gt;Tom &amp;amp; Jerry&lt;/code&gt; becomes &lt;code&gt;?q=Tom&lt;/code&gt; and everything after &lt;code&gt;&amp;amp;&lt;/code&gt; becomes a new (empty) parameter. The page loads, the search just returns wrong results. Maddening until you look at the Network tab.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The double-encoded redirect.&lt;/strong&gt; You pass a &lt;code&gt;?next=&lt;/code&gt; parameter containing a full URL. Some middleware encodes it again. The redirect target now contains &lt;code&gt;%253A&lt;/code&gt; instead of &lt;code&gt;%3A&lt;/code&gt;. The fix is to decode exactly once at each layer, and to be disciplined about &lt;em&gt;which&lt;/em&gt; layer owns encoding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The emoji that kills your sitemap.&lt;/strong&gt; Non-ASCII characters in URLs are legal (via encoding), but if your CMS or export script writes raw UTF-8 into an XML sitemap without encoding, Google may start complaining. Always encode before writing URLs into structured files.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you just need to check one string quickly
&lt;/h2&gt;

&lt;p&gt;For debugging, I keep a browser-side &lt;a href="https://aisubtools.xyz/url-encoder-decoder-online-tool/" rel="noopener noreferrer"&gt;URL encoder/decoder&lt;/a&gt; bookmarked — paste the string, see it encoded and decoded side by side, copy whichever form you need. It runs client-side, so tokens and internal URLs never leave the browser. Pair it with &lt;code&gt;console.log&lt;/code&gt; and the Network tab and you can diagnose 95% of encoding bugs in a minute.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;%XX&lt;/code&gt; is just a character's hex byte value, prefixed with &lt;code&gt;%&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Reserved characters (&lt;code&gt;&amp;amp; ? = / #&lt;/code&gt;) must be encoded when they're &lt;em&gt;data&lt;/em&gt;, not structure&lt;/li&gt;
&lt;li&gt;The address bar hides encoding bugs; test with &lt;code&gt;fetch&lt;/code&gt; or curl&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;encodeURIComponent&lt;/code&gt; for values, &lt;code&gt;encodeURI&lt;/code&gt; for whole URLs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;%25&lt;/code&gt; appearing where &lt;code&gt;%20&lt;/code&gt; should be means something double-encoded&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;URL encoding stops being scary the moment you stop reading &lt;code&gt;%3F&lt;/code&gt; as noise and start reading it as "that's a literal question mark in the data". Once that clicks, every encoded URL you see becomes just... a string with a hat on.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Questions or war stories about encoding bugs? &lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tutorial</category>
      <category>javascript</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I Built 35+ Browser-Side Tools Because I Was Tired of Uploading My Data to Random Servers</title>
      <dc:creator>Zhang Enquan</dc:creator>
      <pubDate>Wed, 02 Sep 2026 14:50:11 +0000</pubDate>
      <link>https://dev.to/devenquan/i-built-35-browser-side-tools-because-i-was-tired-of-uploading-my-data-to-random-servers-4h9n</link>
      <guid>https://dev.to/devenquan/i-built-35-browser-side-tools-because-i-was-tired-of-uploading-my-data-to-random-servers-4h9n</guid>
      <description>&lt;p&gt;Every time I needed to format a JSON payload or compress an image, I found myself on some random tool site with three problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;My data went to their server.&lt;/strong&gt; Paste a password or an API token into a "hash generator" and you're just trusting that site not to log it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ads everywhere.&lt;/strong&gt; The tool was buried under banners, and half the buttons were ad-click traps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sign-up walls.&lt;/strong&gt; Want to download your converted file? Create an account first.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So I built &lt;a href="https://aisubtools.xyz" rel="noopener noreferrer"&gt;AI SubTools&lt;/a&gt; — a collection of 35+ free tools where &lt;strong&gt;everything runs client-side in JavaScript&lt;/strong&gt;. No uploads. No accounts. No ads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why client-side matters
&lt;/h2&gt;

&lt;p&gt;It's not just a privacy stance. When the tool runs in your browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's instant.&lt;/strong&gt; No round-trip to a server, no queue. Image compression happens as fast as your CPU allows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works offline&lt;/strong&gt; once the page is loaded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your data never leaves your device.&lt;/strong&gt; For tools that touch passwords, tokens, or checksums, this should be the default, not a feature.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Some of the more interesting tools
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cron expression generator &amp;amp; tester&lt;/strong&gt; — with plain-English explanations of what your expression actually does&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PNG background remover&lt;/strong&gt; — runs entirely in-browser, no upload&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Robots.txt + XML sitemap generator&lt;/strong&gt; — for the SEO-minded&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EAN/UPC barcode creator&lt;/strong&gt; — for e-commerce sellers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON formatter &amp;amp; validator, Base64 encoder/decoder, MD5/SHA-256 generators&lt;/strong&gt; — the daily-driver classics&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The technical trade-offs
&lt;/h2&gt;

&lt;p&gt;The hardest parts were making &lt;strong&gt;image compression&lt;/strong&gt; and &lt;strong&gt;background removal&lt;/strong&gt; perform well without WASM. Canvas-based processing is fast enough for most images, but there's a real art to avoiding UI freezes on large files — chunked processing and careful memory management matter more than raw algorithm speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The site is a side project that grew out of my own daily frustration. New tools are added regularly, and I'm currently prioritizing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More file-format converters&lt;/li&gt;
&lt;li&gt;Better mobile experience for the image tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What tools would you want added?&lt;/strong&gt; I'd genuinely love to hear what's missing from your daily workflow — the best suggestions will get built.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;P.S. Everything is free, forever. No premium tier hiding the useful stuff.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
