Deploying a static Next.js site on IIS

Static export, trailing slashes, MIME types, the soft-404 trap, and the HTTPS redirect — everything IIS needs that the Next.js docs don't mention.

Next.js and IIS are not a combination anyone writes about. The official docs assume Vercel or Node; the IIS docs assume ASP.NET. I recently put a Next.js portfolio on IIS and hit a handful of problems that cost real time. Here they all are.

Start with a static export

The first instinct is to copy the .next folder onto the server. That does not work. .next is an intermediate build for a Node process (next start) — it is not a browsable site. Serving it from IIS gets you nothing.

If every route is static, skip Node entirely and export plain files:

// next.config.ts
const nextConfig: NextConfig = {
  output: "export",              // emits ./out as plain HTML/CSS/JS
  images: { unoptimized: true }, // no image optimizer in an export
  trailingSlash: true,           // /about/ -> /about/index.html
};

trailingSlash matters more than it looks. Without it the export writes about.html, and IIS's default document rule never resolves /about. With it you get about/index.html, which IIS serves correctly and redirects to with a 301.

Metadata routes need force-static

The build will fail on robots.ts, sitemap.ts and manifest.ts with a message about dynamic not being configured. Each one needs a single line:

export const dynamic = "force-static";

Generated images silently produce nothing

This one is worth checking by hand. If you use icon.tsx or opengraph-image.tsx with ImageResponse, the export creates out/icon/ and out/opengraph-image/ as empty directories. The build reports success. The HTML still links to those paths, so your favicon and social card 404 in production.

The fix is to stop generating them. A static app/icon.svg and a real image file for the social card are simpler anyway — a favicon does not need to be computed at build time.

IIS 404s on file types it does not know

IIS refuses to serve extensions with no registered MIME type, so .webmanifest returns a 404 out of the box. Drop a web.config into public/ — Next copies everything there straight into the export root:

<staticContent>
  <remove fileExtension=".webmanifest" />
  <mimeMap fileExtension=".webmanifest"
           mimeType="application/manifest+json" />
</staticContent>

The soft-404 trap

This is the one that actually hurts, because nothing looks broken. The obvious way to wire up a custom 404 page is:

<error statusCode="404" path="/404.html"
       responseMode="ExecuteURL" />

Visit a URL that does not exist and you see your 404 page. Looks perfect. But check the status code:

curl -o /dev/null -w '%{http_code}' https://example.com/does-not-exist
200

ExecuteURL re-serves the page through the static file handler, which stamps 200 OK on it. Humans see a 404; Google sees a valid page. That is a soft 404 — Search Console flags it, and crawl budget goes to URLs that do not exist.

For a static error page the correct mode is File, which returns the same content with the real status. Note that path becomes a physical path, so the leading slash goes away:

<error statusCode="404" path="404.html"
       responseMode="File" />

HTTPS and www redirects

These need the URL Rewrite module. Without it installed, a <rewrite> block makes IIS return 500 for every request — so confirm it first:

Test-Path "$env:windir\System32\inetsrv\rewrite.dll"

One more thing that is easy to miss: the rules live in your site's web.config, so they only run if the request reaches your site. If your site has no port 80 binding, HTTP requests land on the Default Web Site and your redirect never fires. Add bindings for both hostnames on port 80 before blaming the rules.

Verify from outside

Every problem above passed a local build. Check the deployed site directly:

curl -o /dev/null -w '%{http_code} %{redirect_url}\n' http://example.com/
curl -o /dev/null -w '%{http_code}\n' https://example.com/does-not-exist
curl -s https://example.com/ | grep -o '<link rel="canonical"[^>]*>'

A stale deploy with the wrong canonical tag will quietly tell Google to ignore your entire site, and nothing on the page will look wrong. Check the served HTML, not the source.