<?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: farcrak</title>
    <description>The latest articles on DEV Community by farcrak (@farcrak).</description>
    <link>https://dev.to/farcrak</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%2F4016757%2Ffdb0d602-e6f5-466d-b816-f1f0f8bd79a9.jpg</url>
      <title>DEV Community: farcrak</title>
      <link>https://dev.to/farcrak</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/farcrak"/>
    <language>en</language>
    <item>
      <title>How I built ~1,800 "honest" calculator pages with Next.js + SQLite</title>
      <dc:creator>farcrak</dc:creator>
      <pubDate>Tue, 07 Jul 2026 00:57:08 +0000</pubDate>
      <link>https://dev.to/farcrak/how-i-built-1800-honest-calculator-pages-with-nextjs-sqlite-4pop</link>
      <guid>https://dev.to/farcrak/how-i-built-1800-honest-calculator-pages-with-nextjs-sqlite-4pop</guid>
      <description>&lt;p&gt;It started with a dumb frustration. Every "how long to cook chicken thighs" page gives you a different number, and none of them tell you where the number comes from. Some say 25 minutes, some say 40, and you're standing there with a thermometer wondering who to trust.&lt;/p&gt;

&lt;p&gt;So I did the developer thing and over-engineered a solution. What began as one cooking-time calculator turned into ~1,800 generated pages. Here's how it's built and a couple of footguns that cost me real traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data model: start from a source, not from blog numbers
&lt;/h2&gt;

&lt;p&gt;The core idea: don't hardcode "chicken = 30 min". Start from the &lt;strong&gt;official safe internal temperature&lt;/strong&gt; (USDA/FSIS charts — poultry 74°C, whole cuts 63°C + rest, fish 63°C) and treat time as a function of geometry.&lt;/p&gt;

&lt;p&gt;Each food is a row with a reference point and a driver:&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CookingEntry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;safeMinInternalTempC&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;referenceTempC&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;referenceTimeMin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;referenceTimeMax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;referenceThicknessCm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;referenceWeightG&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;keyGeometryDriver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;thickness&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;total_weight&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the estimate scales the reference time by how far your piece is from the reference, using a power law instead of a linear guess (a steak twice as thick doesn't take twice as long — heat diffusion is closer to thickness²):&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thicknessCm&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;referenceThicknessCm&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;factor&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="nx"&gt;ratio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// clamped to sane bounds&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;minutes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;referenceCenter&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;factor&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;tempFactor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is it perfect? No — it's a household estimate, and the page says so and shows the safe temperature so you can verify with a thermometer. But it's &lt;em&gt;honest about where it comes from&lt;/em&gt;, which is more than most results give you.&lt;/p&gt;

&lt;h2&gt;
  
  
  1 entity = 1 indexable page
&lt;/h2&gt;

&lt;p&gt;The whole thing is programmatic SEO: every food, every city, every scenario is its own route under the Next.js App Router.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/[locale]/cooking/how-long-to-cook/[slug]/page.tsx
app/[locale]/date-time/world-clock/[slug]/page.tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Metadata is generated per-entity in &lt;code&gt;generateMetadata&lt;/code&gt;, and there's a dynamic &lt;code&gt;app/sitemap.ts&lt;/code&gt; that enumerates every dataset entry. The trap here is &lt;code&gt;lastmod&lt;/code&gt;: I first set it to &lt;code&gt;new Date()&lt;/code&gt;, which re-stamped all ~1,800 URLs as "changed" on every deploy. Crawlers learn to distrust that fast. Fix: a single &lt;code&gt;datasetLastReviewed&lt;/code&gt; constant I bump only when the data actually changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  SQLite is fine (really)
&lt;/h2&gt;

&lt;p&gt;The datastore is SQLite via Prisma. For a read-heavy site with curated datasets it's genuinely fine — no connection pool, no separate DB server, the whole thing is a file you can &lt;code&gt;.backup&lt;/code&gt; in a cron job. The dataset itself lives in versioned TSV/JSON, so the DB is more of a cache than a source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Time zones will humble you
&lt;/h2&gt;

&lt;p&gt;The world-clock pages use the &lt;code&gt;Temporal&lt;/code&gt; API (via the polyfill) instead of raw &lt;code&gt;Date&lt;/code&gt;, because DST math is where naive code dies:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zoned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;instant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toZonedDateTimeISO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Europe/Prague&lt;/span&gt;&lt;span class="dl"&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;offsetMin&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;zoned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetNanoseconds&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000_000_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each page also computes the &lt;em&gt;next&lt;/em&gt; clock change by binary-searching for the offset flip, so a "12:00 UTC to Kyiv" page can tell you when the answer will shift. Real IANA zones, date-aware offsets, no static UTC+2 lies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two footguns that cost me traffic
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;www&lt;/code&gt; returning 200.&lt;/strong&gt; Behind a proxy, &lt;code&gt;request.nextUrl.hostname&lt;/code&gt; isn't reliable — I had to check &lt;code&gt;Host&lt;/code&gt; and &lt;code&gt;X-Forwarded-Host&lt;/code&gt; in middleware to 301 &lt;code&gt;www&lt;/code&gt; → apex. Until I did, Google had two copies of every page and split the signals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hreflang&lt;/code&gt; &lt;code&gt;x-default&lt;/code&gt; pointing at the wrong locale.&lt;/strong&gt; It decides what Google serves everyone outside your declared locales. Mine defaulted to the non-English version while the inventory targeted English queries. One-line change, big difference.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Stack, for the curious
&lt;/h2&gt;

&lt;p&gt;Next.js (App Router) + TypeScript + Tailwind + Prisma/SQLite, ~1,800 pages generated from curated datasets, deployed with a boring &lt;code&gt;git pull&lt;/code&gt; + &lt;code&gt;pm2 restart&lt;/code&gt; script. Solo project.&lt;/p&gt;

&lt;p&gt;If you want to poke at the actual output, it's live and free (no signups, no paywall): &lt;a href="https://theunitools.com/en" rel="noopener noreferrer"&gt;https://theunitools.com/en&lt;/a&gt; — I'd genuinely like to hear if a cooking time looks off to you, since that's the part I care about most.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>sqlite</category>
      <category>seo</category>
    </item>
  </channel>
</rss>
