Next.js Static Export — The Complete Production Guide
Everything you need to know about shipping a fully static Next.js site: output modes, constraints, dynamic routes, image optimization, and deployment.

Why Static Export?
Modern hosting is changing. Platforms like GitHub Pages, Cloudflare Pages, and traditional shared hosting offer fast, affordable static file serving — but they don't run Node.js. If your Next.js site doesn't need a live server for data fetching, going fully static with output: "export" gives you:
- Zero cold starts — HTML is pre-rendered, served instantly.
- Maximum CDN cacheability — every route is a static file.
- Simplified deployment — upload an
out/folder anywhere. - Cost efficiency — no server to pay for or maintain.
This is the architecture I chose for my personal portfolio y0ussef.com, and this guide captures everything I learned.
Setting Up Static Export
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export", // ← This is the key setting
trailingSlash: true, // Generates /about/index.html instead of /about.html
images: {
unoptimized: true, // Required — Image Optimization API needs a server
},
};
export default nextConfig;
⚠️
unoptimized: truedisables Next.js's built-in image resizing and WebP conversion. You'll need to handle this yourself — I use asharp-based prebuild script.
The Core Constraints
Understanding what you cannot do in static export is critical:
| Feature | Static Export | Why |
|---|---|---|
getStaticProps | ✅ Fully supported | Runs at build time |
getStaticPaths | ✅ Fully supported | Pre-generates dynamic routes |
getServerSideProps | ❌ Not supported | Requires live server |
| API Routes | ❌ Not supported | Requires live server |
| Middleware | ❌ Not supported | Edge runtime needed |
| Streaming / RSC | ❌ Not supported | Server-only |
| OG Image Generation | ❌ Not supported | API route needed |
Locale Routing Without the i18n Plugin
Next.js's built-in i18n routing (i18n config key) doesn't work with static export. The workaround I use is path-based locale routing:
pages/
en/
index.tsx → /en/
about.tsx → /en/about/
blog/
index.tsx → /en/blog/
[slug].tsx → /en/blog/[slug]/
ar/
index.tsx → /ar/
about.tsx → /ar/about/
Each locale folder is a separate route namespace. Direction and language are set via useEffect in _app.tsx:
// src/pages/_app.tsx
useEffect(() => {
const isArabic = router.pathname.startsWith("/ar");
document.documentElement.dir = isArabic ? "rtl" : "ltr";
document.documentElement.lang = isArabic ? "ar" : "en";
}, [router.pathname]);
Simple, zero-dependency, works perfectly with SSG.
Dynamic Routes with getStaticPaths
For blog posts, portfolio projects, or any dynamic content:
// src/pages/en/blog/[slug].tsx
import { GetStaticProps, GetStaticPaths } from "next";
export const getStaticPaths: GetStaticPaths = () => {
const slugs = getAllBlogSlugs(); // reads from src/content/blog/
return {
paths: slugs.map((slug) => ({ params: { slug } })),
fallback: false, // 404 for unknown slugs — required for static export
};
};
export const getStaticProps: GetStaticProps = ({ params }) => {
const post = getBlogPostBySlug(params!.slug as string, "en");
if (!post) return { notFound: true };
return { props: { post } };
};
fallback: falseis mandatory for static export.fallback: 'blocking'andfallback: trueboth require a server.
Image Optimization Without the Server
Since images.unoptimized: true disables Next.js optimization, I wrote a prebuild script using sharp:
// scripts/convert-images.js
const sharp = require("sharp");
const fs = require("fs");
const path = require("path");
const IMAGES_DIR = path.join(__dirname, "..", "public", "images");
async function convertImages() {
const files = fs.readdirSync(IMAGES_DIR);
for (const file of files) {
const ext = path.extname(file).toLowerCase();
if (![".png", ".jpg", ".jpeg"].includes(ext)) continue;
const source = path.join(IMAGES_DIR, file);
const target = path.join(IMAGES_DIR, `${path.basename(file, ext)}.webp`);
if (fs.existsSync(target)) continue; // Skip if already converted
await sharp(source).webp({ quality: 85 }).toFile(target);
console.log(`Converted: ${file} → ${path.basename(target)}`);
}
}
convertImages();
Hook it into the build:
{
"scripts": {
"convert-images": "node scripts/convert-images.js",
"build": "npm run convert-images && next build"
}
}
Generating sitemap.xml at Build Time
Without API routes, the sitemap must be a static file generated during prebuild:
// scripts/generate-sitemap.js
const fs = require("fs");
const path = require("path");
const SITE_URL = "https://y0ussef.com";
const staticRoutes = [
"/en/", "/ar/",
"/en/about/", "/ar/about/",
"/en/services/", "/ar/services/",
"/en/projects/", "/ar/projects/",
"/en/case-studies/", "/ar/case-studies/",
"/en/contact/", "/ar/contact/",
];
function getBlogSlugs() {
const blogDir = path.join(__dirname, "..", "src", "content", "blog");
if (!fs.existsSync(blogDir)) return [];
return fs.readdirSync(blogDir);
}
function generateSitemap() {
const blogSlugs = getBlogSlugs();
const blogRoutes = blogSlugs.flatMap((slug) => [
`/en/blog/${slug}/`,
`/ar/blog/${slug}/`,
]);
const allRoutes = [...staticRoutes, ...blogRoutes];
const today = new Date().toISOString().split("T")[0];
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${allRoutes
.map(
(route) => ` <url>
<loc>${SITE_URL}${route}</loc>
<lastmod>${today}</lastmod>
<changefreq>weekly</changefreq>
<priority>${route === "/en/" || route === "/ar/" ? "1.0" : "0.8"}</priority>
</url>`
)
.join("\n")}
</urlset>`;
fs.writeFileSync(path.join(__dirname, "..", "public", "sitemap.xml"), xml);
console.log(`Sitemap generated: ${allRoutes.length} URLs`);
}
generateSitemap();
Build Output Example
A typical successful build looks like:
✓ Generating static pages (21/21)
✓ Exporting (21/21)
Route (pages)
├ ○ /en/
├ ○ /en/about/
├ ○ /en/blog/
├ ○ /en/blog/building-alablam-platform/
├ ○ /en/blog/nextjs-static-export-guide/
├ ○ /ar/
└ ... (and all AR mirrors)
Summary
Static export with Next.js Pages Router is a powerful, underrated approach for portfolio sites, marketing pages, and content-heavy sites that don't need live server features. The constraints are real but manageable — once you understand the boundaries, you can build anything.
The key principles I follow:
- All data at build time —
getStaticPropsfor everything. fallback: falseon all dynamic routes.- Handle image optimization yourself via a sharp prebuild script.
- Generate sitemaps at build time via a Node.js script.
- Path-based locale routing instead of the i18n config.
Happy shipping. 🚀
Share this article
Youssef Mahmoud
Full-Stack Engineer & Project Engineer
Have a project in mind?
Let's discuss your requirements and build something great together.
Book a Strategy Call