DEV Community

Liam Altie
Liam Altie

Posted on

How I Serve 10,000+ Dynamic Pages for Free with Next.js ISR (and the Mistakes I Made)

I wanted to build a financial dashboard that covers every public company in the US. That's 10,000+ unique pages — each with charts, tables, and real-time data from the SEC.

The catch? I wanted to host it for free on Vercel's hobby tier.

Here's how I did it, and the bugs that almost killed the project.

The result

secedgardata.com — a free dashboard for 10,000+ public companies.

SEC EDGAR Dashboard showing Alphabet Inc. revenue chart in dark mode

Financial statements table showing Alphabet Inc. income statement with multi-year data

Search any ticker, see revenue charts, income statements, balance sheets, and SEC filing history. No login, no API key.

The problem: 10,000 pages that can't be static

My backend (FastAPI on a home server) serves financial data for every SEC-registered company. I wanted each company to have its own page: /stock/AAPL, /stock/MSFT, /stock/PLTR, and so on.

The naive approach — generateStaticParams for all tickers — was out:

  • Build time: 10,000+ API calls at build = minutes of build time
  • Build server can't reach my backend: Vercel's build servers are in AWS. My backend runs on a mini PC at home behind Tailscale. Connection refused.

That second one burned me. The build looked fine locally, but every deploy on Vercel failed with ECONNRESET.

The fix: On-demand ISR with no static params

// src/app/stock/[ticker]/page.tsx
export const revalidate = 86400; // 24 hours
export const dynamicParams = true;
// No generateStaticParams — all pages are on-demand
Enter fullscreen mode Exit fullscreen mode

That's it. When someone visits /stock/AAPL:

  1. Next.js calls my backend, renders the page
  2. Caches it at the edge for 24 hours
  3. Every subsequent visitor gets the cached version instantly

First visit is ~800ms. After that, it's <50ms from Vercel's CDN.

Mistake #1: Error handling that caches 404s

My API client originally threw errors on network failures:

// ❌ Bad: throws on network errors
const resp = await fetch(url);
if (!resp.ok) throw new Error(`API error: ${resp.status}`);
Enter fullscreen mode Exit fullscreen mode

The problem: if my backend was temporarily down, Next.js would cache the error page as a 404. And that cached 404 would persist for 24 hours.

Amazon's page returned 404 for an entire day because of one transient network blip.

// ✅ Good: return null, let the page show graceful fallback
try {
  const resp = await fetch(url, {
    headers: { "x-dashboard-key": API_KEY },
    next: { revalidate: 86400 },
  });
  if (!resp.ok) return null;
  return resp.json();
} catch {
  return null;
}
Enter fullscreen mode Exit fullscreen mode

Mistake #2: Sentry not working because of Next.js 15

I set up @sentry/nextjs with the standard sentry.client.config.ts. Deployed. Threw test errors. Nothing appeared in Sentry.

Spent an hour checking DSN, environment variables, tunnel routes — everything looked correct. Then I checked the browser console: no Sentry logs at all. The file wasn't even being loaded.

Turns out Next.js 15 with Turbopack ignores sentry.client.config.ts. You need src/instrumentation-client.ts instead:

// src/instrumentation-client.ts
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: "your-dsn-here",
  tracesSampleRate: 0.1,
});
Enter fullscreen mode Exit fullscreen mode

This is documented in the Sentry SDK source but easy to miss. If you're on Next.js 15 and Sentry events aren't showing up — this is probably why.

The sitemap trick: 10,464 URLs

Google needs to know about all 10,000+ pages. I added a dynamic sitemap:

// src/app/sitemap.ts
export const revalidate = 86400;

export default async function sitemap() {
  const tickers = await getAllTickers(); // ~10,464 tickers

  return tickers.map((t) => ({
    url: `https://secedgardata.com/stock/${t.ticker}`,
    changeFrequency: "weekly",
    priority: 0.8,
  }));
}
Enter fullscreen mode Exit fullscreen mode

Submitted to Google Search Console. Got 10,464 URLs recognized on day one.

One gotcha: make sure the domain in your sitemap matches your Search Console property. I had a stale fallback URL from development and Google rejected all 10,464 URLs as "not permitted." Felt great.

The stack

Layer Choice Why
Framework Next.js 15 (App Router) ISR, Server Components
Styling Tailwind + shadcn/ui Fast to build, dark mode free
Charts Recharts Lightweight, React native
Backend FastAPI (Python) Already had it for my API
Hosting Vercel (free tier) ISR + edge caching
Monitoring Sentry + Vercel Analytics Error tracking + PV

Total hosting cost: $0/month (Vercel free tier handles it fine with ISR caching).

For developers: the same data as an API

The backend that powers this dashboard is also available as a REST API:

pip install sec-edgar-sdk

from sec_edgar import SecEdgar
api = SecEdgar("YOUR_RAPIDAPI_KEY")

for year in api.revenue("AAPL", limit=5):
    print(f"FY{year['fiscal_year']}: ${year['value']:,.0f}")
Enter fullscreen mode Exit fullscreen mode

Try it: secedgardata.com

If you hit any bugs or have feature requests, let me know in the comments.

Top comments (0)