How to Fix: Static deployment with nginx web server routes are not loaded when page is refreshed (F5)

7 min read

Encountering 404 errors when refreshing a page or directly accessing a deep link in your Next.js application, even after a successful static deployment with Nginx, is a classic symptom of a misconfiguration where the web server doesn’t understand your application’s client-side routing. This isn’t a bug in Next.js itself, but rather a fundamental mismatch between how traditional web servers serve files and how modern Single Page Applications (SPAs) manage their routes.

Understanding the Root Cause

When Next.js is configured for static export (using output: 'export' in next.config.js), it generates a set of static HTML, CSS, and JavaScript files into an out directory. Crucially, all internal routes are handled by client-side routing, meaning JavaScript on the browser intercepts navigation and updates the DOM without requesting a new full page from the server.

Consider a Next.js app with a route like /about. After a static build, there isn’t necessarily a physical about.html file (unless specifically configured for HTML export per route). Instead, the main entry point, index.html, along with its associated JavaScript bundles, is responsible for rendering the /about content dynamically.

The problem arises when a user directly accesses yourdomain.com/about or refreshes the page while on yourdomain.com/about. In this scenario, the browser makes a direct request to the Nginx web server for the resource at /about. By default, Nginx is a file server. It looks for a file or directory named about within its configured document root. Since no such physical file or directory exists (the content for /about is dynamically rendered by the SPA via index.html), Nginx returns a 404 Not Found error. It doesn’t know to serve index.html and let the client-side router take over.

Step-by-Step Solution

The solution involves configuring Nginx to intelligently serve the main index.html file whenever a requested URI doesn’t correspond to an existing physical file or directory, thereby handing control back to your Next.js application’s client-side router.

Pre-requisites

  • A Next.js project with output: 'export' configured in next.config.js.
  • Your Next.js application built using npm run build, resulting in an out directory containing your static assets.
  • Nginx installed and running on your server.
  • The contents of your Next.js out directory deployed to a specific path on your server (e.g., /var/www/your-app).

Configure Nginx for SPA Routing

You need to modify your Nginx server block configuration. This is typically located in files like /etc/nginx/sites-available/your-site.conf or directly within /etc/nginx/nginx.conf.

Find the server block that defines your website, and specifically the location / block. You will need to add or modify the try_files directive.

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    # Replace with the actual path to your Next.js 'out' directory
    root /var/www/your-app/out;

    index index.html index.htm;

    location / {
        # This is the crucial line for SPA routing
        try_files $uri $uri/ /index.html;
    }

    # Optional: Configure error pages
    error_page 404 /index.html;
    location = /404.html {
        internal;
    }

    # Optional: Cache static assets for better performance
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2|woff|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, no-transform";
        try_files $uri =404;
    }

    # ... other Nginx configurations ...
}

Explanation of the try_files directive:

  • $uri: Nginx first attempts to serve the file that exactly matches the requested URI. For example, if the request is for /styles.css, it tries to find /var/www/your-app/out/styles.css.
  • $uri/: If $uri isn’t found, Nginx then checks if the URI corresponds to a directory. If it does, it tries to serve the default index file within that directory (e.g., /var/www/your-app/out/some-directory/index.html).
  • /index.html: If neither of the above matches (i.e., no physical file or directory exists at the requested URI), Nginx will internally redirect the request to /index.html relative to the root directive. This serves your Next.js application’s main entry point, allowing its client-side router to parse the original URI (e.g., /about) and render the correct content.

After modifying your Nginx configuration, always test the syntax and reload Nginx:

sudo nginx -t
sudo systemctl reload nginx

Verify Your Next.js Static Export

Ensure your next.config.js file has the correct configuration for static output:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export',
  // Optional: If you're deploying to a sub-path, e.g., yourdomain.com/app
  // basePath: '/app',
  // images: { unoptimized: true } // If you're using Next/image with static export
};

module.exports = nextConfig;

Then, build your application:

npm run build

Confirm that an out directory is generated with your static files.

Testing the Solution

1. Deploy the contents of your Next.js out directory to the root path configured in Nginx (e.g., /var/www/your-app/out).

2. Open your application in a browser (e.g., http://yourdomain.com).

3. Navigate to an internal route using client-side links (e.g., click a link to /about).

4. While on the /about page, perform a hard refresh (F5 or Ctrl+R/Cmd+R). The page should load correctly without a 404.

5. Directly type an internal route URL (e.g., http://yourdomain.com/contact) into the browser’s address bar and press Enter. The page should load correctly.

Common Edge Cases

  • Incorrect root directive: Ensure the root directive in your Nginx configuration points precisely to the directory containing your index.html (i.e., the out directory from your Next.js build). A common mistake is pointing to the project root instead of the build output directory.
  • Nginx Permissions: The Nginx user (often www-data or nginx) must have read access to your application’s deployment directory and all its contents. Use sudo chown -R www-data:www-data /var/www/your-app and sudo chmod -R 755 /var/www/your-app if permission issues arise.
  • Browser Caching: Sometimes, the browser might cache the old 404 response. Clear your browser cache or test in an incognito window after applying changes.
  • Conflicting location blocks: If you have multiple location blocks in your Nginx configuration, ensure they don’t override the `location / { try_files … }` directive for your SPA’s routes. Specific location blocks (e.g., for APIs, images, etc.) should come before the general location / block.
  • Next.js basePath: If your Next.js app is deployed under a sub-path (e.g., yourdomain.com/my-app), you must configure basePath: '/my-app' in next.config.js and adjust your Nginx location block accordingly:
    location /my-app/ {
            alias /var/www/your-app/out/;
            try_files $uri $uri/ /my-app/index.html;
        }

    Note the use of alias and the absolute path in try_files for the index.html.

  • Missing index.html: If index.html is somehow missing from your out directory or your Nginx index directive is incorrect, Nginx won’t know which file to serve as the default.

FAQ

Q1: Why does client-side navigation work perfectly, but refreshing or direct URL access fails with a 404?

A1: Client-side navigation is handled entirely by JavaScript within your browser after the initial index.html and app bundles have loaded. The browser intercepts clicks on internal links, updates the URL, and renders the new content without making a full server request. When you refresh or directly access a URL, your browser sends a fresh request to the Nginx server. Without the try_files directive, Nginx looks for a physical file matching that URL. Since your SPA’s routes don’t correspond to physical files, Nginx returns a 404. The try_files directive tells Nginx to fall back to serving index.html in such cases, allowing your SPA’s router to take over.

Q2: Is this solution specific to Next.js?

A2: No, this Nginx configuration pattern (using try_files $uri $uri/ /index.html;) is a standard solution for deploying any Single Page Application (SPA) – such as those built with React, Angular, Vue, or other frameworks like Svelte or Create React App – when they are statically exported and rely on client-side routing. The core problem and solution are identical regardless of the SPA framework, as long as it operates with client-side routing on static assets.

Q3: What if I need server-side rendering (SSR) or API routes in my Next.js application?

A3: This tutorial specifically addresses static deployment (output: 'export') where all content is pre-generated at build time. If your Next.js application uses Server-Side Rendering (SSR), API Routes, or Server Components (requiring a Node.js server to run Next.js), then a simple static Nginx configuration like this will not suffice. You would need to run the Next.js production server (next start) and use Nginx as a reverse proxy to forward requests to your running Node.js server. The Nginx configuration would look different, primarily involving a proxy_pass directive.

Leave a Reply

Your email address will not be published. Required fields are marked *