I recently shipped CalcFi — a site with 160+ financial calculators, each with a 2,000+ word guide, FAQs, and charts. The whole thing is statically generated on Vercel's free tier.
18,673 pages. Zero API calls at runtime. Sub-100ms TTFB.
Here's what I learned building it.
*The Architecture
*
Next.js 16 + App Router
├── 160 calculator directories
│ ├── page.tsx (server component — metadata, JSON-LD, article)
│ └── calc.tsx (client component — calculator UI, state, math)
├── 50 state tax pages (×3 variants each)
├── Category hubs, guides, glossary
└── Static generation for everything
Every calculator follows the same pattern: a server component handles SEO (metadata, structured data, the 2,000-word guide), and a client component handles the interactive calculator. The math never leaves the browser.
What Worked
1. The Two-File Pattern
Splitting each calculator into page.tsx (server) and calc.tsx (client) was the best architectural decision. The server component renders all the content Google needs to index — no hydration required for the article, FAQs, or structured data. The client component is only the interactive calculator.
This means Google sees a fully-rendered page with 2,000+ words of content on first crawl, even with JS disabled.
**2. Static Generation at Scale
**Next.js generateStaticParams handles the 18K pages surprisingly well. Build time: ~45 seconds. The trick is keeping each page's data requirements minimal — no database calls, no external APIs. Everything is computed from local data files.
// Each state × each calculator = thousands of pages
export async function generateStaticParams() {
return states.flatMap(state =>
calculators.map(calc => ({
state: state.slug,
calculator: calc.slug,
}))
)
}
3. Client-Side Math = Zero Server Costs
Every calculator runs entirely in the browser. No API routes, no serverless functions, no database. The formulas are in the client bundle.
This means:
- Vercel free tier handles everything
- No cold starts
- Works offline (once loaded)
- User data never leaves their browser (privacy win)
4. JSON-LD on Everything
Every page has structured data — WebApplication for calculators, FAQPage for FAQ sections, BreadcrumbList for navigation. This is tedious but it's what gets you rich snippets in search results.
const jsonLd = {
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "Retirement Calculator",
"url": "https://calcfi.app/calculators/retirement-calculator",
"applicationCategory": "FinanceApplication",
"operatingSystem": "Any",
"offers": { "@type": "Offer", "price": "0" }
}
What Broke
1. Build Memory at 18K Pages
Around 15,000 pages, the build started hitting memory limits on Vercel's free tier. The fix was splitting generateStaticParams into chunks and using export const dynamicParams = true as a fallback for edge cases. Not elegant, but it works.
2. Sitemap Size Limits
A single sitemap file maxes out at 50,000 URLs (per the spec), but Google practically gets unhappy above ~10,000. I split into multiple sitemap files using Next.js's built-in sitemap.ts:
export default function sitemap(): MetadataRoute.Sitemap {
return [
...calculatorPages,
...statePages,
...guidePages,
]
}
At 18K pages this needed chunking into sitemap index + child sitemaps.
3. OG Images at Scale
Each calculator has a dynamic OG image via opengraph-image.tsx. With 160+ calculators, that's 160+ edge function invocations for social previews. I moved to a single parameterized OG route that takes the calculator slug and generates the image on-demand.
4. Component Consistency Across 160 Calculators
The first 20 calculators were hand-crafted. By calculator #40, I had three different button colors, two different result card patterns, and inconsistent input heights. The fix was extracting a CalculatorShell component that enforces consistency — JSON-LD, FAQs, related tools, disclaimers all come free when you wrap your calc in the shell.
If I started over, I'd build CalculatorShell first and every calculator from day one.
The Numbers
- 18,673 static pages generated at build time
- Build time: ~45s on Vercel
- Lighthouse: 95+ performance across all pages
- Bundle per page: <120KB
- Monthly hosting cost: $0 (Vercel free tier)
- Total codebase: ~160 calculator modules, shared component library, data layer
Key Takeaways
Static generation scales further than you think. 18K pages on a free tier is wild. The constraint isn't page count — it's build memory and sitemap management.
Server components + client components is the right split for tool sites. SEO content renders server-side, interactive bits render client-side. Best of both worlds.
Build the shared component library first. I learned this the hard way. CalculatorShell should have been step 1, not step 40.
JSON-LD is worth the tedium. Rich snippets in search are the difference between 2% and 8% CTR.
Zero-runtime-cost architecture is underrated. No database, no API, no serverless = nothing to go down, nothing to pay for, nothing to scale.
The site is CalcFi.app if you want to see the result. All 160+ calculators, no signup, no ads.
Happy to answer questions about the architecture or Next.js static gen at scale.
Top comments (0)