Skip to main content
ASoc
Tutorial

What Does Next.js Do? Five Jobs, Counted From a Real Build

Next.js routes, renders, bundles, runs server code and emits metadata. Each job checked against this storefront's build output: 425 HTML files, 8 dynamic routes.

The ASoc Team8 min read

Next.js does five jobs for a React app: it routes by folder, renders each page once at build time or per request, bundles with Turbopack, runs server code next to your components (Server Components, Server Actions, route handlers), and generates the metadata crawlers read. React supplies the components; Next.js supplies everything around them.

That sentence is the textbook answer, and it is easy to nod at. This post checks it against a real build instead. The ASoc storefront is a Next.js 16.2.9 app on React 19.2.4, and npm run build prints exactly what the framework did for it, so every figure below comes from that output or from a grep of src/.

The five jobs, and what each one did here

JobWhat Next.js doesThe evidence in this repo
RoutingTurns folders under src/app/ into URLs25 top-level route folders; /templates/[slug] and /blog/[slug] expand to hundreds of pages
RenderingPrerenders to static HTML, or renders on demand425 .html files in .next/server/app; 8 routes marked ƒ (dynamic)
BundlingCompiles and splits JS and CSSTurbopack; 30 chunks, 1.7 MB in .next/static
Server codeRuns functions on the server beside components8 Server Action files, 4 route handlers, 1 proxy.ts
MetadataEmits titles, canonicals, sitemaps, social cardssitemap.ts, robots.ts, manifest.ts, 2 opengraph-image.tsx, 28 metadata exports

None of those rows needed a plugin. That is the real answer to "what does Next.js do": it is the part of the stack you would otherwise assemble from a router, a bundler, a server, and an SEO checklist.

Job 1 and 2: routing and rendering are one decision

In a folder-based router, the file is the route, and the route's code decides how it renders. The build output shows the result as a legend:

○  (Static)   prerendered as static content
●  (SSG)      prerendered as static HTML (uses generateStaticParams)
ƒ  (Dynamic)  server-rendered on demand

The storefront has 111 product pages and 292 blog posts (the count when this was written, before this post was added). Neither set is hand-listed. One function per route enumerates them, and src/app/blog/[slug]/page.tsx shows the whole pattern:

export function generateStaticParams(): Params[] {
  return getPublishedSlugs().map((slug) => ({ slug }));
}

/** Nothing outside `generateStaticParams` exists — no on-demand rendering. */
export const dynamicParams = false;

dynamicParams = false is the line that matters. A slug that isn't in the list returns a 404 rather than being rendered on demand, so a registry entry added ahead of its article can never ship an empty page. The same pattern is walked in more depth in static rendering in the App Router, and the folder conventions in Next.js routing.

The eight dynamic routes are the interesting half: /api/download, /api/webhooks/lemonsqueezy, /auth/callback, /dashboard, /dashboard/settings, /login, /signup and /reset-password. Most are obviously per-user or per-request. /login is the one that surprises people. Nothing about a sign-in form is personal, but the page does this:

export default async function LoginPage({
  searchParams,
}: {
  searchParams: Promise<{ next?: string; error?: string }>;
}) {
  const { next: nextParam, error } = await searchParams;

Awaiting searchParams opts the route into per-request rendering, because the query string does not exist at build time. That is the rendering model working as designed, not a defect, but it is the cheapest way to turn a static page dynamic by accident. If a page you expected to be ○ shows up as ƒ, search it for searchParams, cookies() and headers() first.

Job 3: bundling is the part you stop thinking about

Turbopack compiled the whole site, and the production client output is 30 chunks and 1.7 MB under .next/static. You never configure a loader. The one place the bundler leaks into your code is a trap worth knowing: MDX plugins in next.config.ts have to be named as strings, because Turbopack runs the pipeline in Rust and silently drops an imported function reference.

const withMDX = createMDX({
  options: {
    remarkPlugins: ["remark-gfm"],
    rehypePlugins: ["rehype-slug"],
  },
});

Import remarkGfm as a function instead and the build still succeeds. The tables in every post that has one simply stop rendering as tables. The full config, which sets exactly one option of its own, is dissected in Next.js config.

Job 4: server code, in four shapes

Next.js lets server code live next to the component that uses it, and this repo uses all four shapes the framework offers:

ShapeWhereWhat it does here
Server ComponentAny component without "use client"Renders to HTML, ships no JS for itself
Server Action8 files in src/lib/actions/Sign-in, checkout, redemption, refund, contact, newsletter
Route handler4 route.ts filesThe LemonSqueezy webhook, the gated download, OAuth callback, RSS feed
Proxysrc/proxy.tsRefreshes the Supabase session on every page request

The proxy is the Next.js 16 name for what used to be middleware.ts, and it is the one piece with a real blast radius. It runs on everything except static assets, so a missing environment variable fails the whole site, not one page:

export async function proxy(request: NextRequest) {
  let response = NextResponse.next({ request });

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    // cookie adapter omitted
  );

  await supabase.auth.getClaims();
  return response;
}

