<?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: chuanbuilds</title>
    <description>The latest articles on DEV Community by chuanbuilds (@chuanbuilds).</description>
    <link>https://dev.to/chuanbuilds</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%2F3934283%2Ff09f8acd-a946-4882-b570-c88744993267.png</url>
      <title>DEV Community: chuanbuilds</title>
      <link>https://dev.to/chuanbuilds</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chuanbuilds"/>
    <language>en</language>
    <item>
      <title>I built 100+ file tools that never upload your data — here's how the browser-only architecture actually works</title>
      <dc:creator>chuanbuilds</dc:creator>
      <pubDate>Sun, 06 Sep 2026 09:18:57 +0000</pubDate>
      <link>https://dev.to/chuanbuilds/i-built-100-file-tools-that-never-upload-your-data-heres-how-the-browser-only-architecture-558o</link>
      <guid>https://dev.to/chuanbuilds/i-built-100-file-tools-that-never-upload-your-data-heres-how-the-browser-only-architecture-558o</guid>
      <description>&lt;p&gt;A couple of years into running small web tools, I got tired of the same pattern: every "free PDF tool" site either watermarked my file, throttled me after two uploads, or — worse — quietly sent the document to a server I didn't control. For something as sensitive as a tax form or a contract, that's a real problem.&lt;/p&gt;

&lt;p&gt;So I built the opposite. UnTrackedTools is now 90+ tools (PDF, images, JSON, text, archives, dev utilities) that run &lt;strong&gt;entirely in your browser&lt;/strong&gt;. The file never leaves the device. There's no upload step, no account, no backend processing the bytes.&lt;/p&gt;

&lt;p&gt;This post isn't a product pitch. It's a walkthrough of how that architecture works in practice, what libraries do the heavy lifting, and the parts that bit me along the way — because "client-side" sounds clean until you hit a 400 MB PDF on a phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model: file in, file out, nothing in between
&lt;/h2&gt;

&lt;p&gt;Every tool follows the same shape:&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;input type="file"&amp;gt;
   ↓  File API reads bytes into memory (ArrayBuffer / Blob)
   ↓  a library mutates that in-memory data
   ↓  we hand the result back as a download
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;fetch()&lt;/code&gt; to a server with your file. The libraries I use literally can't phone home with your data — they operate on &lt;code&gt;ArrayBuffer&lt;/code&gt;s sitting in tab memory.&lt;/p&gt;

&lt;p&gt;Here's the actual skeleton most of the tools share:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;      &lt;span class="c1"&gt;// bytes live in the tab now&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// pure, in-memory&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// trigger a download, then revoke the URL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole privacy story: the bytes are in the tab, the transform is synchronous local code, and the only thing that ever crosses the network boundary is the static JS bundle itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually does the work
&lt;/h2&gt;

&lt;p&gt;I'm not reinventing file formats. I lean on battle-tested libraries that happen to run in the browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PDF&lt;/strong&gt; → &lt;a href="https://pdf-lib.js.org/" rel="noopener noreferrer"&gt;&lt;code&gt;pdf-lib&lt;/code&gt;&lt;/a&gt;. Merge, split, rotate, add page numbers, compress — all by reading the PDF into memory and rewriting it client-side. The compressor is honest about its limits: it repackages the PDF with object streams (&lt;code&gt;useObjectStreams&lt;/code&gt;), it doesn't re-encode embedded images. So a scanned PDF (already a flat image) won't shrink much. That's a real constraint, not a bug, and I say so on the tool page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images&lt;/strong&gt; → &lt;a href="https://github.com/fengyuanchen/compressorjs" rel="noopener noreferrer"&gt;&lt;code&gt;compressorjs&lt;/code&gt;&lt;/a&gt;, which re-encodes through the Canvas API. No server round-trip, just the browser's own image pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Archives&lt;/strong&gt; → &lt;code&gt;JSZip&lt;/code&gt; for zipping files together in memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passwords / random&lt;/strong&gt; → the Web Crypto API (&lt;code&gt;crypto.getRandomValues&lt;/code&gt;). This matters more than people think: a password generator built on &lt;code&gt;Math.random()&lt;/code&gt; is generating guessable passwords. Mine pulls from the OS cryptographically-secure RNG.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to see any of those in action, here are three that map directly to the libraries above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://untrackedtools.com/en/tools/pdf-compressor/" rel="noopener noreferrer"&gt;Browser PDF compressor&lt;/a&gt; — pdf-lib, repackages locally&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://untrackedtools.com/en/tools/image-compressor/" rel="noopener noreferrer"&gt;Image compressor&lt;/a&gt; — Canvas re-encode, no upload&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://untrackedtools.com/en/tools/password-generator/" rel="noopener noreferrer"&gt;Password generator&lt;/a&gt; — Web Crypto, client-only&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The parts that bit me
&lt;/h2&gt;

