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