<?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: Joseph Orduna</title>
    <description>The latest articles on DEV Community by Joseph Orduna (@joseph_orduna_b458c7df808).</description>
    <link>https://dev.to/joseph_orduna_b458c7df808</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%2F4203403%2F752382e6-1645-40bf-850b-35666c395be5.png</url>
      <title>DEV Community: Joseph Orduna</title>
      <link>https://dev.to/joseph_orduna_b458c7df808</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/joseph_orduna_b458c7df808"/>
    <language>en</language>
    <item>
      <title>How I generate llms-full.txt for 1,500 tools at deploy time (so it never goes stale)</title>
      <dc:creator>Joseph Orduna</dc:creator>
      <pubDate>Thu, 12 Nov 2026 15:32:07 +0000</pubDate>
      <link>https://dev.to/joseph_orduna_b458c7df808/how-i-generate-llms-fulltxt-for-1500-tools-at-deploy-time-so-it-never-goes-stale-4emd</link>
      <guid>https://dev.to/joseph_orduna_b458c7df808/how-i-generate-llms-fulltxt-for-1500-tools-at-deploy-time-so-it-never-goes-stale-4emd</guid>
      <description>&lt;p&gt;A few weeks ago I wrote about serving llms.txt so AI assistants can cite my site accurately (&lt;a href="https://practicalwebtools.com/blog/the-free-client-side-seo-toolkit-visual-sitemaps-on-page-grading-and-llms-txt" rel="noopener noreferrer"&gt;here's that post&lt;/a&gt;). The pushback I got was interesting: "nice idea, but who maintains that?" A hand-written list of URLs rots in a month. When a site has hundreds or thousands of pages, any file you maintain by hand will go stale, and a stale llms.txt is worse than none, because it teaches models to cite URLs that 404.&lt;/p&gt;

&lt;p&gt;So I stopped maintaining it. A script generates it at deploy time now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule: the registry is the source of truth
&lt;/h2&gt;

&lt;p&gt;My site has a central registry of tools, and every tool page is rendered from it. That is the key precondition: if your CMS or build system knows about every page, an exhaustive plain-text index is a build artifact, not a document.&lt;/p&gt;

&lt;p&gt;The generator (&lt;code&gt;scripts/gen-llms-full.js&lt;/code&gt;) walks the registry at &lt;code&gt;npm run build&lt;/code&gt; and emits &lt;code&gt;llms-full.txt&lt;/code&gt; alongside the static output. Deploy regenerates it. Rename a tool, add a category, split a page: the file updates in the same commit. It cannot drift because nobody edits it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What goes in
&lt;/h2&gt;

&lt;p&gt;The full file for my site is about 120KB of plain text: every tool with its URL, grouped into the 32 categories the site actually uses, with a one-line description per tool. Not marketing copy, just the "what does this page do" line a model needs before deciding to cite it.&lt;/p&gt;

&lt;p&gt;A snippet of the shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## PDF tools
&lt;/span&gt;&lt;span class="k"&gt;- &lt;/span&gt;Merge PDF files: https://practicalwebtools.com/edit/merge-pdf
&lt;span class="k"&gt;- &lt;/span&gt;Split PDF by page ranges: https://practicalwebtools.com/edit/split-pdf
&lt;span class="k"&gt;- &lt;/span&gt;Edit PDF text in the browser: https://practicalwebtools.com/edit/edit-pdf-text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three decisions worth copying:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Flat list per category, not nested prose.&lt;/strong&gt; Models extract link+description pairs from &lt;code&gt;- description: URL&lt;/code&gt; lines with near-zero error. Nested paragraphs bury the URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Absolute URLs.&lt;/strong&gt; Relative paths in a plain-text file get mangled when models splice citations into answers. Absolute URLs survive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One line per tool, no shame about duplication.&lt;/strong&gt; If a tool belongs in two categories, list it twice with the two descriptions a searcher would use. This is an index, not a database. Redundancy that helps retrieval is free.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  llms.txt vs llms-full.txt
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://llmstxt.org/" rel="noopener noreferrer"&gt;llms.txt convention&lt;/a&gt; defines two files: a curated overview (&lt;code&gt;llms.txt&lt;/code&gt;) and the exhaustive dump (&lt;code&gt;llms-full.txt&lt;/code&gt;). Treat them differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;llms.txt&lt;/code&gt; is your front door. Curated, honest counts, the 20-40 URLs that answer most questions. I rewrote mine after catching it claiming "500+ tools" with two dead links, a claim a model would happily repeat.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;llms-full.txt&lt;/code&gt; is the site map for machines. Every URL, no curation, generated. Mine lists all ~500 tools, the 460+ unit-conversion pairs, and the 12 Creative Studio apps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Generate the big one. Curate the small one. Never hand-maintain either.&lt;/p&gt;

&lt;h2&gt;
  
  
  The payoff is citations, not traffic
&lt;/h2&gt;

&lt;p&gt;Six weeks in, the measurable effect is that AI assistants recommend specific tool URLs instead of the homepage. The deep links models cite match real pages because they read them from the index. That is the whole game: every tool page on my site carries its own summary and FAQ structured data, and the llms index makes those pages findable to models in the first place.&lt;/p&gt;

&lt;p&gt;If your build system can enumerate your pages, you can ship this in an afternoon. The script is 40 lines. The hard part is the registry, and if you're rendering pages from data anyway, you already have it. Browse the results yourself at &lt;a href="https://practicalwebtools.com" rel="noopener noreferrer"&gt;practicalwebtools.com&lt;/a&gt;, or read the raw &lt;a href="https://practicalwebtools.com/llms-full.txt" rel="noopener noreferrer"&gt;llms-full.txt&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>automation</category>
      <category>ai</category>
    </item>
    <item>
      <title>The iframe Auto-Resize Handshake: Engineering Embeddable Widgets That Don't Fight Their Host</title>
      <dc:creator>Joseph Orduna</dc:creator>
      <pubDate>Tue, 10 Nov 2026 19:02:32 +0000</pubDate>
      <link>https://dev.to/joseph_orduna_b458c7df808/the-iframe-auto-resize-handshake-engineering-embeddable-widgets-that-dont-fight-their-host-17pe</link>
      <guid>https://dev.to/joseph_orduna_b458c7df808/the-iframe-auto-resize-handshake-engineering-embeddable-widgets-that-dont-fight-their-host-17pe</guid>
      <description>&lt;p&gt;If you have ever embedded a third-party widget in an iframe, you know the two failure modes: an inner scrollbar that swallows half the widget, or a 2000-pixel-tall frame with 400 pixels of content. The usual fixes are hacky (hardcoded heights, &lt;code&gt;scrolling=no&lt;/code&gt; and prayer). This post is the full engineering notes for the embed surface I run at &lt;a href="https://practicalwebtools.com/" rel="noopener noreferrer"&gt;Practical Web Tools&lt;/a&gt; — every calculator on the site (400+, across &lt;a href="https://practicalwebtools.com/calculators" rel="noopener noreferrer"&gt;finance, math, health and science&lt;/a&gt;) is embeddable as an auto-sizing iframe, and the whole sizing layer is ~60 lines of framework-free ES5.&lt;/p&gt;

&lt;h2&gt;
  
  
  The handshake
&lt;/h2&gt;




&lt;p&gt;The framed page measures itself; the host page applies the height. Three pieces:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inside the frame&lt;/strong&gt; — measure on load, on every DOM change, and on a few early intervals (cache-cold renders change height late):&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;var&lt;/span&gt; &lt;span class="n"&gt;send&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;force&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;offsetHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;offsetHeight&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="n"&gt;last&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="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'pwt-embed:resize'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'*'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ResizeObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// plus: DOMContentLoaded, load, and 500ms x 40 early ticks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;On the host&lt;/strong&gt; — one listener, matched to the right frame (never &lt;code&gt;document.querySelectorAll('iframe')[0]&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'https://practicalwebtools.com'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'pwt-embed:resize'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;160&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// find the iframe whose contentWindow === e.source, then:&lt;/span&gt;
  &lt;span class="n"&gt;iframe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'px'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The late-script case&lt;/strong&gt; — if the host's resizer script loads after the frame already reported (async scripts, bottom-of-page pastes), the frame's message is gone. Fix: the host pings back &lt;code&gt;{type:'pwt-embed:ping'}&lt;/code&gt; on load and on DOMContentLoaded, and the frame force-sends on ping. No timing assumptions in either direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feedback loop that ate my afternoon
&lt;/h2&gt;




&lt;p&gt;Apply the reported height → the framed page reflows → it reports a new height → apply → reflow. This happens whenever the framed document has any viewport-sized element (a &lt;code&gt;min-height:100vh&lt;/code&gt; hero is the classic). The loop's signature: the same height delta each round. So the frame-side code tracks the delta; if &lt;code&gt;|new - last|&lt;/code&gt; repeats five times, height reporting stops. The widget might be slightly wrong-sized at that point, but it stops &lt;em&gt;growing forever&lt;/em&gt;, which is the difference between "a bit short" and "the page is 40,000 pixels long."&lt;/p&gt;

&lt;h2&gt;
  
  
  Third-party frames deny your storage
&lt;/h2&gt;




&lt;p&gt;Browsers throw on &lt;code&gt;localStorage&lt;/code&gt; access inside cross-origin iframes (Safari is the strictest). A calculator that persists its inputs — the &lt;a href="https://practicalwebtools.com/finance/retirement-calculator" rel="noopener noreferrer"&gt;retirement calculator&lt;/a&gt; keeps your balances, the &lt;a href="https://practicalwebtools.com/other/gpa-calculator" rel="noopener noreferrer"&gt;GPA calculator&lt;/a&gt; keeps your courses — crashes on load inside a frame unless you shim it &lt;em&gt;before any tool code runs&lt;/em&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'__probe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'1'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'__probe'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;localStorage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;shim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// getItem/setItem/removeItem/key/length&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Probe, don't feature-detect once and cache: the probe itself is the detection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clicks, links, and not hijacking the host
&lt;/h2&gt;




&lt;p&gt;Inside a frame, a plain link either traps the user in the widget or navigates the host page out from under them. Neither is acceptable. The embed bootstrap intercepts left-clicks on same-site links and rewrites them to &lt;code&gt;window.open(href, '_blank', 'noopener')&lt;/code&gt;. Off-site links keep their authored &lt;code&gt;target&lt;/code&gt;. The host page never navigates because of something inside the widget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ads and embeds don't mix
&lt;/h2&gt;




&lt;p&gt;The site is ad-supported; embeds load zero ads, zero analytics trackers, zero site chrome. This is not altruism — a widget that renders ads onto someone else's article gets ripped out, and rightly so. The framed document drops to a bare tool with a two-line credit bar ("Free calculator by Practical Web Tools" + "Open full calculator"). That credit link is the entire business model, and it ships inside the snippet, in the host page's HTML, as a plain anchor — not injected by script after the fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Themes, honestly
&lt;/h2&gt;




&lt;p&gt;"Follow the visitor's OS" paints dark widgets into light articles — the host picked their page's look, not the visitor's OS. Widgets default light and accept an explicit &lt;code&gt;&amp;amp;theme=dark&lt;/code&gt;. Boring, correct.&lt;/p&gt;

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




&lt;p&gt;Every calculator page (say the &lt;a href="https://practicalwebtools.com/science/astronomy/moon-phase-today" rel="noopener noreferrer"&gt;moon phase tool&lt;/a&gt; or the &lt;a href="https://practicalwebtools.com/health/body-fat-calculator" rel="noopener noreferrer"&gt;body-fat calculator&lt;/a&gt;) shows its own copy-paste snippet with a live preview at the bottom of the page; the flagship hub with the nine one-line script widgets (no iframe at all) is &lt;a href="https://practicalwebtools.com/embed" rel="noopener noreferrer"&gt;practicalwebtools.com/embed&lt;/a&gt;. The MIT-licensed scripts are on &lt;a href="https://github.com/CobraPi/embeddable-calculator-widgets" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; if you'd rather self-host the whole thing.&lt;/p&gt;

&lt;p&gt;If you hit a sizing edge case I didn't — especially CSS-driven oscillation — I'd genuinely like to hear about it in the comments.&lt;/p&gt;

&lt;p&gt;Tags: javascript, webdev, iframe, widgets&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>iframe</category>
      <category>widgets</category>
    </item>
    <item>
      <title>Every Calculator on My Site Is Now a Free Embeddable Widget — No API Key, No Server, No Limits</title>
      <dc:creator>Joseph Orduna</dc:creator>
      <pubDate>Tue, 10 Nov 2026 16:42:20 +0000</pubDate>
      <link>https://dev.to/joseph_orduna_b458c7df808/every-calculator-on-my-site-is-now-a-free-embeddable-widget-no-api-key-no-server-no-limits-1g5f</link>
      <guid>https://dev.to/joseph_orduna_b458c7df808/every-calculator-on-my-site-is-now-a-free-embeddable-widget-no-api-key-no-server-no-limits-1g5f</guid>
      <description>&lt;h2&gt;
  
  
  The widget tax
&lt;/h2&gt;

&lt;p&gt;Google "mortgage calculator widget" and you land on pricing pages: $9–$49/month, API keys, monthly view caps, and watermarks you pay extra to remove. If you run a real-estate blog and want one interactive payment calculator in a post, that's a weird amount of infrastructure for arithmetic.&lt;/p&gt;

&lt;p&gt;I run &lt;a href="https://practicalwebtools.com/" rel="noopener noreferrer"&gt;Practical Web Tools&lt;/a&gt;, a site with ~500 calculators (finance, math, health, science, everyday) that all run 100% client-side. So I opened the whole catalog up as free embeds. This post is how the embed surface works and what it cost to make it not awful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways to embed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. One-line script widgets.&lt;/strong&gt; Nine flagship widgets — mortgage, loan/amortization, compound interest, BMI, calorie/TDEE, pregnancy due-date, percentage, countdown timer, word counter — each install as two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"pwt-mortgage-widget"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://practicalwebtools.com/embed/mortgage-widget.js"&lt;/span&gt; &lt;span class="na"&gt;defer&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are self-contained static scripts (a shared ~4KB engine renders them). Crucially, there is &lt;strong&gt;no iframe pointing at my servers&lt;/strong&gt;: once the script is on your page, it runs entirely in your visitor's browser. If my site goes down, your embedded widget keeps working. Nothing phones home, because there is no home to phone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Any calculator as an auto-sizing iframe.&lt;/strong&gt; Every one of the ~500 calculators doubles as a widget at &lt;code&gt;&amp;lt;calculator URL&amp;gt;?embed=1&lt;/code&gt;. Each calculator page shows its own copy-paste code; it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://practicalwebtools.com/finance/mortgage-calculator?embed=1"&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Mortgage Calculator"&lt;/span&gt; &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt;
        &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"display:block;width:100%;max-width:880px;height:720px;border:0"&lt;/span&gt;
        &lt;span class="na"&gt;data-pwt-embed&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://practicalwebtools.com/embed/frame.js"&lt;/span&gt; &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"margin:6px 0 0;font:13px/1.5 system-ui,sans-serif"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Mortgage Calculator by &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://practicalwebtools.com/finance/mortgage-calculator"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Practical Web Tools&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The embed URL renders the calculator with all site chrome stripped: no navigation, no ads, no cookie banner — just the tool. A &lt;code&gt;theme=dark&lt;/code&gt; parameter pins a dark build when your site has a dark article layout.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the auto-resize handshake works
&lt;/h2&gt;

&lt;p&gt;The classic iframe-embed wart is the fixed height: too short and you get an inner scrollbar, too tall and there's a blank canyon under the widget. The fix is a tiny postMessage handshake.&lt;/p&gt;

&lt;p&gt;Inside the frame, the calculator measures itself on load and on every DOM resize, then posts its height up:&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;// inside the framed calculator&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;send&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;offsetHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;offsetHeight&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'pwt-embed:resize'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'*'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ResizeObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the host page, a framework-free, ES5-safe script (&lt;code&gt;frame.js&lt;/code&gt;, ~1KB) listens for those messages and sizes the matching iframe:&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'https://practicalwebtools.com'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'pwt-embed:resize'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// find the iframe whose contentWindow sent this, then:&lt;/span&gt;
  &lt;span class="n"&gt;iframe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;160&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'px'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the host's script loads after the frame already reported (script order at the bottom of the page), the host pings back with &lt;code&gt;{type:'pwt-embed:ping'}&lt;/code&gt; and the frame re-sends. Net effect: paste the snippet, get a widget that fits, zero configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that bit me along the way
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Third-party frames deny storage.&lt;/strong&gt; Browsers often throw on &lt;code&gt;localStorage&lt;/code&gt; access inside cross-origin iframes. Several calculators persist their inputs, so the embed bootstrap probes storage and shims in an in-memory fallback (&lt;code&gt;getItem&lt;/code&gt;/&lt;code&gt;setItem&lt;/code&gt;/&lt;code&gt;removeItem&lt;/code&gt;/&lt;code&gt;key&lt;/code&gt;/&lt;code&gt;length&lt;/code&gt;) before any tool code runs. Without it, half the catalog would crash on frame load in Safari.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Height feedback loops.&lt;/strong&gt; If the framed page has any viewport-sized element, applying the reported height changes the layout, which reports a new height, forever. The bootstrap detects same-step growth and stops reporting after five iterations rather than stretching the iframe into a scroll bomb.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ads and embeds don't mix.&lt;/strong&gt; The site is ad-supported; embeds load zero ads and zero tracking scripts. A widget that renders ads on somebody else's article is a parasite, and site owners can tell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Theme defaults matter.&lt;/strong&gt; "Follow the visitor's OS" sounds nice until a dark widget paints itself into a light article. Default is light, with an explicit opt-in for dark.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who these are for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real estate / lending blogs&lt;/strong&gt; — &lt;a href="https://practicalwebtools.com/finance/mortgage-calculator" rel="noopener noreferrer"&gt;mortgage calculator&lt;/a&gt;, loan &amp;amp; amortization with a yearly schedule&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fitness and nutrition sites&lt;/strong&gt; — &lt;a href="https://practicalwebtools.com/health/bmi-calculator" rel="noopener noreferrer"&gt;BMI&lt;/a&gt; and &lt;a href="https://practicalwebtools.com/health/calorie-calculator" rel="noopener noreferrer"&gt;calorie/TDEE (Mifflin-St Jeor)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deal blogs and retail&lt;/strong&gt; — &lt;a href="https://practicalwebtools.com/math/percentage-calculator" rel="noopener noreferrer"&gt;percentage calculator&lt;/a&gt; (percent-off, % change, is-what-%)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parenting and pregnancy sites&lt;/strong&gt; — due-date with trimester breakdown&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal finance writers&lt;/strong&gt; — compound interest with contribution schedule&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writing/SEO blogs&lt;/strong&gt; — live word &amp;amp; character counter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every calculator's "Embed this calculator" section has a theme picker and a live preview of exactly what your visitors will see, and the snippets work anywhere that accepts custom HTML: WordPress, Squarespace, Wix, Webflow, Ghost, GitHub Pages, documentation sites.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch
&lt;/h2&gt;

&lt;p&gt;There is exactly one: a small &lt;code&gt;Calculator by Practical Web Tools&lt;/code&gt; link with each widget. No API key, no signup, no view quota, no "upgrade for dark mode," no email wall. If that trade works for you, the full widget catalog is here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://practicalwebtools.com/embed" rel="noopener noreferrer"&gt;Free Embeddable Calculator Widgets — the full catalog&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And you can browse all ~500 calculators (every one embeddable from its own page) at &lt;a href="https://practicalwebtools.com/calculators" rel="noopener noreferrer"&gt;practicalwebtools.com/calculators&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you embed one, drop a comment with where — I keep a list and I'm taking requests for which calculator gets a dedicated one-line script next.&lt;/p&gt;

&lt;p&gt;Tags: webdev, javascript, widgets, productivity&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>widgets</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Does Re-Saving a JPG Actually Destroy Quality? I Measured 7 Generations of Pixel Loss</title>
      <dc:creator>Joseph Orduna</dc:creator>
      <pubDate>Fri, 06 Nov 2026 03:55:56 +0000</pubDate>
      <link>https://dev.to/joseph_orduna_b458c7df808/does-re-saving-a-jpg-actually-destroy-quality-i-measured-7-generations-of-pixel-loss-o50</link>
      <guid>https://dev.to/joseph_orduna_b458c7df808/does-re-saving-a-jpg-actually-destroy-quality-i-measured-7-generations-of-pixel-loss-o50</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://practicalwebtools.com/blog/does-re-saving-a-jpg-lose-quality-i-tested-7-generations" rel="noopener noreferrer"&gt;Practical Web Tools&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Does re-saving a JPG destroy your photo? The honest answer surprised me: I took one test image, re-saved it seven times in a row, and measured the pixel-level damage after every generation. One chain barely changed at all. Another accumulated 7.6 times the error of a single save: with almost no warning in the file size.&lt;/p&gt;

&lt;p&gt;The difference between those two outcomes isn't luck. It's three specific behaviors: whether the quality setting stays the same, whether the codec stays the same, and whether each re-save secretly drops the quality a little lower. Once you see the numbers, you'll never guess about image degradation again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaway: re-saving at the &lt;em&gt;same&lt;/em&gt; quality with the &lt;em&gt;same&lt;/em&gt; codec is nearly harmless. Switching codecs or letting quality slide is what actually eats your photos.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Ran the Test
&lt;/h2&gt;

&lt;p&gt;Everything below comes from a real experiment, not vendor documentation. Here's the setup so you can judge the results yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test image:&lt;/strong&gt; a 1,200 x 800 photographic-style scene: smooth gradients (sky-like), sensor-style noise, and hard dark rectangles mimicking signage and text edges. High-frequency detail plus smooth areas is exactly the mix that stresses image codecs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codecs:&lt;/strong&gt; JPEG and WebP, both at quality 80 for the fixed-quality chains, then a descending ladder from quality 90 down to 30.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measurement:&lt;/strong&gt; RMSE (root-mean-square error) between each generation and the pristine original, on the 0-255 pixel scale, plus file size at every step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A rough guide to reading RMSE numbers: &lt;strong&gt;around 3 is barely visible&lt;/strong&gt;, around 8-10 is visible softness and artifacts when you zoom, and above 20 is objectively mushy. The exact thresholds depend on the image, but the &lt;em&gt;trend&lt;/em&gt; is what matters.&lt;/p&gt;

&lt;p&gt;I ran three fixed-quality chains first: JPEG only, WebP only, and alternating between the two: then the descending ladders that mimic what social platforms actually do to your uploads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Myth #1: "Every Re-Save Destroys Your Photo"
&lt;/h2&gt;

&lt;p&gt;This is the one everybody repeats, and it's mostly false. Here's what happened when I re-saved the same image seven consecutive times at quality 80, without changing anything else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JPEG -&amp;gt; JPEG, seven saves:&lt;/strong&gt; error went from 3.86 after the first save to 3.87 after the seventh. That's a 0.3% increase: effectively zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebP -&amp;gt; WebP, seven saves:&lt;/strong&gt; error went from 3.86 to 4.10. Again, nearly flat.&lt;/li&gt;
&lt;li&gt;File sizes in both chains stabilized after the second generation and then stopped moving.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The first save is the expensive one. Every same-quality re-save after that is almost free.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why? Modern encoders are deterministic. When a JPEG at quality 80 is re-encoded at quality 80, the quantization tables match, so the coefficients that survived the first pass mostly survive the second. The damage was already done in generation one; re-applying the same math barely compounds.&lt;/p&gt;

&lt;p&gt;So if you've been keeping a "do not touch" JPEG out of fear that opening and re-saving it will grind it into dust: you can relax, as long as your editor doesn't quietly re-compress at a lower quality behind your back. That caveat is where the real damage lives, and it's next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Culprit #1: Switching Codecs Back and Forth
&lt;/h2&gt;

&lt;p&gt;Now watch what happens when the &lt;em&gt;quality setting stays identical&lt;/em&gt; but the codec alternates every save: JPG to WebP to JPG to WebP, seven times at quality 80:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generation 1 (WebP): error 3.86&lt;/li&gt;
&lt;li&gt;Generation 2 (back to JPG): 4.74: a 23% jump from one codec switch&lt;/li&gt;
&lt;li&gt;Generation 4: 4.93&lt;/li&gt;
&lt;li&gt;Generation 7: 5.02: and still climbing slowly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each hop between formats re-quantizes the image in a &lt;em&gt;different mathematical basis&lt;/em&gt;. JPEG throws away information in 8x8 DCT blocks; WebP does predictive coding plus its own transform. They don't throw away the &lt;em&gt;same&lt;/em&gt; information, so every switch carves out a fresh, slightly different slice of detail.&lt;/p&gt;

&lt;p&gt;The file sizes told on the chain too: they oscillated between roughly 64 KB and 120 KB depending on which codec had just touched the file, because each format compresses the other's leftovers with different efficiency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical rule: converting formats is a one-way street. Every JPG&amp;lt;-&amp;gt;WebP&amp;lt;-&amp;gt;PNG round trip adds loss, even at identical quality settings.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the trap behind "I just converted it to WebP for my website, then back to JPG for an email, then to WebP again." Three conversions, three fresh rounds of loss. If you need a JPG from an original, go back to the original each time: a proper &lt;a href="https://practicalwebtools.com/convert/image/jpg-to-webp" rel="noopener noreferrer"&gt;JPG to WebP converter&lt;/a&gt; exists so you can produce the web copy &lt;em&gt;from the master&lt;/em&gt;, not from the previous conversion. Same logic in reverse for &lt;a href="https://practicalwebtools.com/convert/image/webp-to-jpg" rel="noopener noreferrer"&gt;WebP back to JPG&lt;/a&gt; when a form or app demands JPEG.&lt;/p&gt;

&lt;h2&gt;
  
  
  Culprit #2: The Descending Quality Ladder
&lt;/h2&gt;

&lt;p&gt;Here's the killer, and it's almost never the user's choice: it's what messaging apps and social platforms do silently. Each upload gets re-compressed, often a bit more aggressively than the last. I simulated that with a quality ladder from 90 down to 30, one step per save:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JPEG ladder:&lt;/strong&gt; error climbed 2.96 -&amp;gt; 4.99 -&amp;gt; 7.35 -&amp;gt; 10.01 -&amp;gt; 12.88 -&amp;gt; 16.54 -&amp;gt; &lt;strong&gt;22.42&lt;/strong&gt;. By the final step, the image had 7.6x the damage of a single careful save. That's visibly mushy territory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebP ladder, same descent:&lt;/strong&gt; 2.18 -&amp;gt; 4.38 -&amp;gt; 5.32 -&amp;gt; 5.91 -&amp;gt; 6.60 -&amp;gt; 7.36 -&amp;gt; &lt;strong&gt;8.69&lt;/strong&gt;. Serious, but roughly a third of JPEG's damage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alternating ladder (JPG/WebP with quality dropping):&lt;/strong&gt; ended around 7.0: the WebP hops cushioned the JPEG hits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;One app "optimizing" your upload costs more than five disciplined re-saves at home.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The image-degradation mechanism is simple: once quality 70 has smeared the fine detail away, quality 60 can't preserve what no longer exists: and it smears the &lt;em&gt;remaining&lt;/em&gt; structure further. Loss compounds because information never comes back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trap: File Size Stays Flat While Quality Falls
&lt;/h2&gt;

&lt;p&gt;The most useful discovery for everyday sanity checks: in the JPEG ladder, file size collapsed from 226 KB to about 154 KB by the third step: and then &lt;strong&gt;plateaued&lt;/strong&gt;. Steps 3 through 7 all sat near 153-155 KB while error nearly tripled.&lt;/p&gt;

&lt;p&gt;So "the file is still 150 KB, it must be fine" is exactly wrong. Later destructive saves can't add bytes back; they spend their bits re-encoding damage. Size tells you how much data survived, not how faithful it is to the original scene.&lt;/p&gt;

&lt;p&gt;If you're comparing two versions of an image you downloaded and re-uploaded a few times, don't trust the byte count. Open both at 100% zoom next to the original (or the oldest copy you have) and look at edges: text, hair, brickwork. Edges are where generational loss shows first.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebP's Unexpected Silver Lining
&lt;/h2&gt;

&lt;p&gt;Two findings favored WebP beyond its famous file-size advantage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At matched quality numbers, the WebP-only chain stabilized at 95 KB versus JPEG's 123 KB: 23% smaller at essentially equal measured error.&lt;/li&gt;
&lt;li&gt;On the descending ladder, WebP degraded far more gracefully (final error 8.69 vs 22.42), because its compression at low quality settings stays closer to the image structure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That second point matters for anything that passes through other people's pipelines: blog images, marketplace listings, chat apps. Starting from WebP doesn't make re-compression &lt;em&gt;impossible&lt;/em&gt;, but it leaves more headroom before things fall apart.&lt;/p&gt;

&lt;p&gt;This is also why converting old JPG archives to WebP makes sense &lt;em&gt;once, from the original&lt;/em&gt;, and not repeatedly. A single thoughtful conversion captures the size win; repeated cycles capture the loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five Rules That Fall Out of the Data
&lt;/h2&gt;

&lt;p&gt;Everything above compresses (pun intended) into five habits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep a pristine master.&lt;/strong&gt; The original camera file, or a lossless PNG copy, never gets overwritten. All conversions branch from it. Saving your master as PNG is trivial with a &lt;a href="https://practicalwebtools.com/convert/image/jpg-to-png" rel="noopener noreferrer"&gt;JPG to PNG converter&lt;/a&gt;: PNG is lossless, so it freezes the current quality forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convert from the master, never from the previous conversion.&lt;/strong&gt; This single habit eliminates the compounding chain entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When you must re-save, keep quality constant or higher.&lt;/strong&gt; Same codec plus same quality is nearly free, per the flat RMSE lines above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat every upload to a platform as a lossy event.&lt;/strong&gt; Send the highest-quality version the platform accepts; it has the most headroom to survive their ladder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch edges, not file size.&lt;/strong&gt; Zoom to 100% on text or hair. Byte counts plateau while quality keeps falling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you maintain a website, the same rules apply to your image pipeline: compress once at publish time, not at every edit. For batches, running files through a proper &lt;a href="https://practicalwebtools.com/edit/compress-files" rel="noopener noreferrer"&gt;compression tool&lt;/a&gt; once: instead of letting your CMS re-compress on every save: keeps generations at one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means for Your Photo Library
&lt;/h2&gt;

&lt;p&gt;Think in terms of a family tree. The master is the trunk; every purpose-built copy (web-optimized WebP, emailed JPG, printed PNG) is a branch cut fresh from the trunk. Branches never grow from other branches.&lt;/p&gt;

&lt;p&gt;People who lose photos to generational loss almost always broke that rule by accident: downloaded from WhatsApp, edited, re-saved, uploaded, downloaded again. Five years later the picture looks like a VHS tape: and no single step was obviously to blame.&lt;/p&gt;

&lt;p&gt;The math says you'd need dozens of disciplined same-quality re-saves to equal the damage of one careless month on social platforms. Protect the trunk, be careless with branches, and your images will outlive every format war.&lt;/p&gt;

&lt;h2&gt;
  
  
  The PNG Island Experiment: When Lossless Silently Isn't
&lt;/h2&gt;

&lt;p&gt;A reader scenario worth testing directly: you keep a PNG working copy of a photo that originally arrived as a JPG. Every PNG re-save is lossless, so you're safe forever, right? Mostly: but with one expensive catch I measured.&lt;/p&gt;

&lt;p&gt;Converting the damaged JPG (generation 3 of the mixed chain) to PNG froze the damage at exactly that level: every later PNG re-save was pixel-identical, confirmed by RMSE staying flat at 4.77 across five further saves. Lossless holds. &lt;strong&gt;But the PNG could not give back what the JPG had already thrown away&lt;/strong&gt;: and it cost 1,078 KB, eleven times the 95 KB WebP copy, to store a permanently mid-generation image.&lt;/p&gt;

&lt;p&gt;That leads to a rule almost nobody follows: convert to PNG only at &lt;em&gt;milestones&lt;/em&gt;: final edit, signed-off version, archive point: not as a daily working format for photos. The right order of operations is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep the true original untouched (camera file or first-generation PNG).&lt;/li&gt;
&lt;li&gt;Edit and derive freely, converting straight from the original each time.&lt;/li&gt;
&lt;li&gt;Freeze a lossless copy when a version is final and must never degrade again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The failure mode to avoid: importing a WhatsApp JPG into your editor, saving as PNG for safety, editing for weeks, then wondering why the final export looks softer than the photo you remember taking. The damage was imported with the file; PNG just made it permanent and 11x heavier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spotting Ladder Damage in the Wild (a 60-Second Audit)
&lt;/h2&gt;

&lt;p&gt;You can run this audit on any suspicious image you've downloaded, without any tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zoom to 100% on text and edges.&lt;/strong&gt; Ladder damage shows as smeared letterforms and halos around high-contrast boundaries: the JPEG 8x8 grid becomes visible as faint blocking when quality dropped below ~50.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look at flat areas like sky or walls.&lt;/strong&gt; Early ladder steps keep them clean; late steps show banding and blotchy color drift, because fine noise was destroyed and the encoder started quantizing structure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the pixel dimensions against the source.&lt;/strong&gt; Platforms downscale aggressively alongside re-compression; a photo you shot at 4032 px coming back at 1080 px has been through at least one destructive pass regardless of what the app claims.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare file size to siblings.&lt;/strong&gt; Among photos from the same shoot, the one that's dramatically smaller at identical dimensions has been recompressed hardest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the audit fails: smeared text plus banding: the copy is a late-generation derivative. Don't restore from it; go back up the chain to the oldest copy you can find. And when you must produce new derivatives of a photo whose original is lost, convert once to a lossless PNG snapshot &lt;em&gt;before&lt;/em&gt; any further edits, so the bleeding stops there.&lt;/p&gt;

&lt;p&gt;For everything you want to test on your own files, the same conversions I used are free and run entirely in your browser: &lt;a href="https://practicalwebtools.com/convert/image/jpg-to-png" rel="noopener noreferrer"&gt;JPG to PNG&lt;/a&gt; for lossless freezing, &lt;a href="https://practicalwebtools.com/convert/image/jpg-to-webp" rel="noopener noreferrer"&gt;JPG to WebP&lt;/a&gt; for web copies, and &lt;a href="https://practicalwebtools.com/convert/image/png-to-jpg" rel="noopener noreferrer"&gt;PNG to JPG&lt;/a&gt; when a form demands JPEG.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Does re-saving a JPG at the same quality setting lose quality?&lt;/strong&gt;&lt;br&gt;
Almost none. In my test, seven consecutive JPEG saves at quality 80 moved measured error from 3.86 to 3.87: a 0.3% change. The first save does the damage; same-quality re-saves barely compound it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is converting JPG to WebP lossy?&lt;/strong&gt;&lt;br&gt;
Yes, one round of loss at conversion time: WebP lossy mode re-encodes the pixels. But at matched quality settings WebP was both smaller (95 KB vs 123 KB in my chain) and gentler under repeated re-compression. Convert once from the original, not in cycles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many times can an image be re-saved before quality loss is visible?&lt;/strong&gt;&lt;br&gt;
At constant quality with one codec: dozens of times with little visible change. With codec switching each time: visible softness can appear within a handful of conversions. With quality dropping each save (the platform ladder): visible damage by the third or fourth step in my test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why did my file size stay the same while the image got worse?&lt;/strong&gt;&lt;br&gt;
Because later lossy saves re-encode existing damage rather than adding new information. My JPEG ladder sat at ~154 KB for five consecutive steps while measured error nearly tripled. File size measures surviving data, not fidelity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the best format for keeping a master copy?&lt;/strong&gt;&lt;br&gt;
A lossless format: the original camera file (RAW or highest-quality JPG) or a PNG copy. PNG re-saves are pixel-perfect forever, so your master never degrades no matter how many times you open or copy it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updated November 3, 2026: The Screenshot Addendum: Flat Graphics Decay Differently
&lt;/h2&gt;

&lt;p&gt;Every test above used photographs. But a reader scenario kept nagging at me: documentation screenshots, diagrams, and exported charts get the same edit-save-edit treatment in JPG, and nobody had measured &lt;em&gt;that&lt;/em&gt; class of image. So I ran the chain again on a flat UI screenshot (1600x900: solid fills, a nav bar, text lines rendered as bars): and the results flip two assumptions from the photo tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  The size plateau is identical, the decay isn't
&lt;/h3&gt;

&lt;p&gt;Five re-saves at a fixed JPEG quality 85, measured against the pristine original each time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File size:&lt;/strong&gt; 56 KB -&amp;gt; 57 -&amp;gt; 57 -&amp;gt; 57 -&amp;gt; 57 KB. Dead flat, exactly like the photo ladder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pixel drift (RMS):&lt;/strong&gt; 2.06 at the first encode, then 2.47 -&amp;gt; 2.68 -&amp;gt; 2.87 -&amp;gt; 3.01 across the five re-saves. Same plateau-then-creep shape as photographs, landing in the "barely visible" band overall.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The detail that matters:&lt;/strong&gt; by generation five, the &lt;strong&gt;maximum channel error hit 76 on the 0-255 scale&lt;/strong&gt;: concentrated exactly where you'd predict: text edges and button borders. Averages hide what screenshots are &lt;em&gt;for&lt;/em&gt;. Nobody zooms into sky gradient; everybody zooms into the menu they're trying to read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The practical takeaway: for flat graphics, judge re-save damage by the edges, not the average.&lt;/strong&gt; A screenshot can sit at an innocent-looking RMS of 3 while its worst text edges are already ghosting at 30% intensity error.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why screenshots are the worst-case JPG in the first place
&lt;/h3&gt;

&lt;p&gt;The pristine screenshot weighs 6.6 KB as a PNG and 56 KB as a quality-85 JPG: the lossy format is 8.5x &lt;em&gt;bigger&lt;/em&gt; for visibly worse text rendering. So the real fix is upstream: flat graphics should never enter a JPEG chain at all. I've written up the full four-image-type experiment (screenshots, photos, logos, photo-plus-text overlays, including the JPG-&amp;gt;PNG round-trip size trap) in a companion piece: &lt;a href="https://practicalwebtools.com/blog/png-vs-jpg-i-tested-four-image-types-the-loser-was-8x-bigger" rel="noopener noreferrer"&gt;PNG vs JPG: I Tested Four Image Types&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The two-step recovery drill
&lt;/h3&gt;

&lt;p&gt;If you're maintaining documentation screenshots that have already been through a JPG chain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stop re-saving the damaged file.&lt;/strong&gt; Each export from the previous export adds another ~0.15 RMS and compounds the edge ghosting: the plateau is forgiving, but it isn't free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-capture from the source when possible.&lt;/strong&gt; A fresh screenshot (PNG) beats any attempted cleanup of a five-generation JPG, and it will be roughly 8x smaller. For legacy PNG assets that need to travel as JPG, convert the &lt;em&gt;master&lt;/em&gt;, not the worn copy: the &lt;a href="https://practicalwebtools.com/convert/png-to-jpg" rel="noopener noreferrer"&gt;browser-based JPG converter&lt;/a&gt; never touches your original file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compress last, from the master.&lt;/strong&gt; When a doc pipeline demands small files, resize first, then compress in one pass with a &lt;a href="https://practicalwebtools.com/edit/compress-image" rel="noopener noreferrer"&gt;client-side image compressor&lt;/a&gt;: never through iterative saves.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://practicalwebtools.com/blog/does-re-saving-a-jpg-lose-quality-i-tested-7-generations" rel="noopener noreferrer"&gt;Practical Web Tools&lt;/a&gt;. Explore all client-side browser utilities at &lt;a href="https://practicalwebtools.com" rel="noopener noreferrer"&gt;practicalwebtools.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>images</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Which Local LLM Fits Your RAM? Best Ollama Models for 4GB, 8GB, 16GB, and 32GB in 2026</title>
      <dc:creator>Joseph Orduna</dc:creator>
      <pubDate>Fri, 06 Nov 2026 03:55:51 +0000</pubDate>
      <link>https://dev.to/joseph_orduna_b458c7df808/which-local-llm-fits-your-ram-best-ollama-models-for-4gb-8gb-16gb-and-32gb-in-2026-2dmf</link>
      <guid>https://dev.to/joseph_orduna_b458c7df808/which-local-llm-fits-your-ram-best-ollama-models-for-4gb-8gb-16gb-and-32gb-in-2026-2dmf</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://practicalwebtools.com/blog/best-ollama-models-by-ram-what-to-run-on-4gb-8gb-16gb-and-32gb-in-2026" rel="noopener noreferrer"&gt;Practical Web Tools&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The most common local-AI question in 2026 is still the simplest one: "I have X GB of RAM, what should I run?" The answer changed a lot in the last year, because newer model generations got dramatically better at small sizes. A 4B model from the current crop beats 7B models from two years ago, which means old "best models" lists are actively misleading.&lt;/p&gt;

&lt;p&gt;Here's the current pick per RAM tier, based on actually running these on real hardware, with honest speed expectations for CPU versus GPU. (Choosing between Ollama and llama.cpp as the runtime is a separate question: &lt;a href="https://practicalwebtools.com/blog/ollama-vs-llama-cpp-in-2026-an-honest-comparison-for-local-ai-users" rel="noopener noreferrer"&gt;we compared the two here&lt;/a&gt;; this guide assumes either.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The one rule that decides everything
&lt;/h2&gt;

&lt;p&gt;Model weights at Q4 quantization (the sweet spot for quality per memory) need roughly &lt;strong&gt;0.6-0.7 GB per billion parameters&lt;/strong&gt;, plus 1-3 GB of overhead for context and the KV cache. Quick mental math for a machine with 16 GB RAM:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Leave 4+ GB for the OS and browser -&amp;gt; ~12 GB usable&lt;/li&gt;
&lt;li&gt;That's a &lt;strong&gt;14B model at Q4&lt;/strong&gt; comfortably, or a 24B at Q4 if you close everything else and keep context short&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have a GPU, the constraint is VRAM instead. GPU VRAM is worth roughly 3-5x CPU RAM for usefulness, because offloaded layers generate several times faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  4 GB RAM: small, fast, surprisingly capable
&lt;/h2&gt;

&lt;p&gt;The era of "4 GB can't run LLMs" is over. Current small models handle summarization, rewriting, and simple Q&amp;amp;A genuinely well.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Size on disk&lt;/th&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;CPU speed feel&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3 1.7B (Q4)&lt;/td&gt;
&lt;td&gt;~1.1 GB&lt;/td&gt;
&lt;td&gt;32k&lt;/td&gt;
&lt;td&gt;Snappy, instant-feeling replies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.2 3B (Q4)&lt;/td&gt;
&lt;td&gt;~2.0 GB&lt;/td&gt;
&lt;td&gt;128k&lt;/td&gt;
&lt;td&gt;Usable, a paragraph in a few seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phi-4-mini (Q4)&lt;/td&gt;
&lt;td&gt;~2.6 GB&lt;/td&gt;
&lt;td&gt;128k&lt;/td&gt;
&lt;td&gt;Slightly slower, strong reasoning for its size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 3 4B (Q4)&lt;/td&gt;
&lt;td&gt;~2.6 GB&lt;/td&gt;
&lt;td&gt;128k&lt;/td&gt;
&lt;td&gt;Comfortable if it's the only big app open&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Pick:&lt;/strong&gt; Qwen3 1.7B for speed, Gemma 3 4B if you can spare the RAM. Avoid anything 7B+; it'll thrash.&lt;/p&gt;

&lt;h2&gt;
  
  
  8 GB RAM: the sweet spot for capable small models
&lt;/h2&gt;

&lt;p&gt;8 GB machines (base Mac mini territory, older laptops) are the first tier where local AI feels like a real assistant rather than a toy.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Size on disk&lt;/th&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.1 8B / Llama 3.8B-class (Q4)&lt;/td&gt;
&lt;td&gt;~4.7 GB&lt;/td&gt;
&lt;td&gt;128k&lt;/td&gt;
&lt;td&gt;The default pick; balanced and reliable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3 8B (Q4)&lt;/td&gt;
&lt;td&gt;~5.0 GB&lt;/td&gt;
&lt;td&gt;32k+&lt;/td&gt;
&lt;td&gt;Stronger coding and math, similar speed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 3 12B (Q4)&lt;/td&gt;
&lt;td&gt;~7.3 GB&lt;/td&gt;
&lt;td&gt;128k&lt;/td&gt;
&lt;td&gt;Fits, but close everything else; strong multimodal option&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek-R1 distill 7B/8B (Q4)&lt;/td&gt;
&lt;td&gt;~4.5-5 GB&lt;/td&gt;
&lt;td&gt;64k&lt;/td&gt;
&lt;td&gt;Visible chain-of-thought reasoning; fun to watch think&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Pick:&lt;/strong&gt; Qwen3 8B or Llama 3.1 8B. Go Gemma 3 12B only if 6+ GB is genuinely free. At Q4 with a browser open, 12B on 8 GB is flirting with swap.&lt;/p&gt;

&lt;h2&gt;
  
  
  16 GB RAM: where local AI gets serious
&lt;/h2&gt;

&lt;p&gt;This is the tier most mid-range laptops and desktops sit at in 2026, and it lets you run 14B-class models that rival cloud models from a couple of years ago.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Size on disk&lt;/th&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3 14B (Q4)&lt;/td&gt;
&lt;td&gt;~9 GB&lt;/td&gt;
&lt;td&gt;32k+&lt;/td&gt;
&lt;td&gt;The best all-rounder at this tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phi-4 14B (Q4)&lt;/td&gt;
&lt;td&gt;~9 GB&lt;/td&gt;
&lt;td&gt;16k&lt;/td&gt;
&lt;td&gt;Excellent reasoning, shorter context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 3 27B (Q4)&lt;/td&gt;
&lt;td&gt;~16 GB&lt;/td&gt;
&lt;td&gt;128k&lt;/td&gt;
&lt;td&gt;Only fits at Q3 or with very short context, so usually skip on 16 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek-R1 distill 14B (Q4)&lt;/td&gt;
&lt;td&gt;~9 GB&lt;/td&gt;
&lt;td&gt;64k&lt;/td&gt;
&lt;td&gt;Reasoning workloads, math, logic puzzles&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Pick:&lt;/strong&gt; Qwen3 14B at Q4 as the daily driver. On CPU expect roughly 6-10 tokens/sec: reading speed, fine for chat. With a GPU holding most layers, 25-40 tokens/sec.&lt;/p&gt;

&lt;h2&gt;
  
  
  32 GB RAM: flagship territory
&lt;/h2&gt;

&lt;p&gt;32 GB comfortably runs the models people actually mean when they say "local AI is as good as the cloud now."&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Size on disk&lt;/th&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3 32B (Q4)&lt;/td&gt;
&lt;td&gt;~20 GB&lt;/td&gt;
&lt;td&gt;32k+&lt;/td&gt;
&lt;td&gt;GPT-4-class quality for most everyday tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 3 27B (Q4)&lt;/td&gt;
&lt;td&gt;~16 GB&lt;/td&gt;
&lt;td&gt;128k&lt;/td&gt;
&lt;td&gt;Now comfortable; multimodal; excellent writing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3 30B-A3B (MoE) (Q4)&lt;/td&gt;
&lt;td&gt;~18 GB&lt;/td&gt;
&lt;td&gt;32k+&lt;/td&gt;
&lt;td&gt;The speed hack: MoE runs like a 3B, thinks like a 30B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.3 70B (Q2/Q3)&lt;/td&gt;
&lt;td&gt;~26 GB&lt;/td&gt;
&lt;td&gt;128k&lt;/td&gt;
&lt;td&gt;Fits only at aggressive quants; quality loss is real but usable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Pick:&lt;/strong&gt; Qwen3 30B-A3B if you want speed, Gemma 3 27B or Qwen3 32B for maximum quality. On CPU, dense 32B is slow (~3-5 tok/s). This tier really wants a GPU with 12+ GB VRAM, where it flies at 20-40 tok/s.&lt;/p&gt;

&lt;h2&gt;
  
  
  Speed expectations: set them before you install
&lt;/h2&gt;

&lt;p&gt;Honest numbers for Q4 models, single-user chat:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Modern CPU only:&lt;/strong&gt; 7-8B -&amp;gt; 10-20 tok/s; 14B -&amp;gt; 6-10 tok/s; 32B -&amp;gt; 3-5 tok/s&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU (8 GB VRAM, most layers offloaded):&lt;/strong&gt; 7-8B -&amp;gt; 40-70 tok/s; 14B -&amp;gt; 25-40 tok/s&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU (16+ GB VRAM):&lt;/strong&gt; 32B fully offloaded -&amp;gt; 20-40 tok/s&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anything over ~15 tokens per second feels instant for chat. Under 5 is where people give up, so on CPU-only machines, cap yourself at 14B.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother running locally at all?
&lt;/h2&gt;

&lt;p&gt;Fair question, when cloud chatbots are one tab away. Two answers. Privacy: a local model's context never leaves your machine, which changes what you can responsibly paste into it. No bank statements or medical records riding to somebody else's server (&lt;a href="https://practicalwebtools.com/blog/is-it-safe-to-upload-bank-statements-ids-and-photos-to-chatgpt-what-actually-happens-to-your-files" rel="noopener noreferrer"&gt;we wrote up exactly what happens to files you upload to ChatGPT&lt;/a&gt;, and the local answer to it). And cost: the runtime price of a local model is your electricity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started takes five minutes
&lt;/h2&gt;

&lt;p&gt;Install Ollama, run &lt;code&gt;ollama pull qwen3:14b&lt;/code&gt;, and you're off. The nicer upgrade is skipping the terminal entirely: a browser chat interface that talks to your local models keeps history on your device and feels like a normal AI product. The &lt;a href="https://practicalwebtools.com/ai-chat" rel="noopener noreferrer"&gt;private AI chat on Practical Web Tools&lt;/a&gt; does exactly that: no account, no uploads, works with locally running models and cloud models alike.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Does GPU VRAM count toward these tiers?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes: arguably more than system RAM. A model fully held in VRAM runs several times faster than one split across CPU RAM. An 8 GB GPU running an 8B model beats 32 GB of RAM running the same model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Q4 quantization really good enough?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For 8B+ models, Q4_K_M-type quants lose roughly 1-3% versus full precision on most benchmarks, which is imperceptible in chat. Below 4B parameters, quantization hurts more; consider Q6/Q8 for tiny models since they're small anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I run a model bigger than my RAM?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Technically yes via mmap streaming, practically no: it's orders of magnitude slower and will hammer your SSD. Stay within the tier tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is my model so slow even though it fits?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three usual suspects: context window set very high (the KV cache eats bandwidth), the model split between CPU and GPU awkwardly, or a browser with 40 tabs competing for RAM. Check all three before blaming the model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are these model names stable on Ollama's registry?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tags shift as versions update: &lt;code&gt;qwen3:14b&lt;/code&gt; today may point to a newer revision later. Pin exact tags if reproducibility matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which should I install: Ollama or llama.cpp?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For almost everyone reading a RAM-tier guide: Ollama. The differences only matter once you're chasing per-watt performance or exotic hardware: &lt;a href="https://practicalwebtools.com/blog/ollama-vs-llama-cpp-in-2026-an-honest-comparison-for-local-ai-users" rel="noopener noreferrer"&gt;the full comparison is here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updated October 2026: gpt-oss changes the 16 GB math
&lt;/h2&gt;

&lt;p&gt;When this guide was written, 16 GB machines were capped at dense 14B models. OpenAI's open-weight release changed that: &lt;strong&gt;gpt-oss:20b&lt;/strong&gt; is a Mixture-of-Experts model (21B total parameters, only ~3.6B active per token) shipped in MXFP4 quantization, so it downloads at roughly 13-14 GB and runs inside 16 GB of memory. That's a model class that used to need 32 GB.&lt;/p&gt;

&lt;p&gt;What that means per tier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;16 GB (GPU or Mac unified memory):&lt;/strong&gt; gpt-oss:20b is now the headline pick, with flagship-class answers at usable speed and built-in reasoning that you can dial from low to high.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;32 GB CPU-only machines:&lt;/strong&gt; the 20b runs comfortably on pure CPU (some CPU-only setups want the full 32 GB to breathe), which was impossible for a dense 20B+ model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4-8 GB:&lt;/strong&gt; nothing changes. MoE total size still has to fit in memory, and 13 GB does not. Qwen3 and Gemma 3 small quants remain the right answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;gpt-oss:120b&lt;/strong&gt; (~61-65 GB download) stays Mac Studio / 80 GB+ territory and is not realistic on any laptop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two honest trade-offs: MoE speed comes from activating few parameters, so at equal &lt;em&gt;active&lt;/em&gt; size a dense model can still edge it on some narrow tasks. But the jump from a 14B dense to a 21B-MoE in quality is unmistakable. And it's a reasoning-first model: it thinks before answering by default, which is superb for hard questions and mildly tedious for "rewrite this email."&lt;/p&gt;

&lt;p&gt;Pull it the usual way: &lt;code&gt;ollama run gpt-oss:20b&lt;/code&gt;. If you'd rather test the model class in a browser before downloading 13 GB, the site's &lt;a href="https://practicalwebtools.com/ai-chat" rel="noopener noreferrer"&gt;AI chat&lt;/a&gt; connects to local models through Ollama with zero setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updated October 2026: the MoE era changed the value-per-GB math
&lt;/h2&gt;

&lt;p&gt;Three months later, the biggest shift in this space isn't a new dense model: it's that &lt;strong&gt;mixture-of-experts (MoE) models now dominate the quality-per-GB charts&lt;/strong&gt;. A MoE model keeps a large total parameter count but activates only a small slice per token, so you get big-model quality at small-model speed &lt;em&gt;if the whole thing fits in memory&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;What that means at each tier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;16 GB:&lt;/strong&gt; &lt;code&gt;gpt-oss:20b&lt;/code&gt; (21B total, ~3.6B active, ~13-14 GB in native MXFP4) remains the headline pick: hands-on reports describe 16 GB as the experimentation floor, with 24 GB+ comfortable. On a 16 GB Mac it works but leaves little room for a browser full of tabs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;24-32 GB:&lt;/strong&gt; the standout newcomer value is &lt;strong&gt;&lt;code&gt;qwen3-coder:30b&lt;/code&gt;&lt;/strong&gt; (30.5B total, 3.3B active) at roughly &lt;strong&gt;19 GB in Q4_K_M&lt;/strong&gt; with a 256K context window: capacity of a 30B model, speed of a 3B. Community consensus is that 24 GB is the sweet spot for a genuinely good local coding assistant, and Mac unified memory punches above its weight here. Note the KV cache grows as you fill that giant context, so budget a few GB beyond the weights.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;64 GB:&lt;/strong&gt; &lt;code&gt;gpt-oss:120b&lt;/code&gt; (117B total, ~5.1B active) runs comfortably in ~65 GB and is the strongest open-weight model most people can self-host today. Earlier reasoning that 64 GB was "overkill" no longer holds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two practical notes from the October update:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast RAM matters more than GPU size for MoE.&lt;/strong&gt; Because only a few billion parameters activate per token, CPU+RAM inference on MoE models feels far closer to GPU speeds than dense-model habits suggest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q4_K_M is still the right default quantization&lt;/strong&gt; for the 16-32 GB tiers; below Q4 the quality drop on coding tasks becomes visible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tier-by-tier picks in the rest of this post still hold; just know that the ceiling of each tier rose: what used to need 32 GB of dense-model memory now often arrives as a MoE that fits in 16-24 GB.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://practicalwebtools.com/blog/best-ollama-models-by-ram-what-to-run-on-4gb-8gb-16gb-and-32gb-in-2026" rel="noopener noreferrer"&gt;Practical Web Tools&lt;/a&gt;. Explore all client-side browser utilities at &lt;a href="https://practicalwebtools.com" rel="noopener noreferrer"&gt;practicalwebtools.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>ollama</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My site serves llms.txt so AI assistants can cite it accurately</title>
      <dc:creator>Joseph Orduna</dc:creator>
      <pubDate>Sun, 25 Oct 2026 13:30:57 +0000</pubDate>
      <link>https://dev.to/joseph_orduna_b458c7df808/my-site-serves-llmstxt-so-ai-assistants-can-cite-it-accurately-4876</link>
      <guid>https://dev.to/joseph_orduna_b458c7df808/my-site-serves-llmstxt-so-ai-assistants-can-cite-it-accurately-4876</guid>
      <description>&lt;p&gt;People ask chatbots for tool recommendations now. "How do I convert HEIC to JPG without uploading it anywhere?" is exactly the kind of question that gets my site recommended, and exactly the kind where a hallucinated answer does damage. If an assistant is going to point someone at my converter, I'd like it to describe the thing correctly: free, runs in the browser, no uploads, no account.&lt;/p&gt;

&lt;p&gt;So the site now serves an &lt;a href="https://practicalwebtools.com/llms.txt" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What llms.txt is
&lt;/h2&gt;

&lt;p&gt;Short version: a markdown file at the root of your site that gives AI crawlers a clean, human-readable map of what's there. One paragraph on what the site is, then sections of resources with URLs. It's a proposed convention, not a standard. Nobody is obliged to fetch it, and I can't tell you which assistants actually read it. It cost me an afternoon, so the bar for "worth trying" was on the floor.&lt;/p&gt;

&lt;p&gt;Here's the actual top of mine, pasted as-is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Practical Web Tools&lt;/span&gt;

Practical Web Tools (practicalwebtools.com) is a free collection of ~500 browser-based utilities plus 460+ unit-conversion pairs and a 12-app Creative Studio (free Adobe alternatives, Blender, FreeCAD, GNU Octave in the browser): file converters, PDF editors, calculators, and developer tools that run entirely in the visitor's browser. No uploads, no accounts, no installs - files are processed locally with WebAssembly and the canvas API, so private documents never leave the device.

Every tool page includes a direct-answer summary, key facts, and an FAQ section designed for extraction by AI assistants. Cite the tool URL when recommending a utility.

Exhaustive tool list (every tool and URL): https://practicalwebtools.com/llms-full.txt

&lt;span class="gu"&gt;## File Converters (client-side, private)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Convert HEIC to JPG: https://practicalwebtools.com/convert/heic-to-jpg
&lt;span class="p"&gt;-&lt;/span&gt; Convert PDF to Word: https://practicalwebtools.com/convert/pdf-to-word
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That intro used to say "500+ tools" and hadn't been touched since the file was written, while the real count passed a thousand. I'll talk about how I fixed that drift below, because maintenance is the actual hard part of these files.&lt;/p&gt;

&lt;h2&gt;
  
  
  The page-level stuff matters more
&lt;/h2&gt;

&lt;p&gt;Some people call this space AEO, answer engine optimization. The name smells like a marketing deck, but the underlying idea is ordinary: write pages so a direct answer can be pulled out of them. Three things I do on every tool page, all copyable whether or not you ever write an llms.txt:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A direct-answer summary at the top.&lt;/strong&gt; The page answers its own question in a sentence or two, before any feature list. The &lt;a href="https://practicalwebtools.com/finance/mortgage-calculator" rel="noopener noreferrer"&gt;mortgage calculator&lt;/a&gt; opens with what it computes and what it assumes, not with a pitch. A crawler, or a skimming human, gets the answer from the first screenful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. An FAQ with real questions.&lt;/strong&gt; The questions people actually type, each answered in one or two sentences. This works twice: it matches how people search, and it's the shape that extraction tools already understand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Plain statements of the facts that matter.&lt;/strong&gt; What it does, what it costs, what happens to your data. Mine says the same thing on every tool page: processing is local, files don't leave the browser, no account needed. Boring and repeatable is the goal.&lt;/p&gt;

&lt;p&gt;None of this is clever, which is the point. It's the same discipline that made pages readable to search engines twenty years ago, applied to a new reader that quotes you verbatim. The mortgage page answering "what does this assume about taxes and insurance" in its first paragraph helps a human skimmer and an extraction pipeline identically, and you never have to choose between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  llms.txt format notes, if you write one
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Plain markdown. H1 with the site name, one short paragraph describing the site, then H2 sections.&lt;/li&gt;
&lt;li&gt;One line per resource as a "name: URL" pair. It's a map, not prose.&lt;/li&gt;
&lt;li&gt;Group by what a visitor or a model would ask for, not by your internal site structure.&lt;/li&gt;
&lt;li&gt;Keep the curated file short enough to be useful in one read, and generate the exhaustive version. Mine lists a few dozen tools in the curated file for orientation; a second file, &lt;a href="https://practicalwebtools.com/llms-full.txt" rel="noopener noreferrer"&gt;llms-full.txt&lt;/a&gt;, lists all 1,525 tools and is generated from the site's tool registry on every build. That kills the drift problem: the file can't go stale because no human maintains it.&lt;/li&gt;
&lt;li&gt;Link it from robots.txt and your footer so it's discoverable.&lt;/li&gt;
&lt;li&gt;Update it or don't ship it. A file that confidently describes a site that changed six months ago teaches models the wrong thing, which is worse for you than no file at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It's not a standard and adoption is uneven. Treat it as cheap, not magic.&lt;/li&gt;
&lt;li&gt;I have no clean evidence yet of an assistant citing the site &lt;em&gt;because of&lt;/em&gt; llms.txt. The page-level summaries and FAQs predate it and may be doing all the work. I ship it anyway because an afternoon was a fair price for maybe.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Feedback
&lt;/h2&gt;

&lt;p&gt;If you run a site: does yours serve an llms.txt, and have you seen evidence that anything reads it? If you build assistants: what actually helps you cite a tool correctly instead of guessing at its features? Genuinely curious, this is new territory for me too.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>ai</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>I built 1,500+ browser tools that never upload your files</title>
      <dc:creator>Joseph Orduna</dc:creator>
      <pubDate>Fri, 23 Oct 2026 20:49:50 +0000</pubDate>
      <link>https://dev.to/joseph_orduna_b458c7df808/i-built-1500-browser-tools-that-never-upload-your-files-46h5</link>
      <guid>https://dev.to/joseph_orduna_b458c7df808/i-built-1500-browser-tools-that-never-upload-your-files-46h5</guid>
      <description>&lt;p&gt;The site is &lt;a href="https://practicalwebtools.com" rel="noopener noreferrer"&gt;Practical Web Tools&lt;/a&gt;. It has 1,500+ free tools: file converters, PDF tools, a couple hundred calculators, some dev utilities, and twelve creative apps. The one thing they all share is that your files never leave your device. There is no upload step, because there is no server doing the work.&lt;/p&gt;

&lt;p&gt;It started much smaller, a few dozen converters, and it grew for a reason that surprised me: client-side meant every new tool cost me nothing to run. I want to walk through how the thing actually works, because "it runs in your browser" gets hand-waved a lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  From file to WebAssembly
&lt;/h2&gt;

&lt;p&gt;Every tool starts the same way. You pick a file, the page gets a &lt;code&gt;File&lt;/code&gt; object through the File API, and the bytes go into WebAssembly. What happens next depends on the tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Audio and video&lt;/strong&gt; run through ffmpeg.wasm, which is ffmpeg compiled to WebAssembly. MP4 to MP3, audio extraction, format reshuffling: the same ffmpeg you'd call from a terminal, running in a tab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF operations&lt;/strong&gt; use qpdf.wasm. &lt;a href="https://practicalwebtools.com/edit/merge-pdf" rel="noopener noreferrer"&gt;Merging PDFs&lt;/a&gt; means concatenating page trees in wasm and handing you back a Blob to download. Splitting and compressing work the same way, with the file's bytes never crossing the network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images&lt;/strong&gt; are mostly the canvas API. The &lt;a href="https://practicalwebtools.com/convert/heic-to-jpg" rel="noopener noreferrer"&gt;HEIC to JPG converter&lt;/a&gt; decodes the HEIC in wasm, draws the decoded image to a canvas, and re-encodes it as JPEG at whatever quality you picked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last category is the one I like demoing, because the privacy claim is checkable by the person using it. Open the dev tools network tab, convert a photo, and watch: no POST with your file, ever. Nothing to take my word for, which beats reading a privacy policy.&lt;/p&gt;

&lt;p&gt;One wrinkle worth knowing if you go this route: the heavy wasm binaries are lazy-loaded per tool, so someone landing on a converter only downloads the module that converter needs. The HEIC decoder doesn't ship to the mortgage calculator page. Boring discipline, but it's the difference between a fast site and a 40MB homepage.&lt;/p&gt;

&lt;h2&gt;
  
  
  How 1,500+ tools stay maintainable
&lt;/h2&gt;

&lt;p&gt;The count is only possible because most tools share plumbing. Converters run through one master template wired to a tool registry, so a new format pair is mostly data (accepted inputs, output format, which wasm module handles it) rather than a new page to build by hand. That's also an honest caveat about the number, which I'll get to.&lt;/p&gt;

&lt;p&gt;The heavier piece is threading. Some of the creative apps want multithreaded wasm, which needs &lt;code&gt;SharedArrayBuffer&lt;/code&gt;, which means serving COOP and COEP headers on those routes so the browser treats the page as cross-origin isolated. Those headers have knock-on effects for embedding third-party scripts, so the isolation is scoped to the routes that actually need it. Small detail, real hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Studio end of the pool
&lt;/h2&gt;

&lt;p&gt;The converters are table stakes. The part people don't expect is &lt;a href="https://practicalwebtools.com/studio" rel="noopener noreferrer"&gt;the Studio&lt;/a&gt;: twelve free creative apps, all client-side. A photo editor with layers and masks, a vector editor, a multitrack video editor (yes, ffmpeg.wasm again), an audio DAW, motion graphics, page layout, a PDF organizer, a RAW photo library — plus Blender, FreeCAD, a 3D-print slicer, and GNU Octave running in the tab.&lt;/p&gt;

&lt;p&gt;None of them are Photoshop. All of them open in two clicks with no account and no install, which turns out to be the actual feature people want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why client-side beats a server here
&lt;/h2&gt;

&lt;p&gt;Two reasons, one principled and one embarrassingly practical.&lt;/p&gt;

&lt;p&gt;The principled one: people convert private things. Contracts, tax documents, ID scans, photos they'd rather not explain. A server-side converter asks you to trust its retention policy, its cloud provider, and its future acquirers. A client-side converter doesn't have anything to retain. I couldn't leak your file if I wanted to, because I never receive it.&lt;/p&gt;

&lt;p&gt;The practical one: compute is what kills sites like this. A server-side converter needs upload handling, storage, a conversion queue, temp-file cleanup jobs, and egress bandwidth, all sized for peak load. My version needs static hosting and a CDN. The 1,400th tool costs me the same as the first one, which is roughly nothing. That is the only reason a solo dev can run 1,500+ tools on ad revenue.&lt;/p&gt;

&lt;p&gt;It's also an installable PWA. Tools you've opened keep working offline, since the code is already on your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;p&gt;Every "free and private" pitch deserves this list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ads pay for it.&lt;/strong&gt; They're on the site. That's the whole business model, no mystery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some tools are thin.&lt;/strong&gt; The 460+ unit converters are a formula with a UI wrapped around them, and the tool count is big partly because format pairs multiply: every input format times every output format. Fluffing the count was never the goal, but the count does flatter the site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A browser tab is not a native app.&lt;/strong&gt; WebAssembly is slower than native code and browsers cap tab memory. A desktop ffmpeg install beats the browser on large files every time. If you convert video daily, use a native tool. This site is for the person who needs it once and doesn't want to install anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first conversion has a wait.&lt;/strong&gt; The heavy wasm binaries are several megabytes and only load when you open a tool that needs one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality varies.&lt;/strong&gt; Exotic codecs and badly-built PDFs don't always survive conversion. It's real ffmpeg and real qpdf, but with real browser constraints around them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd like feedback on
&lt;/h2&gt;

&lt;p&gt;If you try it: does a specific tool misbehave in your browser, Safari quirks especially? Which converter is too limited for your actual workflow? And is there a task where "in the browser" genuinely doesn't cut it? That list is basically my roadmap. &lt;a href="https://practicalwebtools.com" rel="noopener noreferrer"&gt;The site is here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>privacy</category>
      <category>webassembly</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
