<?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: M Saqlain Aslam</title>
    <description>The latest articles on DEV Community by M Saqlain Aslam (@m_saqlainaslam_7eb454063).</description>
    <link>https://dev.to/m_saqlainaslam_7eb454063</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%2F1518659%2F97fdf2b9-f4d3-4198-9bfb-f6d8312bc848.png</url>
      <title>DEV Community: M Saqlain Aslam</title>
      <link>https://dev.to/m_saqlainaslam_7eb454063</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/m_saqlainaslam_7eb454063"/>
    <language>en</language>
    <item>
      <title>Building Single-File Calculators and Comparison Tools for WordPress (No Plugin, No Framework)</title>
      <dc:creator>M Saqlain Aslam</dc:creator>
      <pubDate>Tue, 06 Oct 2026 05:55:31 +0000</pubDate>
      <link>https://dev.to/m_saqlainaslam_7eb454063/building-single-file-calculators-and-comparison-tools-for-wordpress-no-plugin-no-framework-22mk</link>
      <guid>https://dev.to/m_saqlainaslam_7eb454063/building-single-file-calculators-and-comparison-tools-for-wordpress-no-plugin-no-framework-22mk</guid>
      <description>&lt;p&gt;I run &lt;a href="https://universalnest.com" rel="noopener noreferrer"&gt;UniversalNest&lt;/a&gt;, a WordPress publication. Most of what we publish is articles, but some topics are better served by a small interactive tool: a &lt;a href="https://universalnest.com/ai-model-compare/" rel="noopener noreferrer"&gt;calculator&lt;/a&gt; or a side-by-side comparison. I've built a few, and I settled on one pattern that I now reuse: &lt;strong&gt;one self-contained HTML file, no framework, no build step, no plugin.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is why I chose it and where it breaks down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not a WordPress plugin?
&lt;/h2&gt;

&lt;p&gt;A plugin is the "correct" answer, but for a single tool it carries a lot of overhead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Plugin updates and compatibility checks for something that renders one widget&lt;/li&gt;
&lt;li&gt;A PHP layer for logic that is really just client-side math&lt;/li&gt;
&lt;li&gt;Extra scripts and styles loaded on every page, not just the one that needs them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single HTML file has none of that. It can be dropped into a page through a custom HTML block or an iframe, or hosted on its own path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern: data as a plain array, render as a pure function
&lt;/h2&gt;

&lt;p&gt;Every tool follows the same shape. The data lives in one array at the top of the script, and one &lt;code&gt;render()&lt;/code&gt; function rebuilds the UI from it. This is the real script from my AI model comparison tool:&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;MODELS&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;maker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OpenAI&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GPT-5.6 Sol&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;closed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;coding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;91.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;codingLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;91.9% Terminal-Bench&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;priceLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$30 / M tokens&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;contextLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;400K tokens&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...more models, same shape&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;maxCoding&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;max&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;MODELS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coding&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;maxContext&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;max&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;MODELS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sortKey&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;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;MODELS&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;sortKey&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;coding&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coding&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coding&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;sortKey&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cheap&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&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;sortKey&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;context&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;codingPct&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;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coding&lt;/span&gt;  &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;maxCoding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&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;contextPct&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;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;maxContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// card markup uses codingPct / contextPct as the bar widths&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`...`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="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="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.priority-btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;btn&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;btn&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;btn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;...&lt;/code&gt; in the card template is just markup. The complete file is at the bottom of this post.&lt;/p&gt;

&lt;p&gt;Three things make this worth doing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Updating the tool means editing one array.&lt;/strong&gt; There is no database or CMS field to change, and nothing to redeploy except the file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sorting and re-ranking are free.&lt;/strong&gt; &lt;code&gt;render()&lt;/code&gt; rebuilds from the data every time, so a "priority" button is just a different sort key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Progress bars are derived values, not stored ones.&lt;/strong&gt; Each bar is &lt;code&gt;value / max * 100&lt;/code&gt;, computed at render time, so adding a new entry never breaks the scale.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Gotchas I ran into
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Data goes stale, and nothing tells the reader.&lt;/strong&gt; A comparison tool is only as good as its last update. I now put a visible "data snapshot" date at the top of the page. If the numbers are from August, the page says August.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't mix incomparable metrics in one bar.&lt;/strong&gt; Two models can report coding scores from different benchmarks. The tool should label the benchmark next to each number and never imply a like-for-like comparison where there isn't one. That's why each entry carries its own &lt;code&gt;codingLabel&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inline styles in a &lt;code&gt;.map()&lt;/code&gt; template get messy fast.&lt;/strong&gt; Keep the visual rules in CSS classes and pass only the truly dynamic values (like a width percentage) inline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Iframes and SEO.&lt;/strong&gt; If the tool is embedded in an iframe, the content inside isn't treated as part of the host page's text. I put the explanation, methodology, and caveats in the surrounding article, and let the tool handle only the interactive part.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this pattern is the wrong choice
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You need user accounts, saved state across devices, or server-side logic&lt;/li&gt;
&lt;li&gt;The data changes often enough that hand-editing an array becomes a chore (at that point, fetch from a JSON endpoint)&lt;/li&gt;
&lt;li&gt;You need the tool to be crawlable as primary content&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a calculator or a comparison table that changes a few times a year, though, one file is hard to beat.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently next time
&lt;/h2&gt;

