DEV Community

farcrak
farcrak

Posted on

How I built ~1,800 "honest" calculator pages with Next.js + SQLite

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.

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.

The data model: start from a source, not from blog numbers

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

Each food is a row with a reference point and a driver:

type CookingEntry = {
  slug: string;
  safeMinInternalTempC: number | null;
  referenceTempC: number;
  referenceTimeMin: number;
  referenceTimeMax: number;
  referenceThicknessCm: number | null;
  referenceWeightG: number | null;
  keyGeometryDriver: "thickness" | "total_weight";
};
Enter fullscreen mode Exit fullscreen mode

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²):

const ratio = input.thicknessCm / entry.referenceThicknessCm;
const factor = Math.pow(ratio, 1.2); // clamped to sane bounds
const minutes = referenceCenter * factor * tempFactor;
Enter fullscreen mode Exit fullscreen mode

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 honest about where it comes from, which is more than most results give you.

1 entity = 1 indexable page

The whole thing is programmatic SEO: every food, every city, every scenario is its own route under the Next.js App Router.

app/[locale]/cooking/how-long-to-cook/[slug]/page.tsx
app/[locale]/date-time/world-clock/[slug]/page.tsx
Enter fullscreen mode Exit fullscreen mode

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

SQLite is fine (really)

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 .backup 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.

Time zones will humble you

The world-clock pages use the Temporal API (via the polyfill) instead of raw Date, because DST math is where naive code dies:

const zoned = instant.toZonedDateTimeISO("Europe/Prague");
const offsetMin = Math.round(zoned.offsetNanoseconds / 60_000_000_000);
Enter fullscreen mode Exit fullscreen mode

Each page also computes the next 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.

Two footguns that cost me traffic

  1. www returning 200. Behind a proxy, request.nextUrl.hostname isn't reliable — I had to check Host and X-Forwarded-Host in middleware to 301 www → apex. Until I did, Google had two copies of every page and split the signals.
  2. hreflang x-default pointing at the wrong locale. 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.

Stack, for the curious

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

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

Top comments (0)