<?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: Shahzeb Khan</title>
    <description>The latest articles on DEV Community by Shahzeb Khan (@shahzeb3939).</description>
    <link>https://dev.to/shahzeb3939</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3900025%2Fa08d4833-465f-41b5-bc39-fcac20a119f6.png</url>
      <title>DEV Community: Shahzeb Khan</title>
      <link>https://dev.to/shahzeb3939</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shahzeb3939"/>
    <language>en</language>
    <item>
      <title>Why I picked Astro over Next.js for a calculator site (and shipped 12 of them)</title>
      <dc:creator>Shahzeb Khan</dc:creator>
      <pubDate>Mon, 27 Apr 2026 08:53:00 +0000</pubDate>
      <link>https://dev.to/shahzeb3939/why-i-picked-astro-over-nextjs-for-a-calculator-site-and-shipped-12-of-them-8go</link>
      <guid>https://dev.to/shahzeb3939/why-i-picked-astro-over-nextjs-for-a-calculator-site-and-shipped-12-of-them-8go</guid>
      <description>&lt;p&gt;I just shipped &lt;a href="https://blueprintcalc.com" rel="noopener noreferrer"&gt;blueprintcalc.com&lt;/a&gt; — 12 home improvement calculators (paint, concrete, drywall, deck, mulch, tile, etc.) that compute material quantities AND output a full shopping list with prices and retailer links.&lt;/p&gt;

&lt;p&gt;I'd defaulted to Next.js for static-ish sites for years. This time I went with Astro and don't regret it. Here's the framework comparison and what actually mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraints
&lt;/h2&gt;

&lt;p&gt;Before picking a framework, I wrote down what the site had to do:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;~95% of traffic will come from Google long-tail search.&lt;/strong&gt; ("how much paint for 12x14 room", "concrete bag calculator 4 inch slab" — that kind of query.) SEO is everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lighthouse 95+ on mobile or I lose ranking.&lt;/strong&gt; Google's Core Web Vitals are not optional for a content site competing with established players (Calculator.net, OmniCalculator).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calculator UI must be interactive&lt;/strong&gt; — change inputs, see results update — without round-tripping to a server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No backend.&lt;/strong&gt; No database. No auth. Every calculation is pure math on numeric inputs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static-deployable&lt;/strong&gt; to Cloudflare's free tier (zero hosting cost while traffic is small).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notice what's NOT on the list: real-time features, server-side rendering of personalized content, complex routing, API routes. The site is essentially 12 interactive widgets embedded in 37 SEO-optimized content pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Next.js was the wrong shape
&lt;/h2&gt;

&lt;p&gt;Next.js is excellent for app-shaped products: dashboards, SaaS, anything with auth + dynamic data. For a content site with islands of interactivity, it's overkill in ways that hurt:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bundle size.&lt;/strong&gt; Next.js ships React + the Next runtime + page-level hydration scaffolding by default. Even with aggressive code splitting, a "simple" Next page lands at ~80kb of JS minified. Multiply by 37 pages and you're paying for a runtime you barely use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hydration.&lt;/strong&gt; Next hydrates the entire page tree on load, even sections that don't need interactivity (FAQ, formula explanation, related-calc links — all static markup in my case). That's wasted JS execution on every page load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSG with a runtime.&lt;/strong&gt; Next.js can produce static HTML with &lt;code&gt;output: 'export'&lt;/code&gt;, but the JS bundle still ships React. You're paying React's cost without using its dynamism.&lt;/p&gt;

&lt;p&gt;I tried it anyway in a small prototype. The paint calculator page came in at 94kb of JS for what should have been ~3kb of input handling. Lighthouse mobile was 81. That's a no for SEO.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Astro does differently
&lt;/h2&gt;

&lt;p&gt;Astro's pitch is "ship zero JavaScript by default, opt-in per island."&lt;/p&gt;

&lt;p&gt;Concretely, this is the architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pages are written in &lt;code&gt;.astro&lt;/code&gt; files, which look like JSX/HTML but compile to plain HTML at build time.&lt;/li&gt;
&lt;li&gt;Any interactive component you want to ship (in any framework — React, Svelte, Vue, or vanilla JS) is wrapped as an "island" with a &lt;code&gt;client:load&lt;/code&gt;, &lt;code&gt;client:idle&lt;/code&gt;, or &lt;code&gt;client:visible&lt;/code&gt; directive.&lt;/li&gt;
&lt;li&gt;The rest of the page ships as static HTML with no JS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For my site, that means the calculator widget gets ~3kb of JS to handle input changes, and the FAQ / formula / related calculators sections ship zero JavaScript. Total per-page JS is roughly 4-6kb depending on the calc.&lt;/p&gt;

