Kirito
The Next.js Static Export That Generated 50,000 404 Pages
Setting output: "export" in Next.js seemed simple until Google indexed 50,000 404 pages because of a trailing slash mismatch.
The Next.js Static Export That Generated 50,000 404 Pages
I love Next.js static exports. No servers. No cold starts. Just HTML files on S3.
But one configuration option turned 50,000 valid pages into 404 errors.
The Setup
We had a marketing site with 50,000 blog posts (static content).
Next.js 14 with App Router.
// next.config.js module.exports = { output: 'export', // Static export trailingSlash: false, // Our setting images: { unoptimized: true } };
Our links looked like:
<Link href="/blog/post-123">Read post</Link> // Renders <a href="/blog/post-123">
During build, Next.js generated:
out/
blog/
post-123.html
post-124.html
...
We uploaded to S3 and configured CloudFront.
The Problem
After launch, Google Search Console showed:
Indexed pages: 50,000
404 pages: 50,000
Every single blog post was marked as a 404.
I visited a post: https://mysite.com/blog/post-123
It worked in the browser.
Why was Google seeing 404?
The Investigation
I fetched the page with curl:
curl -I https://mysite.com/blog/post-123 HTTP/2 200 OK # Works curl -I https://mysite.com/blog/post-123/ HTTP/2 404 Not Found # FAILS
The trailing slash was the culprit.
Googlebot was crawling both versions:
- Without trailing slash → 200 (S3 serves post-123.html)
- With trailing slash → 404 (S3 looks for folder post-123/index.html)
Our S3 bucket was configured to handle index.html for folders, but not to remove trailing slashes.
Google was indexing the trailing-slash URLs because they appeared in our sitemap:
<url> <loc>https://mysite.com/blog/post-123/</loc> <!-- Trailing slash! --> </url>
Our sitemap generator added trailing slashes automatically.
The Fixes
1. Remove trailing slashes from sitemap
// app/sitemap.ts export default function sitemap() { const posts = getAllPosts(); return posts.map(post => ({ url: `https://mysite.com/blog/${post.slug}`, // NO trailing slash lastModified: post.date })); }
2. Configure S3 to remove trailing slashes
Using CloudFront Functions:
// CloudFront viewer request function function handler(event) { var request = event.request; var uri = request.uri; // Remove trailing slash if (uri.endsWith('/') && uri !== '/') { uri = uri.slice(0, -1); request.uri = uri; } // Add .html for extensionless paths if (!uri.includes('.') && !uri.endsWith('/')) { request.uri = uri + '.html'; } return request; }
3. Use redirect rules in S3
<RoutingRules> <RoutingRule> <Condition> <KeyPrefixEquals>blog/</KeyPrefixEquals> <SuffixEquals>/</SuffixEquals> </Condition> <Redirect> <ReplaceKeyPrefixWith>blog/</ReplaceKeyPrefixWith> <RemoveSuffix>/</RemoveSuffix> </Redirect> </RoutingRule> </RoutingRules>
4. Configure Next.js to generate both versions
// next.config.js with custom export module.exports = { output: 'export', trailingSlash: true, // Change to true // Then generate both .html and /index.html };
But this doubles the build time and storage.
The Prevention
Canonical URLs without trailing slashes
// Add canonical tag to every page <Head> <link rel="canonical" href={`https://mysite.com/blog/${post.slug}`} /> </Head>
Redirect trailing slashes in middleware
// middleware.ts (works with static export using edge config) import { NextResponse } from 'next/server'; export function middleware(request) { const url = request.nextUrl.clone(); if (url.pathname.endsWith('/') && url.pathname !== '/') { url.pathname = url.pathname.slice(0, -1); return NextResponse.redirect(url, 301); } }
Test with bot user-agent
# Simulate Googlebot curl -A "Googlebot" -I https://mysite.com/blog/post-123/ # Should return 301 or 200, never 404
Commands to Audit
Find all trailing slash URLs in sitemap
grep -o '<loc>[^<]*/' sitemap.xml | wc -l
Check S3 behavior
aws s3 ls s3://my-bucket/blog/ --recursive | grep '.html$'
Bulk test redirects
# Using httpie cat urls.txt | xargs -I {} http -h {} | grep -E "HTTP|Location"
What I Learned
- Static exports are unforgiving about trailing slashes.
- Sitemaps must match the actual URL structure.
- CloudFront Functions can fix S3 limitations.
- Always test both slash variants before launch.
That mistake cost us 2 weeks of SEO recovery. Now we have a CI check that ensures all generated URLs are consistent and no trailing slash variants return 404.