&lt;p&gt;"Client-side" is easy to say and harder to ship well. Three things caught me out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Big files block the main thread.&lt;/strong&gt;&lt;br&gt;
Most of my transforms run on the UI thread. A 50 KB JSON formatter is instant. A 200-page PDF merge is fine. But push a 300 MB file and the tab freezes while the library churns — the user sees "page not responding" and assumes it's broken. The honest fix is a Web Worker so the parse happens off-thread, and I haven't rolled that out to every tool yet. I'm naming it because pretending it's all smooth would be a lie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Browser memory is the real ceiling.&lt;/strong&gt;&lt;br&gt;
An &lt;code&gt;ArrayBuffer&lt;/code&gt; lives in RAM. Desktop browsers handle a few hundred MB comfortably; mobile Safari gets unhappy much earlier, and there's no clean "out of memory" error — it just kills the tab. So the practical limit isn't my code, it's the device. I surface a size warning on the heavy tools instead of promising the moon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Safari is its own country.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;File System Access API&lt;/code&gt; (&lt;code&gt;showSaveFilePicker&lt;/code&gt;) would let me write the result back without the download-dialog dance. Chrome ships it; Safari largely doesn't. So I target the lowest common denominator — generate a Blob, trigger a download — and it works everywhere, just one extra click on Apple devices. Not elegant, but it doesn't fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "private by architecture" beats "private by promise"
&lt;/h2&gt;

&lt;p&gt;Most sites &lt;em&gt;say&lt;/em&gt; they delete your file after an hour. You're trusting a server you can't see. The architecture here removes the question: there is no server step that touches your file, so there's nothing to delete, log, or leak. Privacy isn't a setting I flip on — it's a consequence of where the bytes live.&lt;/p&gt;

&lt;p&gt;That also means no compliance theater, no "we never share your data" footer clause to litigate. The data never arrived.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're building something similar
&lt;/h2&gt;

&lt;p&gt;The short version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reach for &lt;code&gt;pdf-lib&lt;/code&gt;, &lt;code&gt;compressorjs&lt;/code&gt;, &lt;code&gt;JSZip&lt;/code&gt;, and Web Crypto before you write a byte of format code yourself.&lt;/li&gt;
&lt;li&gt;Treat &lt;code&gt;crypto.getRandomValues&lt;/code&gt; as the only acceptable RNG for anything security-related.&lt;/li&gt;
&lt;li&gt;Plan for the main-thread freeze on large inputs — a Worker is the right call, I just owe you that work.&lt;/li&gt;
&lt;li&gt;Test on mobile Safari early. It will be the constraint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fun part of this approach is that the "feature" (privacy) is free once the architecture is right. The hard part is everything around it: honest limits, memory ceilings, and browser quirks nobody puts in the landing-page copy.&lt;/p&gt;

&lt;p&gt;If you want to poke at the tools or tell me which one should get a Web Worker first, the whole collection lives at &lt;a href="https://untrackedtools.com/en/" rel="noopener noreferrer"&gt;untrackedtools.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>privacy</category>
      <category>javascript</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I stopped trusting "free" Instagram font generators with my captions — so I built a browser-only one</title>
      <dc:creator>chuanbuilds</dc:creator>
      <pubDate>Fri, 28 Aug 2026 02:57:53 +0000</pubDate>
      <link>https://dev.to/chuanbuilds/i-stopped-trusting-free-instagram-font-generators-with-my-captions-so-i-built-a-browser-only-one-1o0m</link>
      <guid>https://dev.to/chuanbuilds/i-stopped-trusting-free-instagram-font-generators-with-my-captions-so-i-built-a-browser-only-one-1o0m</guid>
      <description>&lt;p&gt;Every time I want a slightly-less-boring Instagram bio, I fall into the same trap. I type my caption into some "free font generator," and somewhere between the input box and the copy button I realize I just pasted an unposted caption into a third-party site I've never heard of. Most of these tools upload your text to render it, which means my half-finished caption is now sitting on someone else's server.&lt;/p&gt;

&lt;p&gt;So I built the one I actually wanted: an &lt;a href="https://untrackedtools.com/en/tools/instagram-font-generator/" rel="noopener noreferrer"&gt;Instagram font generator&lt;/a&gt; that runs 100% in the browser. Type, copy, paste. Nothing leaves the tab.&lt;/p&gt;

&lt;p&gt;Here's the part that's actually interesting if you write code: these "fonts" aren't fonts at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  It's all Unicode
&lt;/h3&gt;

&lt;p&gt;Instagram doesn't let you load custom fonts in a bio or caption. What these generators do is swap each letter for a look-alike character from the &lt;a href="https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols" rel="noopener noreferrer"&gt;Mathematical Alphanumeric Symbols&lt;/a&gt; block (U+1D400 and up). "Bold" Latin isn't a bold typeface — it's the codepoint U+1D400 for 𝐀, U+1D41A for 𝐚, and so on.&lt;/p&gt;

&lt;p&gt;So a generator is really just a small map: &lt;code&gt;a → 𝐚&lt;/code&gt;, &lt;code&gt;b → 𝐛&lt;/code&gt; ... for each style. The whole thing fits in a few hundred bytes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The core is a code-point offset
&lt;/h3&gt;

