🚀 Executive Summary

TL;DR: Next.js App Router’s `next export` command often fails to generate dynamic routes for static deployments, causing 404 errors on sites hosted on pure static platforms like S3. The official and recommended solution involves configuring `output: ‘export’` in `next.config.js` and correctly implementing `generateStaticParams` in dynamic `page.js` files to explicitly define all paths for pre-rendering.

🎯 Key Takeaways

  • The Next.js App Router’s `next export` command does not automatically discover and pre-render dynamic routes defined by `generateStaticParams` without explicit configuration.
  • The correct approach for static export of dynamic routes in the App Router requires setting `output: ‘export’` in `next.config.js` and implementing an `async function generateStaticParams()` that returns an array of route parameters in the dynamic `page.js` file.
  • For applications with highly dynamic content or requirements for SSR/ISR, a strategic pivot to a server-based deployment (e.g., Vercel, Node.js server) might be more suitable than forcing a pure static export.

Is the Next.js team ever going to support dynamic routes in static export for the App Router?

Struggling with Next.js App Router’s static export for dynamic routes? Here are three real-world solutions to get your static site generation (SSG) working, from a quick-and-dirty script to the official, long-term fix.

Static Exports and Dynamic Nightmares: Taming the Next.js App Router

I still remember the night we pushed the “Go Live” button on a major marketing site rewrite. It was 2 AM, fueled by stale coffee and that nervous energy you only get on a deployment night. We’d migrated from the old Pages Router to the shiny new App Router. Everything looked great on Vercel, our staging environment. But our production setup was different—a “pure static” deployment to an S3 bucket fronted by CloudFront for compliance reasons. The pipeline ran green, the files were uploaded, and then the panic set in. The homepage worked, but every single one of the 200+ blog post and product pages returned a 404. It turns out, `next export` with the App Router just… didn’t generate them. It’s a gut-wrenching feeling, and if you’re reading this, you’ve probably felt it too.

So, What’s Actually Breaking? The “Why” Behind the 404s

This isn’t just a bug; it’s a fundamental shift in how Next.js thinks about page generation. In the old Pages Router, we had the explicit `getStaticPaths` function. You told Next.js exactly which dynamic paths to pre-render during the build, and it obeyed. Life was simple.

The App Router introduced `generateStaticParams` to serve a similar purpose. The idea is the same: give Next.js an array of slug/id combinations so it knows what pages to build. However, the `next export` command, which is required for a truly static output, doesn’t seem to play as nicely with this new paradigm as the standard `next build` does. The build process, when configured for static export, simply doesn’t crawl and discover these dynamic paths by default. It generates the “shell” for the dynamic route (e.g., `/blog/[slug]`) but not the actual pages (`/blog/my-first-post`, `/blog/another-great-post`). It’s waiting for you to explicitly tell it what to build, and the new way of doing that is a bit more nuanced.

The Fixes: From Duct Tape to a New Engine

After that painful night, my team and I dug in deep. We came up with a few ways to solve this, ranging from “get us through the night” to “let’s fix this for good.”

Solution 1: The “It’s 3 AM” Manual Script

Sometimes you just need to stop the bleeding. This is the hacky, short-term solution to get your site working right now. The idea is to manually create the static HTML files for your dynamic routes after the build but before the deploy.

The Strategy: Run the dev server, write a simple script to curl every dynamic URL you need, and save the HTML output to the correct path in your `out` directory.

Here’s a conceptual bash script you might run in your CI/CD pipeline:


# ci-pipeline.sh

# 1. Build the static site (it will be missing dynamic pages)
npm run build

# 2. Start the built-in Next.js server in the background
npm start &
SERVER_PID=$!

# Give the server a moment to start
sleep 10

# 3. Fetch your dynamic slugs from an API or database
# (Here, we're just using a local file for the example)
SLUGS=$(cat ./scripts/slugs.txt)

# 4. Loop through slugs and use wget/curl to generate the HTML
for slug in $SLUGS; do
  echo "Generating page for slug: $slug"
  mkdir -p out/products/$slug
  wget -O out/products/$slug/index.html http://localhost:3000/products/$slug
done

# 5. Kill the server and proceed with deployment
kill $SERVER_PID

# 6. Now deploy the 'out' directory to S3/Netlify/etc.
echo "Deployment of 'out' directory can now proceed."

Warning: This is brittle. If your data source for slugs changes, you have to update your script. It’s slow and feels dirty because, well, it is. Use this to restore service, but plan to replace it immediately.

Solution 2: The “Right Way” – Using `generateStaticParams` Correctly

This is the solution the Next.js team intends for you to use. It involves properly implementing `generateStaticParams` in your dynamic route’s `page.js` file and ensuring your project is configured for a full static export.

The Strategy: First, ensure your `next.config.js` is set up for static export. Then, in your dynamic page file (e.g., `app/blog/[slug]/page.js`), export an async function called `generateStaticParams` that returns an array of objects, where each object contains the params for one page.