&lt;p&gt;Lighthouse mobile lands consistently at 95+. First Contentful Paint under 1.2s on simulated 4G.&lt;/p&gt;

&lt;h2&gt;
  
  
  The calc engine pattern
&lt;/h2&gt;

&lt;p&gt;Each calculator is a TypeScript module that exports two things — its input schema and a pure compute function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/calculators/paint.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Field&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;length&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Room length (ft)&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="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;width&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Room width (ft)&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="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;height&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Wall height (ft)&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="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;coats&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Number of coats&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="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Result&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;wallArea&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&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;coverage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;350&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// sq ft per gallon&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gallonsNeeded&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="nx"&gt;wallArea&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coats&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;coverage&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="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;gallonsNeeded&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; gallons of paint`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;materials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildShoppingList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gallonsNeeded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inputs&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;A single &lt;code&gt;&amp;lt;Calculator&amp;gt;&lt;/code&gt; Astro component reads the &lt;code&gt;fields&lt;/code&gt; array to render labeled inputs and binds &lt;code&gt;compute()&lt;/code&gt; to recalculate on every change. URL state is synced via query params so results are shareable / bookmarkable.&lt;/p&gt;

&lt;p&gt;Adding a 13th calculator is roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~150 lines of TypeScript (math + shopping list logic)&lt;/li&gt;
&lt;li&gt;A test file&lt;/li&gt;
&lt;li&gt;A page entry that imports the calc and points the layout at it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. No framework friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  SEO: schema.org markup is the unlock
&lt;/h2&gt;

&lt;p&gt;This is the part I underestimated. Every page emits three types of structured data via &lt;code&gt;&amp;lt;script type="application/ld+json"&amp;gt;&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;WebPage&lt;/code&gt; schema (basic page metadata)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HowTo&lt;/code&gt; schema for the formula explanation (gets you "How to" rich results in Google)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FAQPage&lt;/code&gt; schema for the FAQ section (gets you accordion rich results inline below your search snippet)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Astro makes this trivial — schema is just a function that returns a JSON object, and you inject it as a script tag in the layout's &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;. No framework-specific helper, no plugin.&lt;/p&gt;

&lt;p&gt;Without this markup, even a perfectly-optimized page is just a blue link. With it, Google often shows the FAQ inline below your title, which dramatically increases CTR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Next.js would still win
&lt;/h2&gt;

&lt;p&gt;To be fair: if any of these were on my requirements list, I'd have stayed with Next.js.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User auth + per-user data&lt;/strong&gt; — Astro can do server endpoints, but Next.js's app router is much more ergonomic for authenticated UIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time / streaming UIs&lt;/strong&gt; — Next.js Suspense + RSC are genuinely good for this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heavy form-driven app surfaces&lt;/strong&gt; — pages with 20+ stateful form fields, multi-step wizards, etc. Astro can do them but you're fighting the framework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedded as part of a larger React monorepo&lt;/strong&gt; — interop is painful enough that it's usually not worth fighting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For static, SEO-driven content sites with islands of interactivity, Astro is the right tool. For app-shaped products, it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;After ~3 weeks of building:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;12 calculators, 37 pages, 124 unit tests (Vitest)&lt;/li&gt;
&lt;li&gt;Lighthouse: 96 mobile, 99 desktop on the heaviest page&lt;/li&gt;
&lt;li&gt;Per-page JS: ~4-6kb minified+gzipped (calculator interactivity)&lt;/li&gt;
&lt;li&gt;Build time: 2 seconds for the full site&lt;/li&gt;
&lt;li&gt;Hosting cost: $0/month on Cloudflare Workers + Static Assets&lt;/li&gt;
&lt;li&gt;Total infra cost: $10/yr (just the domain)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Site is at &lt;a href="https://blueprintcalc.com" rel="noopener noreferrer"&gt;blueprintcalc.com&lt;/a&gt; if you want to poke at it.&lt;/p&gt;

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

&lt;p&gt;If I were starting over: nothing fundamental. Astro was clearly right for this shape of project. The thing I wasted time on was over-engineering the calculator engine in week 1 — I built a generic &lt;code&gt;fields[]&lt;/code&gt; schema before I had two calculators to compare. Two would have been enough to find the right abstraction; I built it from one.&lt;/p&gt;

&lt;p&gt;Lesson: don't generalize until you've copy-pasted the same thing at least twice.&lt;/p&gt;




&lt;p&gt;If you've built content/SEO sites with Astro and have war stories on programmatic page generation or schema markup gotchas, I'd love to hear them. Currently in the indexing-and-waiting phase before applying for AdSense — open to feedback on the build or the launch strategy.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>astro</category>
      <category>showdev</category>
      <category>seo</category>
    </item>
  </channel>
</rss>