&lt;p&gt;Each style is an offset into that block. Upper-case A–Z, lower-case a–z, and digits 0–9 each have their own base. A minimal version looks like this:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;styleText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;            &lt;span class="c1"&gt;// for...of walks code points, not UTF-16 units&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;codePointAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x41&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mh"&gt;0x5A&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;        &lt;span class="c1"&gt;// A–Z&lt;/span&gt;
      &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCodePoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;upper&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mh"&gt;0x41&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x61&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mh"&gt;0x7A&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// a–z&lt;/span&gt;
      &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCodePoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lower&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mh"&gt;0x61&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x30&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mh"&gt;0x39&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// 0–9&lt;/span&gt;
      &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCodePoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;digit&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mh"&gt;0x30&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                          &lt;span class="c1"&gt;// leave spaces, punctuation, emoji untouched&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Bold starts at U+1D400 (A), U+1D41A (a), U+1D7CE (0)&lt;/span&gt;
&lt;span class="nf"&gt;styleText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello 2026&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0x1D400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0x1D41A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;digit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0x1D7CE&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// =&amp;gt; "𝐇𝐞𝐥𝐥𝐨 𝟐𝟎𝟐𝟔"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This offset trick only works for the contiguous styles (bold, italic, sans, mono, Fraktur…). Styles like bubble ⓐ or squared ② live in the Enclosed Alphanumerics block and aren't contiguous, so for those I just store a literal lookup table instead of computing an offset.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;for...of&lt;/code&gt; loop matters. If you index the string with &lt;code&gt;input[i]&lt;/code&gt; you'll split emoji and astral characters into surrogate pairs and mangle them. &lt;code&gt;codePointAt(0)&lt;/code&gt; plus iterating with &lt;code&gt;for...of&lt;/code&gt; keeps emoji intact.&lt;/p&gt;

&lt;h3&gt;
  
  
  The lowercase h that broke my first build
&lt;/h3&gt;

&lt;p&gt;This is the bug that cost me an hour. The italic style isn't perfectly contiguous: the code point you'd expect for italic lowercase &lt;strong&gt;h&lt;/strong&gt; — &lt;code&gt;U+1D44E + 7&lt;/code&gt; = &lt;code&gt;U+1D455&lt;/code&gt; — is &lt;em&gt;unassigned&lt;/em&gt;. Unicode already had ℎ (U+210E, the Planck constant symbol) as a slanted h, so it reused that instead of allocating a new slot. My offset math produced a blank/tofu box for every "h".&lt;/p&gt;

&lt;p&gt;The fix is a one-line exception in the map. Same story for a few script/cursive capitals (ℋ, ℐ, ℛ, ℨ) — they live outside the main block too. If you build your own, hard-code those handful of exceptions or your output will have mysterious boxes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Copy without a library
&lt;/h3&gt;

&lt;p&gt;Copying is just the Clipboard API:&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="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;styled&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I add a &lt;code&gt;document.execCommand('copy')&lt;/code&gt; fallback for older mobile browsers that don't support it yet, but on anything current the async API is all you need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why some phones show boxes
&lt;/h3&gt;

&lt;p&gt;The stylized characters are real Unicode, but not every device ships a font that covers the Mathematical Alphanumeric Symbols block. That's why a style can look perfect on your laptop and turn into □□□ on a friend's older phone. The practical rule: test the exact style on the phone you'll post from, and if in doubt use bold or italic — those have the widest support. Also, don't style a whole paragraph; one word reads as intentional, a wall of 𝐛𝐨𝐥𝐝 reads as spam.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why browser-only
&lt;/h3&gt;

&lt;p&gt;The tool joins 100-plus other utilities in the &lt;a href="https://untrackedtools.com/en/" rel="noopener noreferrer"&gt;UntrackedTools&lt;/a&gt; collection, and they're all client-side for the same reason: a caption, a draft bio, a config snippet — none of it should have to leave your machine to be transformed. No account, no upload, no analytics. Open the tab, style your text, close it.&lt;/p&gt;

&lt;p&gt;If you post to Instagram (or TikTok/X — the characters paste anywhere Unicode goes), give the &lt;a href="https://untrackedtools.com/en/tools/instagram-font-generator/" rel="noopener noreferrer"&gt;Instagram font generator&lt;/a&gt; a try. It's the one I use before every post now.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>unicode</category>
      <category>privacy</category>
    </item>
    <item>
      <title>My Inverter Kept Tripping Until I Learned Running Watts Surge Watts</title>
      <dc:creator>chuanbuilds</dc:creator>
      <pubDate>Thu, 13 Aug 2026 08:34:39 +0000</pubDate>
      <link>https://dev.to/chuanbuilds/my-inverter-kept-tripping-until-i-learned-running-watts-surge-watts-4nkc</link>
      <guid>https://dev.to/chuanbuilds/my-inverter-kept-tripping-until-i-learned-running-watts-surge-watts-4nkc</guid>
      <description>&lt;p&gt;When I wired the electrical system in my campervan, I picked a 1000W pure-sine inverter. My loads summed to about 700W, so 1000W felt like comfortable headroom. Then I plugged in a 800W coffee maker and the inverter shut off instantly. Every time.&lt;/p&gt;

