<?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: Juwel Hossen</title>
    <description>The latest articles on DEV Community by Juwel Hossen (@juwel_hossen_6b25cf5d59e1).</description>
    <link>https://dev.to/juwel_hossen_6b25cf5d59e1</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%2F4246387%2F225fcc9e-4d8a-4e21-9c7a-39a1d54358ca.png</url>
      <title>DEV Community: Juwel Hossen</title>
      <link>https://dev.to/juwel_hossen_6b25cf5d59e1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/juwel_hossen_6b25cf5d59e1"/>
    <language>en</language>
    <item>
      <title>How I built 2,300 static pages of calculators in 8 languages without a framework</title>
      <dc:creator>Juwel Hossen</dc:creator>
      <pubDate>Sun, 15 Nov 2026 18:53:06 +0000</pubDate>
      <link>https://dev.to/juwel_hossen_6b25cf5d59e1/how-i-built-2300-static-pages-of-calculators-in-8-languages-without-a-framework-3d84</link>
      <guid>https://dev.to/juwel_hossen_6b25cf5d59e1/how-i-built-2300-static-pages-of-calculators-in-8-languages-without-a-framework-3d84</guid>
      <description>&lt;p&gt;I work in operations at a factory in Bangladesh. In my spare time I built &lt;a href="https://www.lifeeasytools.com" rel="noopener noreferrer"&gt;LifeEasyTools&lt;/a&gt;, a site with 515 free calculators and everyday tools in 8 languages. It has no framework and no database for the tools: one Node.js script turns plain JavaScript objects into 2,359 static pages.&lt;/p&gt;

&lt;p&gt;Here is how it works, what went wrong, and what I would do again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why static pages
&lt;/h2&gt;

&lt;p&gt;Most of my users are on low-cost Android phones and slow mobile data. A calculator should show up instantly and never wait for a server to do maths that the phone can do itself.&lt;/p&gt;

&lt;p&gt;So every page is plain HTML. Calculations run in the browser, and the PDF and image tools process files locally, so nothing is uploaded. A typical tool page is about 50 KB of HTML (13 KB gzipped), plus one shared stylesheet and one shared script.&lt;/p&gt;

&lt;h2&gt;
  
  
  One file per tool
&lt;/h2&gt;