Step 1: Configure `next.config.js`

You need to tell Next.js you want a truly static output.


/** @type {import('next').NextConfig} */
const nextConfig = {
  // This is the magic flag!
  output: 'export',

  // Optional: Set to false if you're not using the `Image` component with a cloud provider
  images: {
    unoptimized: true,
  },
};

module.exports = nextConfig;

Step 2: Implement `generateStaticParams`

In your dynamic page component file, fetch your data and map it to the format Next.js expects.


// In: app/blog/[slug]/page.js

// This function gets called at build time
export async function generateStaticParams() {
  // Fetch your blog posts from a headless CMS, database, etc.
  // For this example, let's pretend we have a function getPosts()
  const posts = await fetch('https://api.my-cms.com/posts').then((res) => res.json());

  // Map the posts to the format Next.js expects: [{ slug: 'post-1' }, { slug: 'post-2' }]
  return posts.map((post) => ({
    slug: post.slug,
  }));
}

// Your regular page component
export default async function BlogPostPage({ params }) {
  const { slug } = params;
  // Fetch the specific post data again here for the page render
  const post = await getPostBySlug(slug);

  return (
    <div>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </div>
  );
}

// Helper function to fetch a single post (you'd have this somewhere)
async function getPostBySlug(slug) {
  // ... logic to fetch single post
  return { title: 'Example Title', content: 'Example content.' };
}

Now, when you run `npm run build` (you no longer need `next export`, as the `output: ‘export’` config handles it), Next.js will call `generateStaticParams`, see the list of slugs, and pre-render a static `index.html` file for each one inside the `out` directory.

Solution 3: The “Nuclear Option” – Rethink Your Hosting

I have to be honest. Sometimes the problem isn’t the code; it’s the architecture. If you find yourself fighting the framework constantly to produce a static site, it might be a sign that your application’s needs have outgrown the constraints of a pure static-only host.

The Strategy: Abandon `next export` and embrace the full power of Next.js by deploying to a platform that can run a Node.js server. This unlocks features like Server-Side Rendering (SSR) for pages that need fresh data and Incremental Static Regeneration (ISR) to update static pages without a full rebuild.

Pure Static Export (S3, Cloudflare Pages) Server-Based Deploy (Vercel, Node Server on ECS/EC2)
  • Pros: Blazing fast, highly scalable, cheap, simple infrastructure.
  • Cons: A full site rebuild is required for any content update. No server-side logic at runtime. Can struggle with highly dynamic content as we’ve seen.
  • Pros: Full Next.js feature set (SSR, ISR, API Routes). Update content without a full redeploy. More flexible.
  • Cons: More complex infrastructure (even on Vercel, it’s a “serverfull” model). Potentially higher cost. Slower time-to-first-byte on SSR pages compared to static.

My Take: This isn’t a defeat. It’s a strategic pivot. We moved one of our internal dashboards from a static export to a small Node.js container on ECS because they needed real-time data. The dev team was happier, and the product owner got the features they needed. Matching the deployment strategy to the product requirements is a core part of a Lead Architect’s job.

Ultimately, the path you choose depends on your project’s specific needs. For most content-driven sites that were perfect for SSG before, mastering Solution #2 is the key. But don’t be afraid to question your core architectural assumptions if the friction becomes too great. Good luck out there.

Darian Vance - Lead Cloud Architect

Darian Vance

Lead Cloud Architect & DevOps Strategist

With over 12 years in system architecture and automation, Darian specializes in simplifying complex cloud infrastructures. An advocate for open-source solutions, he founded TechResolve to provide engineers with actionable, battle-tested troubleshooting guides and robust software alternatives.


🤖 Frequently Asked Questions

âť“ Why are my dynamic routes not working with Next.js App Router static export?

Dynamic routes in Next.js App Router, when configured for static export, require explicit definition. The `next export` command, now handled by the `output: ‘export’` configuration, needs `generateStaticParams` in your dynamic `page.js` files to specify which paths to pre-render as static HTML files during the build process.

âť“ How does `generateStaticParams` in App Router compare to `getStaticPaths` in Pages Router for static exports?

Both `generateStaticParams` (App Router) and `getStaticPaths` (Pages Router) serve to define dynamic paths for static pre-rendering. However, for a truly static output with the App Router, you must explicitly set `output: ‘export’` in `next.config.js`, which then enables `generateStaticParams` to function as intended for static generation, a step not explicitly required for `getStaticPaths` in the Pages Router.

âť“ What is a common implementation pitfall when trying to statically export dynamic routes with Next.js App Router?

A common pitfall is failing to include `output: ‘export’` in your `next.config.js` file. Without this crucial configuration, Next.js will not produce a truly static output, even if `generateStaticParams` is correctly implemented, leading to missing dynamic pages (404s) in a static deployment.

Leave a Reply

Discover more from TechResolve - SaaS Troubleshooting & Software Alternatives

Subscribe now to keep reading and get access to the full archive.

Continue reading