NNicheStackTutorials
← All tutorials
Astro

Multi-Page Calculator Tool in Astro (Static Shell, Interactive Islands)

Loan calculator across 3 pages — shared Svelte island, prerendered shells, URL state via query params.

·14 min read·Astro
Multi-Page Calculator Tool in Astro (Static Shell, Interactive Islands) cover

Tested on: Astro 5.7, Svelte 5 Tool: /tools/loan-calculator with amortization table + chart pages.

Structure

src/pages/tools/loan-calculator/
├── index.astro          # input form
├── schedule.astro       # amortization table
└── chart.astro          # payoff chart
src/components/loan/
├── LoanForm.svelte
├── AmortTable.svelte
└── PayoffChart.svelte

Shared state via URL (no backend)

LoanForm.svelte:

<script lang="ts">
  let principal = $state(200000);
  let rate = $state(6.5);
  let years = $state(30);

  function navigate(target: string) {
    const q = new URLSearchParams({
      p: String(principal),
      r: String(rate),
      y: String(years)
    });
    window.location.href = `${target}?${q}`;
  }
</script>

<label>Principal <input type="number" bind:value={principal} /></label>
<label>Rate % <input type="number" step="0.1" bind:value={rate} /></label>
<label>Years <input type="number" bind:value={years} /></label>

<button onclick={() => navigate('/tools/loan-calculator/schedule/')}>
  View Schedule
</button>

index.astro

---
import Layout from '../../../layouts/ToolLayout.astro';
import LoanForm from '../../../components/loan/LoanForm.svelte';
---

<Layout title="Loan Calculator">
  <LoanForm client:load />
</Layout>

schedule.astro — read query params

---
import Layout from '../../../layouts/ToolLayout.astro';
import AmortTable from '../../../components/loan/AmortTable.svelte';
---

<Layout title="Amortization Schedule">
  <AmortTable client:load />
</Layout>

AmortTable.svelte:

<script lang="ts">
  import { onMount } from 'svelte';

  let rows = $state<{ month: number; payment: number; balance: number }[]>([]);

  onMount(() => {
    const q = new URLSearchParams(window.location.search);
    const p = Number(q.get('p') ?? 200000);
    const r = Number(q.get('r') ?? 6.5) / 100 / 12;
    const n = Number(q.get('y') ?? 30) * 12;
    const payment = (p * r * Math.pow(1 + r, n)) / (Math.pow(1 + r, n) - 1);

    let balance = p;
    for (let i = 1; i <= n; i++) {
      const interest = balance * r;
      balance -= payment - interest;
      rows.push({ month: i, payment, balance: Math.max(0, balance) });
    }
  });
</script>

<table>
  <thead><tr><th>Month</th><th>Payment</th><th>Balance</th></tr></thead>
  <tbody>
    {#each rows as row}
      <tr><td>{row.month}</td><td>{row.payment.toFixed(2)}</td><td>{row.balance.toFixed(2)}</td></tr>
    {/each}
  </tbody>
</table>

Error — chart lib bloating every page

chart.js imported in shared layout — 190 KB on index

Only import Chart.js in PayoffChart.svelte. Only load on chart.astro with client:visible.

SEO per tool page

Each .astro page gets unique <title> and <meta description>. Google indexes /tools/loan-calculator/schedule/ separately.

Build

npm run build
# 3 static HTML shells + shared Svelte chunks
ls dist/tools/loan-calculator/
index.html: 4 KB + 14 KB JS (form only)
schedule.html: 4 KB + 18 KB JS (table only)
chart.html: 4 KB + 95 KB JS (chart lazy)

URL state means shareable links, no database, works on any static host.

Tags: AstroCalculatorSvelteIslands