&lt;p&gt;Turns out I'd sized for the wrong number.&lt;/p&gt;

&lt;p&gt;There are two wattage figures for almost every device with a motor or compressor: running watts (what it draws steadily) and surge watts (what it yanks for a fraction of a second at startup). A coffee maker, a fridge compressor, a power tool, a vacuum — they all spike on startup, often 2–4× the running draw. My 800W coffee maker was surging close to 1600W the moment it switched on, and my 1000W inverter, rated for maybe 2000W surge for a few milliseconds, was not having it under a cold start.&lt;/p&gt;

&lt;p&gt;The fix isn't just "buy a bigger inverter," either. Oversizing has its own cost: inverters are most efficient at mid-load, and a 3000W unit running a 50W fan wastes an ugly chunk of power just staying awake. You want headroom over the combined surge of everything that could run at once — not the running sum, and not a blind max.&lt;/p&gt;

&lt;p&gt;So I listed every device I'd ever run off inverter power, with both numbers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Device&lt;/th&gt;
&lt;th&gt;Running&lt;/th&gt;
&lt;th&gt;Surge&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fridge compressor&lt;/td&gt;
&lt;td&gt;45W&lt;/td&gt;
&lt;td&gt;120W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coffee maker&lt;/td&gt;
&lt;td&gt;800W&lt;/td&gt;
&lt;td&gt;1600W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Laptop charger&lt;/td&gt;
&lt;td&gt;90W&lt;/td&gt;
&lt;td&gt;90W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phone&lt;/td&gt;
&lt;td&gt;18W&lt;/td&gt;
&lt;td&gt;18W&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Coffee + fridge starting together = ~1720W surge. That's the number the inverter has to survive for a second, not the 953W running total. I landed on a 2000W pure-sine unit and the trips stopped.&lt;/p&gt;

&lt;p&gt;One more thing people skip: pure-sine vs modified-sine matters for anything with electronics in it. Cheap modified-sine inverters can fry sensitive gear or make weird noises in speakers. For a van where you're charging laptops and running a fridge controller, pure-sine is not optional.&lt;/p&gt;

&lt;p&gt;I worked the whole thing through &lt;a href="https://camppowercalc.com/en/" rel="noopener noreferrer"&gt;CampPowerCalc&lt;/a&gt; and their &lt;a href="https://camppowercalc.com/en/blog/campervan-inverter-sizing-guide/" rel="noopener noreferrer"&gt;campervan inverter sizing guide&lt;/a&gt; — it walks through exactly this running-vs-surge step instead of just telling you to add up watts. The &lt;a href="https://en.wikipedia.org/wiki/Power_inverter" rel="noopener noreferrer"&gt;power inverter&lt;/a&gt; page on Wikipedia is a decent reference if you want the electrical background.&lt;/p&gt;

&lt;p&gt;If your inverter trips on one specific thing, don't assume the device is broken. Check its surge draw first.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I Built a Tariff Lookup and Learned Why "Laptop" Is Not a Good Enough Word</title>
      <dc:creator>chuanbuilds</dc:creator>
      <pubDate>Tue, 11 Aug 2026 09:07:50 +0000</pubDate>
      <link>https://dev.to/chuanbuilds/i-built-a-tariff-lookup-and-learned-why-laptop-is-not-a-good-enough-word-59j4</link>
      <guid>https://dev.to/chuanbuilds/i-built-a-tariff-lookup-and-learned-why-laptop-is-not-a-good-enough-word-59j4</guid>
      <description>&lt;p&gt;Shipping side project hardware internationally made me sit down and actually read the Harmonized System for the first time. Ports and laptops, it turns out, do not live in the same subheading. A laptop is 8471.30 (portable automatic data processing machines); a port replicator or USB hub is a different heading entirely. Same desk, different tax rate.&lt;/p&gt;

&lt;p&gt;The interesting engineering problem: tariff schedules are hierarchical — 2-digit chapter, 4-digit heading, 6-digit subheading — and the first six digits are shared across 200+ countries. That means one lookup table can serve the US, EU, UK and China, which is exactly the kind of data structure you want as a static export.&lt;/p&gt;

&lt;p&gt;I documented the classification logic while building a &lt;a href="https://tariffpedia.com/hs-code/847130/" rel="noopener noreferrer"&gt;laptop import duty reference&lt;/a&gt; and it's been the single most useful artifact for quoting customers accurate landed costs. The code is boring; the data model is the product.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>hardware</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>I sized my camping battery wrong three times before I built a calculator for it</title>
      <dc:creator>chuanbuilds</dc:creator>
      <pubDate>Tue, 11 Aug 2026 08:06:35 +0000</pubDate>
      <link>https://dev.to/chuanbuilds/i-sized-my-camping-battery-wrong-three-times-before-i-built-a-calculator-for-it-1km3</link>
      <guid>https://dev.to/chuanbuilds/i-sized-my-camping-battery-wrong-three-times-before-i-built-a-calculator-for-it-1km3</guid>
      <description>&lt;p&gt;The first time I killed a battery at 2am, it was my own fault. I'd added up the watts on the stickers — fridge 45W, lights 10W, phone 12W — and rounded up to a 100Ah battery. What I'd missed is that "watts on a label" and "amp-hours in a box" aren't the same unit, and a compressor fridge spikes to 3–4× its running draw the second it kicks on.&lt;/p&gt;

