diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts
index 2e7bebaaf90d..aaa3821d304a 100644
--- a/packages/next/build/webpack-config.ts
+++ b/packages/next/build/webpack-config.ts
@@ -299,6 +299,9 @@ export function getDefineEnv({
'process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE': JSON.stringify(
config.experimental.skipMiddlewareUrlNormalize
),
+ 'process.env.__NEXT_MANUAL_TRAILING_SLASH': JSON.stringify(
+ config.experimental?.skipTrailingSlashRedirect
+ ),
'process.env.__NEXT_HAS_WEB_VITALS_ATTRIBUTION': JSON.stringify(
config.experimental.webVitalsAttribution &&
config.experimental.webVitalsAttribution.length > 0
diff --git a/packages/next/client/normalize-trailing-slash.ts b/packages/next/client/normalize-trailing-slash.ts
index d2405675a1de..11547936af6b 100644
--- a/packages/next/client/normalize-trailing-slash.ts
+++ b/packages/next/client/normalize-trailing-slash.ts
@@ -6,7 +6,7 @@ import { parsePath } from '../shared/lib/router/utils/parse-path'
* in `next.config.js`.
*/
export const normalizePathTrailingSlash = (path: string) => {
- if (!path.startsWith('/')) {
+ if (!path.startsWith('/') || process.env.__NEXT_MANUAL_TRAILING_SLASH) {
return path
}
diff --git a/test/e2e/skip-trailing-slash-redirect/app/pages/index.js b/test/e2e/skip-trailing-slash-redirect/app/pages/index.js
index 04fbd07fee5b..8cf5943c3cd9 100644
--- a/test/e2e/skip-trailing-slash-redirect/app/pages/index.js
+++ b/test/e2e/skip-trailing-slash-redirect/app/pages/index.js
@@ -8,6 +8,10 @@ export default function Page(props) {
to another
+
+ to another
+
+
to /blog/first
diff --git a/test/e2e/skip-trailing-slash-redirect/index.test.ts b/test/e2e/skip-trailing-slash-redirect/index.test.ts
index e31d7a6909fc..cb3e14c15f54 100644
--- a/test/e2e/skip-trailing-slash-redirect/index.test.ts
+++ b/test/e2e/skip-trailing-slash-redirect/index.test.ts
@@ -174,6 +174,54 @@ describe('skip-trailing-slash-redirect', () => {
expect(await res.text()).toContain('another page')
})
+ it('should not apply trailing slash to links on client', async () => {
+ const browser = await webdriver(next.url, '/')
+ await browser.eval('window.beforeNav = 1')
+
+ expect(
+ new URL(
+ await browser.elementByCss('#to-another').getAttribute('href'),
+ 'http://n'
+ ).pathname
+ ).toBe('/another')
+
+ await browser.elementByCss('#to-another').click()
+ await browser.waitForElementByCss('#another')
+
+ expect(await browser.eval('window.location.pathname')).toBe('/another')
+
+ await browser.back().waitForElementByCss('#to-another')
+
+ expect(
+ new URL(
+ await browser
+ .elementByCss('#to-another-with-slash')
+ .getAttribute('href'),
+ 'http://n'
+ ).pathname
+ ).toBe('/another/')
+
+ await browser.elementByCss('#to-another-with-slash').click()
+ await browser.waitForElementByCss('#another')
+
+ expect(await browser.eval('window.location.pathname')).toBe('/another/')
+
+ await browser.back().waitForElementByCss('#to-another')
+ expect(await browser.eval('window.beforeNav')).toBe(1)
+ })
+
+ it('should not apply trailing slash on load on client', async () => {
+ let browser = await webdriver(next.url, '/another')
+ await check(() => browser.eval('next.router.isReady ? "yes": "no"'), 'yes')
+
+ expect(await browser.eval('location.pathname')).toBe('/another')
+
+ browser = await webdriver(next.url, '/another/')
+ await check(() => browser.eval('next.router.isReady ? "yes": "no"'), 'yes')
+
+ expect(await browser.eval('location.pathname')).toBe('/another/')
+ })
+
it('should respond to index correctly', async () => {
const res = await fetchViaHTTP(next.url, '/', undefined, {
redirect: 'manual',