All products

Calqlate

Live in production

Free financial calculators for India. Every calculation runs in the visitor's browser — no backend, no database, no runtime API calls, and every page is fully static, crawlable HTML.

Role
Architect & sole engineer
Live at
calqlate.angabani.com
  • Astro
  • TypeScript
  • Tailwind CSS v4
  • Chart.js
  • Vitest
  • sharp
  • Cloudflare Pages
  • GitHub Actions
  • Wrangler

Architecture, in one picture

BUILD TIME — runs once, on CI BROWSER — what the visitor downloads calculators.json Zod-validated collection lib/finance/*.ts pure functions, Vitest rendered once Astro static render formula · definitions · worked example · FAQ · initial results · schedule theme preset one .mjs file gen:brand CSS · logo · favicons OG image · manifest CI gate astro check → vitest → build → wrangler pages deploy static HTML to the edge Page is complete and readable with zero JavaScript executed crawlers · screen readers · answer engines all see everything on interaction Calculator island — ~4 KB gzip re-runs the same pure functions in the browser nothing typed is transmitted, stored or logged IntersectionObserver Chart.js — ~68 KB gzip separate chunk, only if the chart is scrolled to Lighthouse 100 / 100 / 100 / 100 · mobile deferred — never blocks first render
The split is the whole design. Everything a reader or a crawler needs is decided at build time; the browser downloads an interactive enhancement, not a prerequisite.

At a glance

Type Static, multi-page website with small interactive “islands”
Framework Astro (static output, islands architecture)
Language TypeScript (strict)
Styling Tailwind CSS v4, single generated theme layer
Charts Chart.js 4, lazy-loaded only where used
Tests Vitest — pure calculation logic, verified against known values
Hosting Cloudflare Pages (static edge)
CI/CD GitHub Actions → wrangler pages deploy
Performance Lighthouse 100 / 100 / 100 / 100 (mobile, preview build) · ~4 KB first-load JS
Currency INR (₹) only, Indian digit grouping (lakh / crore)

Tech stack

Core

Concern Choice Notes
Site framework Astro 7 output: 'static'; islands hydrate individually; zero JS by default
Language TypeScript 6, strict astro check in CI gate
Styling Tailwind CSS v4 via @tailwindcss/vite @theme tokens; no config file; one generated theme partial
Content Astro Content Collections one JSON file, Zod-validated schema, file() loader
Charts Chart.js 4 dynamic import() inside a single client:visible-style island, gated by IntersectionObserver
Sitemap @astrojs/sitemap calculator pages boosted in priority
Unit tests Vitest (+ v8 coverage) math layer only — no DOM, no framework
Icon / OG generation sharp build-time raster generation from vector source

Tooling & delivery

Concern Choice
Package manager npm (package-lock.json, npm ci in CI)
Runtime (build) Node ≥ 22.12 (CI pinned to 22.19)
Hosting Cloudflare Pages, custom domain, trailingSlash: 'always'
Deploy GitHub Actions: npm ciastro checkvitestastro buildwrangler pages deploy
Edge headers public/_headers — security headers + immutable caching for hashed assets

Deliberately not used

No backend framework, no database, no ORM, no auth, no state store, no CSS-in-JS runtime, no web fonts (system font stack), no client-side router. Interactivity is progressive enhancement layered on static HTML.


Architecture

Principles

  1. Static & crawlable. Every page renders its full content — headings, formulas, definitions, worked examples, FAQs — as raw HTML with no hydration required. Interactive widgets are an enhancement, never a prerequisite for the information.
  2. All math on the client. Calculations run in the browser. Nothing the visitor types is transmitted, stored, or logged.
  3. Pure calculation core. Financial formulas live in a UI-free library of pure functions, each unit-tested against an independently verifiable value before it is wired to a page.
  4. SEO + GEO first. Semantic HTML, one <h1>, clean URLs, sitemap.xml, robots.txt that explicitly welcomes AI answer-engine crawlers, and schema.org structured data on every page.
  5. Core Web Vitals as a budget. Ship the smallest possible JS; reserve space for late-loading widgets so layout never shifts.

Repository structure

src/
  pages/
    index.astro                     # home / landing
    calculators/
      index.astro                   # calculator directory
      mortgage-calculator.astro     # one .astro file per calculator page
    robots.txt.ts                   # generated robots.txt (AI crawlers allow-listed)
  layouts/
    BaseLayout.astro                # <head>, meta, Open Graph / Twitter, JSON-LD
  components/
    CalculatorLayout.astro          # shared calculator shell: header, footer,
                                    #   breadcrumb, ad slots, schema, related list
    MortgageCalculator.astro        # the interactive island (form + results + charts)
    Faq.astro                       # FAQ list + matching FAQPage JSON-LD
    SiteHeader / SiteFooter / AdSlot / Logo
  lib/
    finance/
      mortgage.ts                   # PURE math: payment, amortization schedule, extras
      mortgageView.ts               # maps calc results -> display-ready view model
    format/
      currency.ts                   # the ONLY place Intl / en-IN formatting lives
    categories.ts                   # calculator category grouping + labels
  content/
    calculators.json                # one record per calculator (drives head/schema/lists)
  content.config.ts                 # Zod schema for the collection
  styles/
    global.css                      # Tailwind entry + base layer + component classes
    theme.generated.css             # AUTO-GENERATED theme (do not edit)
tests/
  mortgage.test.ts                  # known-value validation of the finance core
  currency.test.ts                  # formatting edge cases
themes/
  ledger-desk.mjs                   # theme preset — warm paper retro
  graphite.mjs                      # theme preset — neutral dark grey
  midnight.mjs                      # theme preset — code-editor dark, blue accent
theme.config.mjs                    # one-line selector: which preset is active
scripts/
  gen-theme.mjs                     # preset -> theme.generated.css + logo/favicon/OG SVGs + manifest
  gen-icons.mjs                     # preset -> raster icons + og.png (sharp)
public/                             # static assets, _headers, generated icons

Render & data flow

calculators.json + Zod schema
        │  getCollection

   Astro page (.astro)


 CalculatorLayout.astro ──────────► BaseLayout.astro
        │                            head · OG/Twitter · JSON-LD
        ├──► Static explanatory HTML  (formula, terms, worked example, FAQ)

        └──► MortgageCalculator island
                  ▲                    │
                  │ server render      │ hydrated
    src/lib/finance/*.ts               ▼
      (pure functions)      Client recalculation on input, in-browser
                                       │  IntersectionObserver

                             Chart.js loaded on demand

theme.config.mjs -> themes/*.mjs ──npm run gen:brand──► theme.generated.css
                                                        + logo / favicon / OG
  • The content collection (calculators.json, validated by a Zod schema) is the single source for each calculator’s <title>, meta description, keywords, FAQ entries, related-calculator list, and JSON-LD.
  • Each page composes CalculatorLayoutBaseLayout. The layout server-renders the finance functions once to produce the initial numbers, table, and chart data, so the page is useful before any JavaScript runs.
  • The calculator island is the only hydrated component. On input it re-runs the same pure functions in the browser and updates the DOM.
  • Chart.js (~68 KB gzip) is a separate chunk, dynamically imported the first time the chart scrolls near the viewport.

Data model — calculators.json

{
  "slug": "mortgage-calculator",        // -> /calculators/mortgage-calculator/
  "title": "Mortgage Calculator",
  "shortTitle": "Mortgage",
  "category": "home-loan",
  "description": "… meta description, <= 160 chars …",
  "keywords": ["mortgage calculator", "home loan emi", "…"],
  "featured": true,
  "faqs": [{ "question": "…", "answer": "… plain text / minimal inline HTML …" }],
  "related": ["amortization-calculator", "auto-loan-calculator"]
}

Key engineering details

1 · Zero-hydration content, one small island

The calculator page ships ~4 KB gzip of first-load JavaScript — just the island that wires up the form. Everything a search engine, screen reader, or AI answer engine needs (the formula, the definitions, a worked example, the FAQ, the initial results, the amortization table) is in the server-rendered HTML. Chart.js loads only on demand and only if the user reaches the chart.

2 · Pure, tested finance core

src/lib/finance/** contains pure functions only — plain numbers in, plain numbers/objects out. No Intl, no DOM, no rounding for display; full precision is preserved and rounding happens only at the presentation edge (src/lib/format/currency.ts, the single home of en-IN formatting).

Every formula has a Vitest test asserting it against an independently calculable value (e.g. a known EMI for a given principal / rate / term, the final-payment adjustment that drives the balance to exactly zero) before it is used on a page. astro check and vitest are both required-green in CI.

3 · Single-source theming

The entire visual identity — palette, corner radii, font stacks, chart colours, logo, favicons, Open Graph image, PWA manifest colours — resolves to one file.

themes/<name>.mjs      # a complete theme preset (all values)
theme.config.mjs       # one line: which preset is active

        ▼  npm run gen:brand   (also runs automatically before every build)

src/styles/theme.generated.css   # Tailwind @theme block + :root semantic vars
public/{favicon,logo,logo-mono-*}.svg
public/{icons, og.png, favicon.ico}   # rasterised with sharp
public/site.webmanifest
  • Tailwind’s slate-* / brand-* scales are remapped in the generated @theme block, so hundreds of existing utility classes re-skin with no markup changes.
  • A small set of semantic custom properties (--surface, --ink, --shadow, --accent, --accent-text, --mark-stroke, --chart-1..6, color-scheme, …) covers the things Tailwind utilities can’t reach: the inline logo SVG, the hard-shadow colour, and the Chart.js palette (read once via getComputedStyle when the chart initialises).
  • Three presets ship: ledger-desk (warm-paper retro), graphite (neutral dark grey), midnight (near-black, editor blue). Switching is a one-line edit plus npm run gen:brand. Runtime cost of the system is zero beyond ordinary CSS variables.

4 · SEO, GEO & structured data

  • Semantic document outline, single <h1>, ordered headings, clean slash-terminated URLs.
  • sitemap-index.xml generated at build; individual calculator pages get a priority boost over index pages.
  • robots.txt is generated (src/pages/robots.txt.ts) and explicitly allow-lists answer-engine and model crawlers — GPTBot, ClaudeBot, PerplexityBot, Google-Extended, Applebot-Extended, CCBot, and others — so GEO visibility is unambiguous.
  • JSON-LD on every surface: WebApplication + BreadcrumbList on calculator pages, FAQPage wherever an FAQ renders (generated from the same source as the visible list, so they can’t drift), WebSite on the home page.
  • Full Open Graph + Twitter Card tags, including image dimensions and alt text; the OG image itself is regenerated per theme.

5 · Performance & Core Web Vitals

  • Lighthouse 100 / 100 / 100 / 100 (Performance / Accessibility / Best Practices / SEO) on a mobile-emulated preview build.
  • System font stack — no web-font download, no render-blocking requests.
  • First-load JS ≈ 4 KB gzip (island only). Chart.js ≈ 68 KB gzip is a separate, lazily-imported chunk.
  • Ad-slot containers are reserved at their real dimensions from day one, so filling them later causes no cumulative layout shift.
  • public/_headers sets Cache-Control: public, max-age=31536000, immutable for content-hashed /_astro/* assets, plus X-Content-Type-Options, Referrer-Policy, X-Frame-Options, and a minimal Permissions-Policy.

6 · CI/CD & hosting

Push to main triggers GitHub Actions:

npm ci  →  npm run check (astro check)  →  npm test (vitest)  →  npm run build  →  wrangler pages deploy dist
  • prebuild regenerates theme.generated.css from the active preset, so a deploy can never ship a stale theme.
  • wrangler is pinned as a dev dependency (installed by the cached npm ci) so the publish step never does a fresh, flaky npm i wrangler on the runner.
  • Output is a fully static dist/ served from Cloudflare’s edge. trailingSlash: 'always' matches Cloudflare Pages’ canonical URL form, so there are no redirect hops on canonical links, the sitemap, or internal links.

Local development

npm install
npm run dev            # Astro dev server
npm test               # Vitest (watch: npm run test:watch)
npm run check          # astro check (types + template diagnostics)
npm run build          # static build -> dist/
npm run preview        # serve the production build locally

npm run gen:brand      # regenerate theme CSS + logo/favicon/OG from the active preset

Switching theme: edit the single re-export line in theme.config.mjs to point at another file in themes/, run npm run gen:brand, commit.


Currency is INR-only by design; a multi-currency mode is deliberately out of scope. All monetary display uses Intl.NumberFormat('en-IN', …) for lakh/crore grouping.

Next

See the rest of the work, or get in touch.