DEV Community

Cover image for When Static Sites Stop Scaling: Migrating FreeDevTools (125K+ Pages) from Static Astro to SSR
Athreya aka Maneshwar
Athreya aka Maneshwar

Posted on

When Static Sites Stop Scaling: Migrating FreeDevTools (125K+ Pages) from Static Astro to SSR

Hello, I'm Maneshwar. I'm working on FreeDevTools online currently building one place for all dev tools, cheat codes, and TLDRs — a free, open-source hub where developers can quickly find and use tools without any hassle of searching all over the internet.

FreeDevTools started as a static Astro Dev Resource site.

It worked perfectly when the project had a few thousand resources.

But as the collection crossed 125,000+ pages, the static approach collapsed under its own weight.

This post documents the real migration journey, why static rendering broke, why SSR became unavoidable, and the exact technical changes needed to convert Astro static routes into stable SSR pages without breaking routing, pagination, or content collections.

This is not theory. This is exactly what happened while migrating tldr, mcp, svg icons, and other massive datasets from static to SSR.

1. Why Static Rendering Became Impossible

As FreeDevTools grew, every full build had to:

  • generate 125k+ HTML pages
  • compress each page with gzip and zstd
  • inline critical CSS into every page
  • write final output > 40GB

Final result:

  • full build time → 6+ hours
  • deployment → way too heavy for each update
  • partial deploy didn’t help much, because many pages depended on shared optimizations (critical CSS, layout changes, metadata improvements)

Whenever we optimized page speed globally, we had to rebuild everything.

Static wasn’t just slow — it became unmanageable.

SSR solved this:

  • less build-time page generation
  • real-time rendering
  • no massive artifacts
  • small deployments
  • content changes appear immediately

But turning a huge static Astro site into SSR is not a 1-line config change.

The migration exposed several route collisions, priority issues, and Astro SSR quirks.

2. What Actually Changes When Switching Astro to SSR?

Static pages rely on:

  • getStaticPaths()
  • Astro.props
  • pre-generated routes
  • pre-rendered HTML

SSR pages rely on:

  • no prerendering
  • Astro.params
  • dynamic fetching inside the component
  • runtime content loading

Static → SSR Examples

Before (static):

export async function getStaticPaths() {
  const items = await getCollection('collection');
  return items.map((item) => ({ params: { id: item.id }, props: { data: item.data } }));
}

const { data } = Astro.props;
Enter fullscreen mode Exit fullscreen mode

After (SSR):

export const prerender = false;

const { id } = Astro.params;
const entries = await getCollection('collection');
const item = entries.find((e) => e.id === id);
Enter fullscreen mode Exit fullscreen mode

Key rule: Astro.props does not exist in SSR pages.

3. The First Big Problem: Route Collisions in SSR

Static mode silently resolves overlapping dynamic routes because each path is explicitly generated.

SSR mode does NOT tolerate ambiguous patterns.

Example from TLDR:

/tldr/[platform]/[command]
/tldr/[platform]/[page]
Enter fullscreen mode Exit fullscreen mode

Both match /tldr/adb/shell.

In SSR this immediately breaks the build.

Solution: Merge these into a single route

src/pages/tldr/[platform]/[slug].astro
Enter fullscreen mode Exit fullscreen mode

Inside it:

const { platform, slug } = Astro.params;

const isNumeric = /^\d+$/.test(slug);

if (isNumeric) {
  // pagination
} else {
  // command page
}
Enter fullscreen mode Exit fullscreen mode

This pattern repeats across other sections like MCP and SVG icons.

4. Removing Static Generation Logic

Every SSR file needs:

export const prerender = false;
Enter fullscreen mode Exit fullscreen mode

And delete every getStaticPaths() — if you forget, Astro throws warning:

[WARN] getStaticPaths() ignored in SSR mode
Enter fullscreen mode Exit fullscreen mode

Then replace:

Static SSR
Astro.props Direct fetching
getStaticPaths() No longer exists
Pre-generated params Use Astro.params
Pre-generated data Query content collections per request

5. Content Collections Still Work — But Fetch Dynamically

Astro content collections behave the same in SSR:

const entries = await getCollection('tldr');
Enter fullscreen mode Exit fullscreen mode

Rendering markdown still uses:

const { Content } = await render(entry);
Enter fullscreen mode Exit fullscreen mode

The only difference is:
You fetch at runtime instead of during build.

6. The Biggest Pain: Route Priority in SSR

Astro’s SSR router matches the most specific file first, not the most logical one.

Example:

/tldr/[page].astro
/tldr/[platform]/index.astro
Enter fullscreen mode Exit fullscreen mode

For URL /tldr/adb/, Astro may match:

  • the wrong one ([page].astro)
  • before the correct platform index route

The fix is NOT middleware rewrites.

They cause infinite redirects.

Fix: Handle route priority inside the route itself.

Example from TLDR:

// Inside [page].astro
if (!/^\d+$/.test(page)) {
  // maybe it's a platform
  if (!url.endsWith('/')) return Astro.redirect(url + '/', 301);

  const isPlatform = platforms.includes(page);

  if (isPlatform) {
    return renderPlatformIndex(page);
  }
}
Enter fullscreen mode Exit fullscreen mode

This made the routing stable again.

7. Avoid Middleware Rewrites (Critical Lesson)

Middleware rewrites caused:

ERR_TOO_MANY_REDIRECTS
HTTP 508: rewrite loop detected
Enter fullscreen mode Exit fullscreen mode

Reason:
Middleware rewrite + route-file redirect = infinite loop.

Final working pattern:

// src/middleware.ts
export const onRequest = (_, next) => next();
Enter fullscreen mode Exit fullscreen mode

Middleware does nothing.
Route files handle everything.

8. Real Examples From the Migration

TLDR

  • Consolidated (command + pagination) into [slug].astro
  • [page].astro also handles platform detection because route priority matches it first
  • No middleware rewrites
  • Only inline redirects inside route file

MCP

MCP had the worst collision:

/mcp/[category]/index
/mcp/[page]
Enter fullscreen mode Exit fullscreen mode

[category]/index matched numeric categories like /mcp/1/

Solution:

Inside [category]/index.astro, detect numeric category:

if (/^\d+$/.test(category)) {
  // this is actually the main pagination
  return renderMainPagination();
}
Enter fullscreen mode Exit fullscreen mode

This fixed the priority problem permanently.

9. Performance Notes After Migration

  • Less build-time page generation
  • Builds shrank from 40GB → ~100MB
  • Deploy time dropped from 6 hours → minutes
  • Content changes reflected instantly
  • No more partial build hacks required

SSR + caching (if needed later) keeps request-time performance solid.

Conclusion

This migration reinforced a simple rule: the architecture that works for 5k pages will not work for 125k.

Static generation collapses under size, but SSR requires strict routing discipline.

After aligning the routes, consolidating dynamic segments, and removing static-era assumptions, the site now performs reliably without the build-time overhead.

The outcome is a cleaner codebase and a system that can grow further without hitting build limits again.

FreeDevTools

Any feedback or contributors are welcome!

It’s online, open-source, and ready for anyone to use.

👉 Check it out: FreeDevTools
⭐ Star it on GitHub: freedevtools

Top comments (0)