&lt;p&gt;The second time, I'd learned about watt-hours but forgot the inverter efficiency loss. The third time, I trusted the "up to 8 hours" claim on a product page that was measured at the fridge's &lt;em&gt;minimum&lt;/em&gt; setting with nothing else running.&lt;/p&gt;

&lt;p&gt;So I sat down and actually worked the math. The unit that matters is watt-hours — watts × hours. A 45W fridge running 24h isn't 45Wh, it's ~1,080Wh, and that's before the surge tax. Here's the tiny function I kept reaching for:&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;// watts pulled, hours run per day, plus a safety buffer&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;dailyWh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;watts&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rig&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="na"&gt;watts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;  &lt;span class="c1"&gt;// compressor fridge, runs near-constant&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;watts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;// LED lights&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;watts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// phone + laptop top-up&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dailyWh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rig&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// ~1354 Wh needed per day&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That 1,354Wh is the number you actually size against — not the 67W sum of the stickers. Then you divide by your battery's nominal voltage to get amp-hours, and shave off ~20% so you're not parking a lithium cell at 100% discharge every night (which quietly halves its lifespan).&lt;/p&gt;

&lt;p&gt;Once I'd done this by hand one too many times, I put it into a &lt;a href="https://camppowercalc.com/en/" rel="noopener noreferrer"&gt;&lt;strong&gt;camping power calculator&lt;/strong&gt;&lt;/a&gt; so I'd stop redoing the arithmetic on the back of a receipt. It walks the same path — list the devices, get the real Wh, then the battery and solar size that actually hold — and it's the thing I now send to friends instead of a paragraph of text.&lt;/p&gt;

&lt;p&gt;Solar is the part people get backwards. A panel's "100W" is a lab number at perfect noon sun; you realistically collect 4–6 sun-hours a day, and only in summer. So a 100W panel is closer to 400–600Wh/day, not 2,400. If your rig eats 1,354Wh, one panel won't keep up — you need two, or a smaller fridge. The physics here is worth reading up on; &lt;a href="https://en.wikipedia.org/wiki/Watt-hour" rel="noopener noreferrer"&gt;watt-hour&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/Photovoltaic_system" rel="noopener noreferrer"&gt;photovoltaic system&lt;/a&gt; are the two pages I wish I'd opened before trip one.&lt;/p&gt;

&lt;p&gt;The takeaway I keep repeating to myself: size for the &lt;em&gt;peak and the real daily draw&lt;/em&gt;, not the sticker sum. A 20% buffer isn't optional padding, it's the difference between a battery that lasts the season and one you're replacing in the spring.&lt;/p&gt;

</description>
      <category>camping</category>
      <category>offgrid</category>
      <category>javascript</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I finally stopped hand-indenting YAML. Here's the local converter I added to my toolkit</title>
      <dc:creator>chuanbuilds</dc:creator>
      <pubDate>Fri, 07 Aug 2026 07:51:06 +0000</pubDate>
      <link>https://dev.to/chuanbuilds/i-finally-stopped-hand-indenting-yaml-heres-the-local-converter-i-added-to-my-toolkit-52o4</link>
      <guid>https://dev.to/chuanbuilds/i-finally-stopped-hand-indenting-yaml-heres-the-local-converter-i-added-to-my-toolkit-52o4</guid>
      <description>&lt;p&gt;YAML is fine until it isn't. I keep a bunch of config files for side projects — CI pipelines, Docker compose, some Kubernetes manifests — and every time I need to move a JSON blob into one of them, I end up hand-converting it. That's how you get an hour of debugging a single missing space.&lt;/p&gt;

&lt;p&gt;The annoying part is the tools. The converters you find by searching either upload your file somewhere or make you paste config into a third-party server. For a config file that contains API keys or internal URLs, that's a non-starter for me.&lt;/p&gt;

&lt;p&gt;So I added a JSON ↔ YAML converter that runs entirely in the browser to my collection of client-side tools:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://untrackedtools.com/en/tools/json-yaml-converter/" rel="noopener noreferrer"&gt;&lt;strong&gt;JSON to YAML converter&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It does the two directions. Paste JSON, get YAML — or paste YAML, get JSON. Output is a one-click copy, nothing leaves the tab, no upload, no account. The conversion itself is standard library logic, not some ML thing, so what you get out is deterministic and lossless for valid input.&lt;/p&gt;

&lt;p&gt;A quick example, because it's the kind of thing that looks trivial until you've been burned by it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"services"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"api"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"registry.example.com/api:2.4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ports"&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="s2"&gt;"8080:80"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"DB_HOST"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postgres"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"DB_PORT"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5432"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;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;Hand-writing that as YAML means getting every level of indentation right:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.example.com/api:2.4&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;8080:80&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DB_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;DB_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5432'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One wrong space and your container just... doesn't come up. The converter removes the whole class of mistake — you work in the format you're comfortable in, and paste the result into the config.&lt;/p&gt;

