Issue with Next.js on IIS: Subpage Navigation Fails Without .html
The error i see on the browser: HTTP Error 404.0 – Not Found
The resource you are looking for has been removed, had its name changed, or is temporarily unavailable.
I have a Next.js app running on an IIS server. The app’s base URL (e.g., site.com
) works fine, but navigating to subpages (e.g., site.com/subpage
) fails unless I append .html
(e.g., site.com/subpage.html
). Additionally, navigation works within the app until I refresh the page or enter the URL manually.
Navigating the app without touching the browser url or refresh button works fine, it navigates me to the correct urls and even updates the url to the navigated page. so if i go from site.com
to the about
page, it updates the url to site.com/about
. But the moment i press the refresh button or when i place my cursor inside of the url and press enter, i get the 404 error.
important to know is that i use the app router nextjs provides so i need the app to be getting its routing trough the nextjs server which is running on NodeJs. The no managed code
is also toggled in the app pool on iis.
Environment:
- Next.js: Version 14.1.3
- IIS: Version 23H2
- iisnode: Installed via Azure iisnode
- url rewrite: Installed via iis.net (i didn’t add any configurations as i was told by google searches the webconfig handles this)
Project Tree
├───app
├───about (site.com/about)
├───api
│ └───page.tsx
├───config (these are actual screens that should route to site.com/config/*)
│ ├───activity
│ │ └───page.tsx
│ ├───components
│ │ └───page.tsx
│ ├───issuetype
│ │ └───page.tsx
│ ├───scenario
│ │ └───page.tsx
│ └───subprocess
│ └───page.tsx
├───dashboard (site.com/dashboard)
│ └───page.tsx
├───functions
├───interfaces
└───services
├───.env.local
├───next.config.ts
├───package.json
├───server.js
├───tsconfig.json
├───web.config
The out folder looks like this
C:.
├──config
├──_next
│ ├───nBWbtfKUl7qhX9oaAecjF
│ └───static
│ ├───chunks
│ │ ├───app
│ │ │ ├───about
│ │ │ ├───config
│ │ │ │ ├───activity
│ │ │ │ ├───issuetype
│ │ │ │ ├───scenario
│ │ │ │ └───subprocess
│ │ │ └───dashboard
│ │ └───pages
│ ├───css
│ ├───media
│ └───nBWbtfKUl7qhX9oaAecjF
├──404.html
├──about.html
├──about.txt
├──dashboard.html
├──dashboard.txt
├──index.html
├──index.txt
├──next.html
└──vercel.html
Current web.config
in IDE:
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="NextJs Routes Handler" stopProcessing="true">
<match url=".*" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
<!-- Exclude specific paths from rewriting -->
<add input="{REQUEST_URI}" pattern="^/(api|_next/static|static)/" negate="true" />
</conditions>
<action type="Rewrite" url="/" />
</rule>
</rules>
</rewrite>
<iisnode node_env="test" nodeProcessCommandLine=""C:Program Filesnodejsnode.exe"" interceptor=""%programfiles%iisnodeinterceptor.js"" />
</system.webServer>
<location path="" overrideMode="Deny">
<system.webServer>
<handlers>
<add name="iisnode" path="hello.js" verb="*" modules="iisnode" />
</handlers>
</system.webServer>
</location>
</configuration>
This gets formatted to web.config
in IDE:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<security>
<authorization>
<remove users="*" roles="" verbs="" />
<add accessType="Allow" />
<add accessType="Allow" roles="GG-DEP-IT" />
</authorization>
</security>
</system.webServer>
</configuration>
web.config
file
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="NextJs Routes Handler" stopProcessing="true">
<match url=".*" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
<!-- Exclude specific paths from rewriting -->
<add input="{REQUEST_URI}" pattern="^/(api|_next/static|static)/" negate="true" />
</conditions>
<action type="Rewrite" url="/" />
</rule>
</rules>
</rewrite>
<iisnode node_env="test" nodeProcessCommandLine=""C:Program Filesnodejsnode.exe"" interceptor=""%programfiles%iisnodeinterceptor.js"" />
</system.webServer>
<location path="" overrideMode="Deny">
<system.webServer>
<handlers>
<add name="iisnode" path="server.js" verb="*" modules="iisnode" />
</handlers>
</system.webServer>
</location>
</configuration>
server.js
file
const {createServer} = require('http');
const {parse} = require('url');
const next = require('next');
const dev = process.env.NODE_ENV !== 'production';
const port = process.env.PORT || 3000;
const app = next({dev});
const handle = app.getRequestHandler();
app.prepare().then(() => {
createServer((req, res) => {
// Be sure to pass `true` as the second argument to `url.parse`.
// This tells it to parse the query portion of the URL.
const parsedUrl = parse(req.url, true);
const {pathname, query} = parsedUrl;
handle(req, res, parsedUrl);
}).listen(port, (err) => {
if (err) throw err;
console.log(`> Ready on http://localhost:${port}`);
});
});
next.config.ts
file
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
experimental: {},
reactStrictMode: true,
swcMinify: true,
};
export default nextConfig;
package.json
file
{
"name": "reasoncodes",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"export": "next export",
"lint": "next lint",
"test": "vitest --watch --ui --coverage.enabled=true"
},
"dependencies": {
"@next/env": "^14.2.3",
"@testing-library/react": "^14.2.2",
"@vitejs/plugin-react": "^4.2.1",
"axios": "^1.6.7",
"jsdom": "^24.0.0",
"next": "14.1.3",
"primeflex": "^3.3.1",
"primeicons": "^6.0.1",
"primereact": "^10.6.5",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.51.0",
"vite-tsconfig-paths": "^4.3.2"
},
"devDependencies": {
"@hookform/devtools": "^4.3.1",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@vitest/coverage-v8": "^1.4.0",
"@vitest/ui": "^1.4.0",
"eslint": "^8",
"eslint-config-next": "14.1.3",
"typescript": "^5",
"vitest": "^1.4.0"
}
}
I tried following lots of guides but none seem to help me.
im out of ideas at this point and this is not worthy of shipping out to a customer.