When I first picked up Next.js, its promise felt almost unfair: routing without a router config, hybrid rendering, optimization out of the box, and sensible production defaults. But the moment I needed more than a few static pages, blog articles, project pages, protocol docs, I hit the question: How do I scale content without duplicating files?
This is where dynamic routing and pre-rendering strategy clicked for me. Below is what I learned, framed for anyone moving from "copy/paste pages" to a maintainable system.
Why Pre‑Rendering Matters
Traditional client-only React waits for JavaScript to load, then renders. Next.js flips that by letting you produce HTML ahead of time (improving perceived performance, SEO, and accessibility) while still hydrating into an interactive app.
Rendering modes (mental model):
| Mode | When HTML Generated | Good For | Notes |
|---|---|---|---|
| SSG (Static Generation) | Build time | Blogs, docs, marketing | Fast CDN cache; rebuild to update |
| ISR (Incremental Static Regeneration) | On-demand after revalidation | Large catalogs, near-static data | Combines static speed + freshness |
| SSR (Server-Side Rendering) | Per request | User dashboards, auth-specific pages | Dynamic but higher TTFB |
| RSC (React Server Components) | Server on demand (streamed) | Data-heavy layout & partial hydration | App Router default advantage |
Start static whenever possible. Only move *toward* SSR if a real-time dependency demands it.
The Static File Trap
Early on I wrote pages like:
/pages/posts/post-1.js
/pages/posts/post-2.js
/pages/posts/post-3.jsFine for 3 posts. Terrible for 300. Repetition + zero abstraction.
The breakthrough: let the filesystem declare a pattern, and let code supply the variable parts.
Pages Router vs App Router (Context)
If you're using the legacy /pages directory:
[slug].tsxgetStaticProps, getStaticPaths, getServerSidePropsIn the App Router (`/app`) world:
/app/posts/[slug]/page.tsxexport async function generateStaticParams()export async function generateMetadata()I'll show both mental models so you can transfer knowledge.
Classic Pages Router Pattern (SSG)
// lib/posts.ts
// Mock post registry (replace with CMS, DB, filesystem)
export const posts = [
{ slug: 'hello-world', title: 'Hello World', date: '2025-08-01', content: '...' },
{ slug: 'deep-dive-routing', title: 'Routing Deep Dive', date: '2025-08-12', content: '...' },
];
export function getAllSlugs() { return posts.map(p => p.slug); }
export function getPost(slug: string) { return posts.find(p => p.slug === slug)!; }
// pages/posts/[slug].tsx
import { getAllSlugs, getPost } from '../../lib/posts';
import type { GetStaticPaths, GetStaticProps } from 'next';
export const getStaticPaths: GetStaticPaths = async () => ({
paths: getAllSlugs().map(slug => ({ params: { slug } })),
fallback: false // or 'blocking' for large catalogs
});
export const getStaticProps: GetStaticProps = async ({ params }) => {
const post = getPost(params!.slug as string);
return { props: { post } };
};
export default function PostPage({ post }: { post: any }) {
return (
<article>
<h1>{post.title}</h1>
<time>{post.date}</time>
<p>{post.content}</p>
</article>
);
}Key pieces:
getStaticPaths enumerates which dynamic routes to pre-render.getStaticProps injects per-page data at build time.App Router Equivalent
In /app/posts/[slug]/page.tsx you no longer need getStaticProps; you just fetch directly. To statically pre-render a set of slugs, implement generateStaticParams.
// app/posts/[slug]/page.tsx
import { posts } from '@/lib/posts';
import { notFound } from 'next/navigation';
export async function generateStaticParams() {
return posts.map(p => ({ slug: p.slug }));
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = posts.find(p => p.slug === params.slug);
if (!post) return {};
return { title: post.title, description: post.content.slice(0, 120) };
}
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = posts.find(p => p.slug === params.slug);
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<time>{post.date}</time>
<p>{post.content}</p>
</article>
);
}Advantages now:
fetch(url, { cache: 'force-cache' }) vs no-store).Incremental Scaling (ISR & Large Catalogs)
If you have thousands of posts updated sporadically:
revalidate field in getStaticProps.export const revalidate = 3600; or route segment config.You keep static speed while refreshing stale pages after the revalidation window.
Common Pitfalls I Hit
| Pitfall | Symptom | Fix |
|---|---|---|
| Forgetting fallback strategy | 404 on new content | Use fallback: 'blocking' or ISR |
| Data mismatch between builds | Old content persists | Trigger rebuild / adjust revalidate |
| Overusing SSR | Slower TTFB | Prefer SSG + hydrate dynamic islands |
| Client-only state for read-only data | Unneeded JS | Fetch on server (RSC) |
| Duplicate layout logic | Inconsistent UX | Use shared layout / MDX components |
Mental Model Shift
Instead of "pages = components," think: filesystem declares shapes; code resolves specifics. The dynamic segment ([slug], [id], [category]) is just a variable binding provided early enough for static work if you can enumerate it.
Checklist Before Choosing a Strategy
Example: Hybrid Content + Dynamic Widget
Serve blog body statically, hydrate only a small related-posts recommender.
# app/posts/[slug]/RelatedClient.tsx
'use client';
import { useEffect, useState } from 'react';
export function RelatedClient({ slug }: { slug: string }) {
const [items, setItems] = useState<string[]>([]);
useEffect(() => {
fetch('/api/related?slug=' + slug)
.then(r => r.json())
.then(d => setItems(d.items));
}, [slug]);
return <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>;
}Key Takeaways
Closing Thoughts
Learning this wasn’t about reciting docs, it was about solving my scaling problem. Once I internalized the flow (enumerate → pre-render → hydrate only what’s needed), dynamic routing became a creative tool instead of a hurdle.
If you’re just starting:
Then repeat with real data. Momentum compounds.
---
Files define structure. Functions supply data. Static wins until it can’t.