&lt;p&gt;Each tool is a JavaScript object:&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;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'concrete-calculator'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'everyday'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Concrete Calculator: Volume, Bags, Cement, Sand &amp;amp; Stone'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sb"&gt;`${seg('units', 'Units', [['m', 'Metric (m)'], ['ft', 'Feet']], 'm')}
         ${row(num('L', 'Length', 5), num('W', 'Width', 4), num('T', 'Thickness', 0.125))}`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;js&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sb"&gt;`DN.init(function (v) {
         var vol = v.L * v.W * v.T;
         return DN.result({ answer: { label: 'Concrete needed', value: DN.fmt(vol, 3) + ' m³' } });
       });`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sb"&gt;`&amp;lt;h2&amp;gt;How to calculate concrete volume&amp;lt;/h2&amp;gt; ...`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;faq&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'How many bags of cement are in 1 cubic metre?'&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;seg&lt;/code&gt;, &lt;code&gt;num&lt;/code&gt; and &lt;code&gt;row&lt;/code&gt; are tiny helpers that return form HTML. &lt;code&gt;DN&lt;/code&gt; is a small shared runtime (about 25 KB gzipped) that reads the form, formats numbers for the visitor's locale, and draws the result card, tables and charts.&lt;/p&gt;

&lt;p&gt;The build script loops over every tool and writes the page with its title, description, breadcrumbs, JSON-LD (&lt;code&gt;WebApplication&lt;/code&gt;, &lt;code&gt;FAQPage&lt;/code&gt;, &lt;code&gt;BreadcrumbList&lt;/code&gt;), hreflang links and a sitemap entry. A full build of all 2,359 pages takes about 35 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Translations without a framework
&lt;/h2&gt;

&lt;p&gt;Each language has a translation file per tool: title, description, intro, content and a &lt;code&gt;ui&lt;/code&gt; map of the English strings used in the form and the script.&lt;/p&gt;

&lt;p&gt;The build translates the form HTML text nodes and every single-quoted string in the tool's script through that map. When a sentence is built from pieces (&lt;code&gt;'in ' + days + ' days'&lt;/code&gt;), a small &lt;code&gt;jsFix&lt;/code&gt; list replaces that expression with a properly ordered one, because word order differs between languages.&lt;/p&gt;

&lt;p&gt;The catch: it is easy to leave English behind. So a test runs every translated tool in headless Chromium, clicks Calculate, and lists any English phrase left in the visible result. That test found things I would never have spotted by hand, such as holiday names in English on Bangla pages, or a chart legend that still said "Part / Rest".&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the maths
&lt;/h2&gt;

&lt;p&gt;For a calculator site, a wrong answer is the worst bug. I keep a "golden" test file: 36 real cases, each with a reference value computed independently from the published formula, not copied from the tool's code.&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="n"&gt;pmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;P&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;P&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;r&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;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&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;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;CASES&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="s1"&gt;'emi-calculator'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;9.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'y'&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;pmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.095&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'EMI 5 lakh 9.5% 5y'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'bmi-calculator'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'metric'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;175&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="mf"&gt;22.9&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'BMI'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ... net salary in France, Spain, Italy, Netherlands and Poland, due dates, unit converters&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;Playwright fills each form, runs it, and checks the result contains the expected number. When a test failed the first time, it was usually my test input that was wrong (a mortgage down payment is a percentage, not an amount), which is a good reason to write the reference values independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layout shift on file tools
&lt;/h2&gt;

&lt;p&gt;Chrome's layout-shift measurement showed 0.077 on the PDF tools on phones. The file drop zone gets extra buttons (link, camera, paste, Drive, Dropbox) from JavaScript after the first paint, and on narrow screens they wrapped onto two lines and pushed everything down.&lt;/p&gt;

&lt;p&gt;The fix was two parts: show the extra sources as one row of fixed-height tiles on phones, and reserve their height in CSS until the script marks the zone as ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.fstart&lt;/span&gt; &lt;span class="nc"&gt;.drop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nf"&gt;not&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;.dready&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;.dbtn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;87px&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;Layout shift went to 0 on every page I measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  SEO lessons from a site with 2,000+ pages
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Link siblings around each page, not the first few.&lt;/strong&gt; Pages in long families (land units, US state paycheck calculators, currency pairs) linked to the first ten items of the list, so items later in the list got almost no internal links. Linking each page to its neighbours on both sides fixed 161 weakly linked pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep titles under about 65 characters.&lt;/strong&gt; Dropping the brand suffix on long titles let Google show the words that matter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One canonical per page and full hreflang sets&lt;/strong&gt; for 8 languages, generated from the same data so they never drift apart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer first.&lt;/strong&gt; Converter pages now start with the factor ("1 foot = 0.3048 metres") before anything else.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;515 tools, 2,359 pages, 8 languages (English, Bangla, Hindi, Spanish, Arabic, Portuguese, Indonesian, French)&lt;/li&gt;
&lt;li&gt;Build: about 35 seconds for all pages&lt;/li&gt;
&lt;li&gt;Hosting: one small VPS behind Cloudflare; deploys upload only changed files and notify IndexNow&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would do again
&lt;/h2&gt;

&lt;p&gt;Plain objects plus a build script were enough for 500 tools. The two things that paid off most were the golden tests and the "find leftover English" test. Both caught real problems before users did.&lt;/p&gt;

&lt;p&gt;If you want to try a tool or tell me what is missing, the site is &lt;a href="https://www.lifeeasytools.com" rel="noopener noreferrer"&gt;lifeeasytools.com&lt;/a&gt;. I am happy to answer questions about the build in the comments.&lt;/p&gt;

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