&lt;p&gt;I'd move the data array into a separate &lt;code&gt;data.json&lt;/code&gt; from the start. Keeping data and rendering in one file is convenient at first, but it makes updates feel like code changes when they are really content changes.&lt;/p&gt;

&lt;p&gt;If you've embedded small tools in WordPress, I'd like to know how you handle keeping the data fresh. A manual update works for me now, but I doubt it scales.&lt;/p&gt;

&lt;p&gt;Full source: the complete single file&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;title&amp;gt;&lt;/span&gt;AI Model Compare — UniversalNest&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;



  :root{
    --bg: #F7F8FA;
    --surface: #FFFFFF;
    --line: #E2E5EA;
    --ink: #12151C;
    --muted: #6B7280;
    --signal: #FF4B3E;
    --signal-soft: #FFEDEB;
    --blue: #0047FF;
    --blue-soft: #EAF0FF;
    --focus: #0047FF;
  }
  *{box-sizing:border-box;}
  html,body{margin:0;padding:0;}
  body{
    background:var(--bg);
    color:var(--ink);
    font-family:'Inter',system-ui,sans-serif;
    -webkit-font-smoothing:antialiased;
    line-height:1.5;
  }
  .wrap{max-width:980px;margin:0 auto;padding:40px 20px 80px;}

  /* header */
  .eyebrow{
    font-family:'JetBrains Mono',monospace;
    font-size:12px;
    letter-spacing:0.08em;
    text-transform:uppercase;
    color:var(--muted);
    display:flex;
    align-items:center;
    gap:8px;
    margin-bottom:14px;
  }
  .eyebrow::before{
    content:"";
    width:7px;height:7px;border-radius:50%;
    background:var(--signal);
    display:inline-block;
  }
  h1{
    font-family:'Space Grotesk',sans-serif;
    font-weight:700;
    font-size:clamp(28px,4.2vw,42px);
    line-height:1.08;
    margin:0 0 12px;
    letter-spacing:-0.01em;
  }
  .sub{
    color:var(--muted);
    font-size:16px;
    max-width:56ch;
    margin:0 0 28px;
  }
  .updated{
    font-family:'JetBrains Mono',monospace;
    font-size:12px;
    color:var(--muted);
    border-top:1px solid var(--line);
    padding-top:14px;
    margin-bottom:32px;
  }

  /* priority selector */
  .priority-row{
    display:flex;
    flex-wrap:wrap;
    gap:8px;
    margin-bottom:36px;
  }
  .priority-btn{
    font-family:'Inter',sans-serif;
    font-size:13.5px;
    font-weight:500;
    padding:9px 16px;
    border-radius:999px;
    border:1px solid var(--line);
    background:var(--surface);
    color:var(--ink);
    cursor:pointer;
    transition:border-color .15s ease, background .15s ease;
  }
  .priority-btn:hover{border-color:var(--blue);}
  .priority-btn:focus-visible{outline:2px solid var(--focus);outline-offset:2px;}
  .priority-btn.active{
    background:var(--ink);
    border-color:var(--ink);
    color:#fff;
  }

  /* comparison grid */
  .grid{
    display:grid;
    grid-template-columns:repeat(auto-fit,minmax(210px,1fr));
    gap:14px;
  }
  .card{
    background:var(--surface);
    border:1px solid var(--line);
    border-radius:10px;
    padding:20px 18px 18px;
    position:relative;
    transition:transform .15s ease, box-shadow .15s ease, opacity .2s ease;
  }
  .card.rank-1{border-color:var(--signal);}
  .badge{
    position:absolute;
    top:-11px;
    left:16px;
    background:var(--signal);
    color:#fff;
    font-family:'JetBrains Mono',monospace;
    font-size:10.5px;
    letter-spacing:0.04em;
    text-transform:uppercase;
    padding:3px 8px;
    border-radius:5px;
  }
  .maker{
    font-family:'JetBrains Mono',monospace;
    font-size:11px;
    color:var(--muted);
    text-transform:uppercase;
    letter-spacing:0.04em;
  }
  .model-name{
    font-family:'Space Grotesk',sans-serif;
    font-weight:600;
    font-size:19px;
    margin:2px 0 16px;
    line-height:1.2;
  }
  .type-tag{
    display:inline-block;
    font-family:'JetBrains Mono',monospace;
    font-size:10.5px;
    padding:2px 7px;
    border-radius:4px;
    margin-bottom:14px;
  }
  .type-tag.open{background:var(--blue-soft);color:var(--blue);}
  .type-tag.closed{background:#F1F2F4;color:var(--muted);}

  .metric{margin-bottom:12px;}
  .metric-label{
    display:flex;
    justify-content:space-between;
    font-size:11.5px;
    color:var(--muted);
    margin-bottom:5px;
    font-family:'JetBrains Mono',monospace;
  }
  .metric-value{color:var(--ink);font-weight:600;}
  .gauge-track{
    height:5px;
    background:#EEF0F3;
    border-radius:3px;
    overflow:hidden;
  }
  .gauge-fill{
    height:100%;
    background:var(--ink);
    border-radius:3px;
  }
  .card.rank-1 .gauge-fill{background:var(--signal);}

  .price-row{
    display:flex;
    justify-content:space-between;
    align-items:baseline;
    font-family:'JetBrains Mono',monospace;
    font-size:13px;
    margin:16px 0 4px;
    padding-top:12px;
    border-top:1px solid var(--line);
  }
  .price-value{font-weight:600;font-size:15px;}

  .readmore{
    display:inline-block;
    margin-top:14px;
    font-size:13px;
    font-weight:500;
    color:var(--blue);
    text-decoration:none;
  }
  .readmore:hover{text-decoration:underline;}
  .readmore:focus-visible{outline:2px solid var(--focus);outline-offset:2px;}

  footer{
    margin-top:48px;
    padding-top:20px;
    border-top:1px solid var(--line);
    font-size:13px;
    color:var(--muted);
  }
  footer a{color:var(--blue);text-decoration:none;}
  footer a:hover{text-decoration:underline;}

  @media (prefers-reduced-motion: reduce){
    .card{transition:none;}
  }





  Live spec sheet
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Which AI model actually fits your job?&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Four current frontier and open-weight models, lined up on the numbers that matter: coding benchmarks, price per million tokens, and context window. Pick a priority to re-rank.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  DATA SNAPSHOT — AUG 2026 · SOURCED FROM UNIVERSALNEST MODEL COVERAGE


    Overview
    Best for coding
    Cheapest
    Largest context





    Full write-ups on each model, methodology, and benchmark caveats live on
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://universalnest.com/category/ai-news-tech/ai-tools-productivity/"&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"noopener"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;UniversalNest — AI Tools &lt;span class="ni"&gt;&amp;amp;amp;&lt;/span&gt; Productivity&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;.
    Built by &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://universalnest.com"&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"noopener"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;UniversalNest&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;.




const MODELS = [
  {
    maker: "OpenAI",
    name: "GPT-5.6 Sol",
    type: "closed",
    coding: 91.9,
    codingLabel: "91.9% Terminal-Bench",
    price: 30,
    priceLabel: "$30 / M tokens",
    context: 400,
    contextLabel: "400K tokens",
    url: "https://universalnest.com/gpt-claude-gemini-ai-model-comparison/"
  },
  {
    maker: "Anthropic",
    name: "Claude Opus 4.8",
    type: "closed",
    coding: 69.2,
    codingLabel: "69.2% SWE-bench Pro",
    price: 22,
    priceLabel: "$22 / M tokens",
    context: 500,
    contextLabel: "500K tokens",
    url: "https://universalnest.com/best-llms-for-coding/"
  },
  {
    maker: "Google",
    name: "Gemini 3.1 Pro",
    type: "closed",
    coding: 76.4,
    codingLabel: "76.4% SWE-bench Pro",
    price: 12,
    priceLabel: "$12 / M tokens",
    context: 1000,
    contextLabel: "1M tokens",
    url: "https://universalnest.com/gpt-claude-gemini-ai-model-comparison/"
  },
  {
    maker: "DeepSeek",
    name: "DeepSeek V4 Pro",
    type: "open",
    coding: 80.6,
    codingLabel: "80.6% SWE-bench",
    price: 0.87,
    priceLabel: "$0.87 / M tokens",
    context: 256,
    contextLabel: "256K tokens",
    url: "https://universalnest.com/deepseek-vs-gpt-coding-price-comparison/"
  }
];

const grid = document.getElementById('grid');
const maxCoding = Math.max(...MODELS.map(m =&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt; m.coding));
const maxContext = Math.max(...MODELS.map(m =&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt; m.context));
const minPrice = Math.min(...MODELS.map(m =&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt; m.price));

function render(sortKey){
  let list = [...MODELS];
  let rankField = null;
  let badgeText = "";

  if(sortKey === "coding"){
    list.sort((a,b)=&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;b.coding-a.coding);
    rankField = "coding"; badgeText = "Top coding score";
  } else if(sortKey === "cheap"){
    list.sort((a,b)=&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;a.price-b.price);
    rankField = "price-asc"; badgeText = "Lowest price";
  } else if(sortKey === "context"){
    list.sort((a,b)=&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;b.context-a.context);
    rankField = "context"; badgeText = "Largest context";
  }

  grid.innerHTML = list.map((m, i) =&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt; {
    const isTop = rankField &lt;span class="ni"&gt;&amp;amp;amp;&amp;amp;amp;&lt;/span&gt; i === 0;
    const codingPct = Math.round((m.coding / maxCoding) * 100);
    const contextPct = Math.round((m.context / maxContext) * 100);
    return `
    &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;div class="card ${isTop ? 'rank-1' : ''}"&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
      ${isTop ? `&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;div class="badge"&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;${badgeText}&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/div&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;` : ``}
      &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;div class="maker"&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;${m.maker}&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/div&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
      &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;div class="model-name"&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;${m.name}&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/div&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
      &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;span class="type-tag ${m.type}"&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;${m.type === 'open' ? 'Open-weight' : 'Closed'}&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/span&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;

      &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;div class="metric"&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
        &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;div class="metric-label"&lt;span class="ni"&gt;&amp;amp;gt;&amp;amp;lt;&lt;/span&gt;span&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;Coding benchmark&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/span&lt;span class="ni"&gt;&amp;amp;gt;&amp;amp;lt;&lt;/span&gt;span class="metric-value"&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;${m.codingLabel}&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/span&lt;span class="ni"&gt;&amp;amp;gt;&amp;amp;lt;&lt;/span&gt;/div&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
        &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;div class="gauge-track"&lt;span class="ni"&gt;&amp;amp;gt;&amp;amp;lt;&lt;/span&gt;div class="gauge-fill" style="width:${codingPct}%"&lt;span class="ni"&gt;&amp;amp;gt;&amp;amp;lt;&lt;/span&gt;/div&lt;span class="ni"&gt;&amp;amp;gt;&amp;amp;lt;&lt;/span&gt;/div&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
      &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/div&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;

      &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;div class="metric"&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
        &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;div class="metric-label"&lt;span class="ni"&gt;&amp;amp;gt;&amp;amp;lt;&lt;/span&gt;span&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;Context window&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/span&lt;span class="ni"&gt;&amp;amp;gt;&amp;amp;lt;&lt;/span&gt;span class="metric-value"&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;${m.contextLabel}&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/span&lt;span class="ni"&gt;&amp;amp;gt;&amp;amp;lt;&lt;/span&gt;/div&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
        &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;div class="gauge-track"&lt;span class="ni"&gt;&amp;amp;gt;&amp;amp;lt;&lt;/span&gt;div class="gauge-fill" style="width:${contextPct}%"&lt;span class="ni"&gt;&amp;amp;gt;&amp;amp;lt;&lt;/span&gt;/div&lt;span class="ni"&gt;&amp;amp;gt;&amp;amp;lt;&lt;/span&gt;/div&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
      &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/div&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;

      &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;div class="price-row"&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
        &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;span&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;PRICE / M TOKENS&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/span&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
        &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;span class="price-value"&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;${m.priceLabel}&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/span&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
      &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/div&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;

      &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;a class="readmore" href="${m.url}" target="_blank" rel="noopener"&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;Full breakdown on UniversalNest →&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/a&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
    &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/div&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;`;
  }).join('');
}

document.querySelectorAll('.priority-btn').forEach(btn=&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;{
  btn.addEventListener('click', ()=&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;{
    document.querySelectorAll('.priority-btn').forEach(b=&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;b.classList.remove('active'));
    btn.classList.add('active');
    render(btn.dataset.sort);
  });
});

render('default');



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

&lt;/div&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>wordpress</category>
      <category>html</category>
    </item>
    <item>
      <title>Building a Social Media Downloader: Handling Rate Limits, Format Detection, and Platform API Changes</title>
      <dc:creator>M Saqlain Aslam</dc:creator>
      <pubDate>Sun, 04 Oct 2026 10:40:49 +0000</pubDate>
      <link>https://dev.to/m_saqlainaslam_7eb454063/building-a-social-media-downloader-handling-rate-limits-format-detection-and-platform-api-changes-50a0</link>
      <guid>https://dev.to/m_saqlainaslam_7eb454063/building-a-social-media-downloader-handling-rate-limits-format-detection-and-platform-api-changes-50a0</guid>
      <description>&lt;p&gt;I run a small tool called &lt;a href="https://socialsaver.net/" rel="noopener noreferrer"&gt;SocialSaver&lt;/a&gt; - paste a link from Instagram, TikTok, Facebook, or X, and get the video file back. Simple on the surface. Underneath, it's one of the more annoying things I've built, because none of the hard parts are the parts you'd expect.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fda2t4z1pwbmwx9p39znq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fda2t4z1pwbmwx9p39znq.webp" alt=" " width="800" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's what actually eats the engineering time.&lt;/p&gt;

&lt;p&gt;The stack, briefly&lt;/p&gt;

&lt;p&gt;Backend is a NestJS API inside an Nx monorepo; frontend is Angular. Nothing exotic - the complexity isn't in the framework choices, it's in dealing with platforms that actively don't want this to work.&lt;/p&gt;

&lt;p&gt;Problem 1: there's no stable API to call&lt;/p&gt;

&lt;p&gt;None of these platforms offer a public "give me this video's direct file URL" endpoint for obvious reasons. So extraction means parsing whatever the platform's web client actually loads - embedded JSON blobs in page source, internal GraphQL calls, or CDN URLs buried in a response that's 90% unrelated tracking data.&lt;/p&gt;

&lt;p&gt;The catch: that internal structure changes without notice. A parser that worked last week breaks silently this week, and you don't find out until download requests start failing. There's no changelog to subscribe to - the only signal is your own error rate.&lt;/p&gt;

&lt;p&gt;Problem 2: rate limiting is adversarial, not polite&lt;/p&gt;

&lt;p&gt;This isn't like calling a documented REST API where you get a 429 and a Retry-After header. Platforms vary wildly:&lt;/p&gt;

&lt;p&gt;Some return a normal-looking 200 with an empty or garbage payload — so you have to validate response shape, not just status code, to detect throttling&lt;br&gt;
Some rotate which endpoint gets throttled first, so retry logic that assumes "it's always this one path" eventually breaks&lt;br&gt;
IP-based limits mean request volume from a single server can get flagged even with well-behaved traffic&lt;/p&gt;

&lt;p&gt;Practical fix that's worked reasonably well: queue + backoff with jitter, and treat "suspiciously empty response" as a rate-limit signal even when the HTTP status says success.&lt;/p&gt;

&lt;p&gt;Problem 3: format detection is a moving target&lt;/p&gt;

&lt;p&gt;"Download the video" sounds like one job. In practice it's: detect platform → detect content type (single video, carousel, story, reel) → detect available quality/format variants → pick the best one that's actually reachable (some URLs in the response are decoys or region-locked). Each platform encodes this differently, and multi-image carousel posts in particular are where most downloader tools silently fail.&lt;/p&gt;

&lt;p&gt;Problem 4: this breaks in production, not in dev&lt;/p&gt;

&lt;p&gt;Because none of this is documented, you can't really write forward-looking tests for "what happens when Instagram changes their embedded JSON structure." You find out from real user reports. The practical answer has been: logging that captures the raw unexpected response shape (not just "failed"), so when something breaks, there's enough signal to patch it within hours instead of guessing blind.&lt;/p&gt;

&lt;p&gt;Where it's at now&lt;/p&gt;

&lt;p&gt;SocialSaver handles &lt;a href="https://socialsaver.net/instagram-downloader" rel="noopener noreferrer"&gt;Instagram&lt;/a&gt;, &lt;a href="https://socialsaver.net/tiktok-downloader" rel="noopener noreferrer"&gt;TikTok&lt;/a&gt;, &lt;a href="https://socialsaver.net/facebook-downloader" rel="noopener noreferrer"&gt;Facebook&lt;/a&gt;, and &lt;a href="https://socialsaver.net/twitter-downloader" rel="noopener noreferrer"&gt;X&lt;/a&gt; today - no login, no account required; paste a link and get a file back. The architecture above is basically why a tool that sounds like a weekend project ends up needing ongoing maintenance instead of a one-time build.&lt;/p&gt;

&lt;p&gt;Curious if anyone else working on similar scraping/extraction-adjacent tools has found a cleaner way to detect silent format changes than "wait for logs to tell you." Would genuinely like to hear it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>node</category>
      <category>api</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