That is why this repo's own notes say not to deploy main until the two Supabase variables are set in the host: with src/proxy.ts in place and the variables absent, every request errors, including the ones for pages that are otherwise fully static. The rename itself is covered in the Next.js 16 proxy guide, and Server Actions in Next.js Server Actions.

The split between server and client is also measurable. Of 92 component files, the "use client" directive appears in 28 files across src/, which include a few hooks and helpers beside the components. Everything else is a Server Component, which is why the marketing pages hydrate so little.

Job 5: metadata is the part SEO depends on

A crawler does not run your components' intent; it reads <head>, /sitemap.xml, /robots.txt and the social card. Next.js turns each of those into a file convention:

FileOutputCount here
sitemap.ts/sitemap.xml1
robots.ts/robots.txt1
manifest.ts/manifest.webmanifest1
opengraph-image.tsxA 1200×630 PNG per route2 (site-wide and per post)
metadata / generateMetadataTitle, description, canonical, Open Graph28 exports

The per-post card is where a real defect lived. A metadata route under a dynamic segment is its own route, so it needs its own generateStaticParams even though the page beside it already declares one. Without it the 292 cards were rendered per request, which the build prints as ƒ, instead of baked once like everything else:

// src/app/blog/[slug]/opengraph-image.tsx
export function generateStaticParams() {
  return getPublishedSlugs().map((slug) => ({ slug }));
}

Nothing warns you. The page is fine, the card is fine, and the only symptom is a legend symbol in the build log and a slower first request for every social share.

What Next.js does not do here

Reading the census backwards is just as useful. A stock Next.js tutorial leans on next/image, next/dynamic and the next/font family. This codebase imports zero of the first two from real code: every product image is a plain <img> pointing at a pre-built WebP variant, so there is no request-time image optimizer to run or pay for. The import counts across src/**/*.ts(x) are:

24 next/link         10 next/navigation     4 next/server
 3 next/cache         2 next/og             1 next/script
 1 next/headers       1 next/font/google

The framework is doing a great deal, and almost all of it is invisible: routing, rendering and bundling cost zero imports because they are conventions, not APIs. The reasoning behind skipping next/image is in image optimization without next/image.

Troubleshooting

SymptomLikely causeFix
A page you expected static shows ƒ in the buildIt awaits searchParams, or calls cookies() / headers()Move the dynamic read into a smaller component, or accept per-request rendering
A dynamic-segment page builds, but its opengraph-image is ƒThe image route has no generateStaticParams of its ownExport the same function from opengraph-image.tsx
Markdown tables render as plain textMDX plugin passed as an imported functionName it as a string in next.config.ts
Every page 500s after a deployproxy.ts needs an env var the host lacksSet the variable, or narrow the matcher
An unknown slug renders a blank pagedynamicParams left at its defaultSet export const dynamicParams = false
middleware.ts flagged as deprecatedRenamed in Next.js 16Run the codemod and rename the export to proxy

FAQ

Is Next.js a frontend or a backend framework? Both, in one project. Components render on the server by default, Server Actions and route handlers run server code, and only files marked "use client" ship interactive JavaScript to the browser.

Does Next.js replace React? No. React still provides components, state and the rendering model. Next.js adds routing, build-time rendering, bundling and the server runtime around it, which is why react and next are both in package.json.

Do I need a server to run Next.js? Only for the routes that need one. Most of this site is prerendered HTML that any CDN can serve; the eight ƒ routes and the proxy need a Node runtime or a platform that provides one.

Is Next.js good for SEO? It removes the common blockers, because the HTML is complete before JavaScript runs and metadata, sitemaps and social cards are file conventions. It does not write the content, and a build-time page that says nothing still ranks for nothing.

Templates built on this setup

The census above is the structure every ASoc landing template starts from: static routes by default, server code only where a form or a session needs it, metadata files already wired. Browse the Next.js landing page templates or the Tailwind landing page templates to start from that baseline rather than rebuilding it.

Keep reading

Tutorial8 min read

What Is Supabase Used For? Four Things, in One Real App

Four things, precisely: auth, row-level authorization, private storage, atomic writes -- including the rate-limit race this app actually hit and fixed.

Read more
Tutorial8 min read

What Is a Web Page Template? Two Products Share the Word

A hosted design you edit in a builder, and a source repo you own, are both called templates. What is actually inside one, from a catalog of 111.

Read more
Tutorial8 min read

Where to Put Components in Next.js: 92 Files, None of Them in app/

The App Router routes special files, not folders — so the placement question is about one import path and one place to look, not about avoiding accidental routes.

Read more