&lt;p&gt;It joins 100-plus tools in the &lt;a href="https://untrackedtools.com/en/" rel="noopener noreferrer"&gt;Untracked Tools&lt;/a&gt; collection, all client-side for the same privacy reason. If you deal with config files at all, bookmark the converter — it's one of those "saved me on a Friday afternoon" tools.&lt;/p&gt;

</description>
      <category>json</category>
      <category>yaml</category>
      <category>tools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Built a Free Invoice Generator for Freelancers — Here's What Shipping Solo Taught Me</title>
      <dc:creator>chuanbuilds</dc:creator>
      <pubDate>Thu, 30 Jul 2026 07:46:45 +0000</pubDate>
      <link>https://dev.to/chuanbuilds/i-built-a-free-invoice-generator-for-freelancers-heres-what-shipping-solo-taught-me-34g6</link>
      <guid>https://dev.to/chuanbuilds/i-built-a-free-invoice-generator-for-freelancers-heres-what-shipping-solo-taught-me-34g6</guid>
      <description>&lt;p&gt;Like a lot of freelancers, I got frustrated with the same two bad options for sending invoices: hand-formatting them in Word, or signing up for a full accounting suite just to export a PDF.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;InvoiceFormly&lt;/strong&gt; (&lt;a href="https://invoiceformly.com/en/" rel="noopener noreferrer"&gt;https://invoiceformly.com/en/&lt;/a&gt;) — a free, browser-based invoice generator. Type your line items, drop in your logo, and download a clean PDF. No sign-up required for basic use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 15&lt;/strong&gt; (App Router) deployed on &lt;strong&gt;Cloudflare Pages&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PayPal Subscriptions&lt;/strong&gt; for the optional Pro tier&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The interesting bits
&lt;/h2&gt;

&lt;p&gt;The trickiest parts were PDF rendering fidelity across browsers and handling tax/rounding correctly for multiple currencies. I kept the editor and the exported PDF layout in sync so what you see is exactly what gets sent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm posting
&lt;/h2&gt;

&lt;p&gt;I'm a solo indie developer and this is my first real SaaS. If you send invoices as a freelancer or run a small business, give it a try: &lt;a href="https://invoiceformly.com/en/" rel="noopener noreferrer"&gt;https://invoiceformly.com/en/&lt;/a&gt;. I'd genuinely love feedback — especially from anyone who's solved cross-currency tax edge cases cleanly.&lt;/p&gt;

&lt;p&gt;Happy to answer anything about the build in the comments.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>nextjs</category>
      <category>saas</category>
    </item>
    <item>
      <title>I killed my campervan battery on night one. So I built a power calculator.</title>
      <dc:creator>chuanbuilds</dc:creator>
      <pubDate>Fri, 17 Jul 2026 00:28:29 +0000</pubDate>
      <link>https://dev.to/chuanbuilds/i-killed-my-campervan-battery-on-night-one-so-i-built-a-power-calculator-b38</link>
      <guid>https://dev.to/chuanbuilds/i-killed-my-campervan-battery-on-night-one-so-i-built-a-power-calculator-b38</guid>
      <description>&lt;p&gt;The first night in the van, I ran the fridge, charged a laptop, and left a light on. By 11pm the battery was dead and my phone was at 2%. I'd "estimated" my power needs the way you estimate how much pizza to order — vaguely, and wrong.&lt;/p&gt;

&lt;p&gt;Turns out off-grid power isn't that complicated, it's just unforgiving if you guess. You need to know how many watts each thing pulls, for how many hours a day, and then size a battery and solar to cover it with some margin. The math is simple; doing it in your head at a campsite is not.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://camppowercalc.com/en/" rel="noopener noreferrer"&gt;CampPowerCalc&lt;/a&gt;. You tell it the appliances you actually run — fan, fridge, laptop, lights, water pump — and roughly how long each runs, and it works out the watt-hours, then sizes the battery and solar you'd want for vanlife, RV, or just a weekend off-grid. It uses real appliance draw data and straight physics, no "trust me bro" fudge factors.&lt;/p&gt;

&lt;p&gt;The thing I like is it shows the gap. My first plan was way under-sized; seeing the number made me buy one bigger battery instead of discovering the shortfall at 2am in the woods. Different setups need different answers, so treat the result as a solid starting point and sanity-check it against your own gear — but it beats guessing and eating cold food.&lt;/p&gt;

&lt;p&gt;If you're planning any off-grid power, it's here: &lt;a href="https://camppowercalc.com/en/" rel="noopener noreferrer"&gt;https://camppowercalc.com/en/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>tools</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built a tiny import-duty calculator because the official ones are a maze</title>
      <dc:creator>chuanbuilds</dc:creator>
      <pubDate>Thu, 16 Jul 2026 01:54:07 +0000</pubDate>
      <link>https://dev.to/chuanbuilds/i-built-a-tiny-import-duty-calculator-because-the-official-ones-are-a-maze-3cb2</link>
      <guid>https://dev.to/chuanbuilds/i-built-a-tiny-import-duty-calculator-because-the-official-ones-are-a-maze-3cb2</guid>
      <description>&lt;p&gt;A few months ago a friend asked me to help figure out the tariff on a synthesizer he wanted to buy from a shop in Japan and ship to the US. Simple question, right? How much duty will this actually owe?&lt;/p&gt;

&lt;p&gt;Six tabs and an hour later, I still didn't have a clean answer. The official sources exist — they're just buried in HS-code tables written for customs brokers, not humans. You're expected to know that a "synthesizer" maps to a specific harmonized code, then cross-reference the MFN rate, then remember that the rate depends on where the thing was &lt;em&gt;made&lt;/em&gt;, not where you bought it.&lt;/p&gt;

&lt;p&gt;So I built the thing I wished existed: &lt;a href="https://tariffpedia.com" rel="noopener noreferrer"&gt;TariffPedia&lt;/a&gt;. You type a product name or paste an HS code, pick the destination (US, EU, UK, or China for now), and it shows the representative MFN duty rate with a link straight to the official government source behind each number. No account, no paywall, no "sign up to see the result."&lt;/p&gt;

&lt;p&gt;The part I cared about most was the source links. Anyone can quote a percentage; the useful thing is being one click from the page that actually says it, so you can verify before you rely on it. Last week I used it to sanity-check a camera lens I was importing, and the ten seconds it took beat the alternative of guessing and hoping the courier didn't slap on a surprise fee.&lt;/p&gt;

&lt;p&gt;One honest caveat: these are &lt;em&gt;representative&lt;/em&gt; MFN rates. The real number shifts with the product's country of origin, trade agreements, and the occasional exemption, and I'm not a customs broker. For anything with real money behind it, check the official source (TariffPedia links to it) or ask a licensed broker. I just wanted the guessing to stop for the small, everyday "wait, will this get taxed?" questions.&lt;/p&gt;

&lt;p&gt;If you've ever stared at a customs form wondering what number goes where, it's here: &lt;a href="https://tariffpedia.com" rel="noopener noreferrer"&gt;https://tariffpedia.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Digital Nomad Taxes: the basics before you panic</title>
      <dc:creator>chuanbuilds</dc:creator>
      <pubDate>Tue, 14 Jul 2026 01:05:56 +0000</pubDate>
      <link>https://dev.to/chuanbuilds/digital-nomad-taxes-the-basics-before-you-panic-1i29</link>
      <guid>https://dev.to/chuanbuilds/digital-nomad-taxes-the-basics-before-you-panic-1i29</guid>
      <description>&lt;p&gt;Everyone's "just work from Bali" story skips the boring part: you probably still owe tax. Being a digital nomad is a lifestyle, not a tax category. Here's what actually matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where you're taxed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;US citizens&lt;/strong&gt; are taxed on worldwide income regardless of where they live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most others&lt;/strong&gt; are taxed where they're a tax resident — commonly the 183-day rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Double-taxation treaties&lt;/strong&gt; exist between many countries so you're not charged twice, but you must actually claim them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Two levers US expats use
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Foreign Earned Income Exclusion (FEIE)&lt;/strong&gt; — excludes a chunk of earned income if you meet the residency test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Foreign Tax Credit&lt;/strong&gt; — offsets US tax by what you already paid abroad.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Don't DIY the hard stuff
&lt;/h2&gt;

&lt;p&gt;If you've got income in three countries or a business structure, talk to someone. The cost of a mistake beats the cost of a consult.&lt;/p&gt;

&lt;p&gt;I keep the plain-language version and simple calculators at &lt;a href="https://taxedabroad.com/en/" rel="noopener noreferrer"&gt;Taxed Abroad&lt;/a&gt; — it's where I dump what I learn so I don't forget it. Use it to know which questions to ask, then confirm with a pro.&lt;/p&gt;

</description>
      <category>taxes</category>
      <category>digitalnomad</category>
      <category>expat</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>Two image tools I use that need zero install</title>
      <dc:creator>chuanbuilds</dc:creator>
      <pubDate>Mon, 13 Jul 2026 12:00:50 +0000</pubDate>
      <link>https://dev.to/chuanbuilds/two-free-image-tools-i-use-that-need-zero-install-4mbk</link>
      <guid>https://dev.to/chuanbuilds/two-free-image-tools-i-use-that-need-zero-install-4mbk</guid>
      <description>&lt;p&gt;I edit a lot of images for the web and I got sick of heavy desktop apps and sketchy upload sites. Two browser tools ended up in my daily rotation:&lt;/p&gt;

&lt;h2&gt;
  
  
  CompactJPG — shrink images without losing the plot
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://compactjpg.com" rel="noopener noreferrer"&gt;CompactJPG&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Free online image compressor for JPG/PNG/WebP. The part I care about: it processes files locally in your browser, so nothing gets uploaded to some server. You drag in a photo, pick a level, and download a much smaller file that still looks fine. I use it before publishing any screenshot or product shot — shaves page weight without visible quality loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  ClarifyPix — fix an image with AI
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://clarifypix.com" rel="noopener noreferrer"&gt;ClarifyPix&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When a photo is blurry, low-res, or has a messy background, ClarifyPix runs AI on it to upscale, deblur, remove the background, or touch it up. You upload the photo for processing (deleted after an hour), and there's no free tier — a $1.99/week trial covers short projects. Handy for ecommerce listings, profile pics, or old photos you want to rescue.&lt;/p&gt;




&lt;p&gt;Both are part of a small set of tools I keep at &lt;a href="https://untrackedtools.com/en/" rel="noopener noreferrer"&gt;Untracked Tools&lt;/a&gt;. CompactJPG is free with no account; ClarifyPix needs an account and a paid trial. If your workflow still involves opening Photoshop to crop a JPEG, try the compressor first — it'll save you five minutes a day.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>indiehackers</category>
      <category>tools</category>
    </item>
    <item>
      <title>I shipped 8 small web tools for overseas users — here's the messy truth</title>
      <dc:creator>chuanbuilds</dc:creator>
      <pubDate>Sun, 12 Jul 2026 02:18:37 +0000</pubDate>
      <link>https://dev.to/chuanbuilds/i-shipped-8-small-web-tools-for-overseas-users-heres-the-messy-truth-1051</link>
      <guid>https://dev.to/chuanbuilds/i-shipped-8-small-web-tools-for-overseas-users-heres-the-messy-truth-1051</guid>
      <description>&lt;p&gt;A while back I stopped waiting for the one big SaaS idea and started building a bunch of small, boring tools that each solve exactly one problem. The theory: a portfolio of tiny sites, each monetized with ads or a subscription, compounding traffic over months instead of betting everything on a launch.&lt;/p&gt;

&lt;p&gt;It's been equal parts fun and humbling. Here's what's live so far and what I actually learned shipping them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tools
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Images &amp;amp; AI&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://compactjpg.com/" rel="noopener noreferrer"&gt;CompactJPG&lt;/a&gt; — a free online image compressor. Files are processed locally in the browser, nothing gets uploaded, and it keeps quality better than I expected.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://clarifypix.com/" rel="noopener noreferrer"&gt;ClarifyPix&lt;/a&gt; — AI image fixes: upscale a blurry photo, remove a background, clean up an image. You upload a photo for processing (deleted after an hour); no free tier, but a $1.99/week trial.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Calculators (people love a good calculator)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://camppowercalc.com/en/" rel="noopener noreferrer"&gt;Camp Power Calc&lt;/a&gt; — type in your devices (fridge, lights, phone, rice cooker) and it tells you the total draw, battery size, and solar panel you need so you don't lose power mid-trip.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://roompaintcalc.com/en/" rel="noopener noreferrer"&gt;Room Paint Calculator&lt;/a&gt; — punch in room size, doors, windows and get liters of paint plus a rough cost.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://frycalc.com/" rel="noopener noreferrer"&gt;Fry Calc&lt;/a&gt; — temps, time, and oil for frying / air fryer based on ingredient and portion. Made it after one too many ruined batches.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://coldplungecalc.com/en/" rel="noopener noreferrer"&gt;Cold Plunge Calculator&lt;/a&gt; — dial in water temp, duration, and how much ice to add based on body weight and volume, so cold therapy stays safe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Collections &amp;amp; money&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://untrackedtools.com/en/" rel="noopener noreferrer"&gt;Untracked Tools&lt;/a&gt; — the hub that lists every small tool I build in one place. No ads, instant load.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://taxedabroad.com/" rel="noopener noreferrer"&gt;Taxed Abroad&lt;/a&gt; — tax guidance for expats and digital nomads: residency, treaties, filing obligations, in plain language.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Specific beats clever.&lt;/strong&gt; "Camp power calculator" gets searched. "Smart outdoor energy platform" does not. I wasted a week on a clever name for the paint tool before renaming it to what people actually type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distribution is the real product.&lt;/strong&gt; Building took days; getting the first 100 visitors took longer. Submitting to directories, writing a few posts, and answering questions in the right places moved the needle more than any feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Calculators quietly convert.&lt;/strong&gt; They're not flashy, but someone who just calculated their campsite battery need is a warm visitor. AdSense on these pages also performs better than on vague landing pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't fake the numbers.&lt;/strong&gt; Early on I was tempted to post MRR screenshots I didn't have. Didn't. The builders who share real, including ugly, numbers are the ones people trust and link to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this is going
&lt;/h2&gt;

&lt;p&gt;The plan is to keep shipping one focused tool at a time, link them together through &lt;a href="https://untrackedtools.com/en/" rel="noopener noreferrer"&gt;Untracked Tools&lt;/a&gt;, and let SEO + a few good backlinks do the slow work. No growth hacks, just consistent small bets.&lt;/p&gt;

&lt;p&gt;If you're building small tools too, the hub above is the easiest way to see everything in one spot. Happy to compare notes with anyone on the same path.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
