Next.js: Nextjs should have more options for trailing slash.

Created on 14 Jul 2020  路  14Comments  路  Source: vercel/next.js

Feature request

Is your feature request related to a problem? Please describe.

Nuxtjs have several options to choose trailing slash. (undefined, true, false)
nuxt-community/nuxt-i18n#422

Describe the solution you'd like

Nextjs should have more options for trailing slash.
If use undefined Nextjs should not redirect route when use URL that ends with a slash

trailingSlashes: true

/abc/ -> /abc/ (keeps the same)
/abc -> /abc/ (changes)

trailingSlashes: false

/abc -> /abc (keeps the same)
/abc/ -> /abc (changes)

trailingSlashes: undefined (default)

/abc/ -> /abc/ (keeps the same)
/abc -> /abc (keeps the same)

Most helpful comment

I think not being forced to opt-in into automatic trailingSlash url normalisation would be very helpful. My team wanted to upgrade to latest version of Next.js recently but this change keeps us from doing so.

Our use-case is international website that does not use trailing slashes at the end of the url except for home pages for different countries:

  • /gb-en/ <- home page /
  • /gb-en/page-1 <- page-1 /page-1

We would benefit from trailingSlashes: undefined (default)

All 14 comments

@armspkt Can you expand a bit more on _why_ you need this behavior (other than "just because nuxt does it this way"). Is there a specific use-case you're trying to solve? What's the practical use of having different urls point to the same content? Is there a benefit to having that behavior?

@Janpot I need this feature because many users on my website access via both (with a slash and without a slash) and I think it good to have those options that we don't have to redirect for the better user experience and I saw many sites work that way too.

There is no specific use-case for Nextjs yet but There is a case about i18n localePath for Nuxtjs. https://github.com/nuxt/nuxt.js/pull/6331

I think it better to have more options to choose from.

Is there a specific use-case you're trying to solve?

  • there's not yet.

What's the practical use of having different urls point to the same content?

  • I saw many websites did not redirect from slash to not have slash.
    eg. https://nextjs.org/showcase and https://nextjs.org/showcase/
    Why nextjs-site have the same content but not redirect?

https://www.youtube.com/feed/trending and https://www.youtube.com/feed/trending/
https://github.com/pulls and https://github.com/pulls/
https://twitter.com/explore and https://twitter.com/explore/
https://www.facebook.com/groups/feed and https://www.facebook.com/groups/feed/

Can you explain why those websites did not redirect?

Is there a benefit to having that behavior?

  • In my opinion, it better for user experience and I saw some developers want it works this way too.

I don't want it because Nuxtjs have it but I want it because it is nice to have feature.
That's all.

Yeah, i think we need that, idk if this solutions is the best, but, need a solution.
How nextjs do this https://nextjs.org/showcase and https://nextjs.org/showcase/?

Yeah, i think we need that, idk if this solutions is the best, but, need a solution.

How nextjs do this https://nextjs.org/showcase and https://nextjs.org/showcase/?

Check their website's next.config.js file. I think that you will find your answer there.

Yeah, i think we need that, idk if this solutions is the best, but, need a solution.
How nextjs do this https://nextjs.org/showcase and https://nextjs.org/showcase/?

Check their website's next.config.js file. I think that you will find your answer there.

Now in "next": "^9.4.5-canary.32" have a solution by default :)

Something wrong going on when using basePath and trailingSlash: true:
Next/Link removes trailing slash when going to "/"
And in some old browsers got error (which was not in 9.4.5-canary.27)

Unhandled promise rejection,TypeError: t.entries is not a function
/_next/static/runtime/polyfills-35e39b6a0bf08dcb376c.js

Using 9.4.5-canary.34

@Unsfer Do you know, by any chance, which browser throws this error?

It was some old version of FF, which doesn't sopport Object.entries. I think it's supposed to be polyfilled

A richer system would be very good to be able to decide in which paths redirects should be avoided at all costs. For example, in our case, as I commented here:

https://github.com/vercel/next.js/issues/15867#issuecomment-683748417

We want to avoid 308 before 404 pages.

We want this:

old_behavior

And the current feature forces to:

trailingslash

@aralroca feel free to reach out to [email protected] and we'll see what we can do for your specific case under enterprise support.

By default Next.js will redirect urls with trailing slashes to their counterpart without a trailing slash.

Can I disable it? Or can it be used only as a designated route?
I think this is a good feature, but my boss requires the URL to remain the same. 馃槬
If I use patch-package to remove the default redirection and removePathTrailingSlash, will there be any side effects?

diff --git a/node_modules/next/dist/client/normalize-trailing-slash.js b/node_modules/next/dist/client/normalize-trailing-slash.js
index 9ceda38..71b277b 100644
--- a/node_modules/next/dist/client/normalize-trailing-slash.js
+++ b/node_modules/next/dist/client/normalize-trailing-slash.js
@@ -1,6 +1,6 @@
 "use strict";exports.__esModule=true;exports.removePathTrailingSlash=removePathTrailingSlash;exports.normalizePathTrailingSlash=void 0;/**
  * Removes the trailing slash of a path if there is one. Preserves the root path `/`.
- */function removePathTrailingSlash(path){return path.endsWith('/')&&path!=='/'?path.slice(0,-1):path;}/**
+ */function removePathTrailingSlash(path){return path;}/**
  * Normalizes the trailing slash of a path according to the `trailingSlash` option
  * in `next.config.js`.
  */const normalizePathTrailingSlash=process.env.__NEXT_TRAILING_SLASH?path=>{if(/\.[^/]+\/?$/.test(path)){return removePathTrailingSlash(path);}else if(path.endsWith('/')){return path;}else{return path+'/';}}:removePathTrailingSlash;exports.normalizePathTrailingSlash=normalizePathTrailingSlash;
diff --git a/node_modules/next/dist/lib/load-custom-routes.js b/node_modules/next/dist/lib/load-custom-routes.js
index c56f7e4..f67024f 100644
--- a/node_modules/next/dist/lib/load-custom-routes.js
+++ b/node_modules/next/dist/lib/load-custom-routes.js
@@ -5,5 +5,5 @@ const errMatches=err.message.match(/at (\d{0,})/);if(errMatches){const position=
 // for not being a string
 const{tokens,error}=tryParsePath(route.source);if(error){invalidParts.push('`source` parse failed');}sourceTokens=tokens;}// make sure no unnamed patterns are attempted to be used in the
 // destination as this can cause confusion and is not allowed
-if(typeof route.destination==='string'){if(route.destination.startsWith('/')&&Array.isArray(sourceTokens)){const unnamedInDest=new Set();for(const token of sourceTokens){if(typeof token==='object'&&typeof token.name==='number'){const unnamedIndex=new RegExp(`:${token.name}(?!\\d)`);if(route.destination.match(unnamedIndex)){unnamedInDest.add(`:${token.name}`);}}}if(unnamedInDest.size>0){invalidParts.push(`\`destination\` has unnamed params ${[...unnamedInDest].join(', ')}`);}else{const{tokens:destTokens,error:destinationParseFailed}=tryParsePath(route.destination,true);if(destinationParseFailed){invalidParts.push('`destination` parse failed');}else{const sourceSegments=new Set(sourceTokens.map(item=>typeof item==='object'&&item.name).filter(Boolean));const invalidDestSegments=new Set();for(const token of destTokens){if(typeof token==='object'&&!sourceSegments.has(token.name)){invalidDestSegments.add(token.name);}}if(invalidDestSegments.size){invalidParts.push(`\`destination\` has segments not in \`source\` (${[...invalidDestSegments].join(', ')})`);}}}}}const hasInvalidKeys=invalidKeys.length>0;const hasInvalidParts=invalidParts.length>0;if(hasInvalidKeys||hasInvalidParts){console.error(`${invalidParts.join(', ')}${invalidKeys.length?(hasInvalidParts?',':'')+` invalid field${invalidKeys.length===1?'':'s'}: `+invalidKeys.join(','):''} for route ${JSON.stringify(route)}`);numInvalidRoutes++;}}if(numInvalidRoutes>0){if(hadInvalidStatus){console.error(`\nValid redirect statusCode values are ${[...allowedStatusCodes].join(', ')}`);}console.error();throw new Error(`Invalid ${type}${numInvalidRoutes===1?'':'s'} found`);}}async function loadRedirects(config){if(typeof config.redirects!=='function'){return[];}const _redirects=await config.redirects();checkCustomRoutes(_redirects,'redirect');return _redirects;}async function loadRewrites(config){if(typeof config.rewrites!=='function'){return[];}const _rewrites=await config.rewrites();checkCustomRoutes(_rewrites,'rewrite');return _rewrites;}async function loadHeaders(config){if(typeof config.headers!=='function'){return[];}const _headers=await config.headers();checkCustomRoutes(_headers,'header');return _headers;}async function loadCustomRoutes(config){const[headers,rewrites,redirects]=await Promise.all([loadHeaders(config),loadRewrites(config),loadRedirects(config)]);if(config.trailingSlash){redirects.unshift({source:'/:file((?:[^/]+/)*[^/]+\\.\\w+)/',destination:'/:file',permanent:true},{source:'/:notfile((?:[^/]+/)*[^/\\.]+)',destination:'/:notfile/',permanent:true});if(config.basePath){redirects.unshift({source:config.basePath,destination:config.basePath+'/',permanent:true,basePath:false});}}else{redirects.unshift({source:'/:path+/',destination:'/:path+',permanent:true});if(config.basePath){redirects.unshift({source:config.basePath+'/',destination:config.basePath,permanent:true,basePath:false});}}return{headers,rewrites,redirects};}
+if(typeof route.destination==='string'){if(route.destination.startsWith('/')&&Array.isArray(sourceTokens)){const unnamedInDest=new Set();for(const token of sourceTokens){if(typeof token==='object'&&typeof token.name==='number'){const unnamedIndex=new RegExp(`:${token.name}(?!\\d)`);if(route.destination.match(unnamedIndex)){unnamedInDest.add(`:${token.name}`);}}}if(unnamedInDest.size>0){invalidParts.push(`\`destination\` has unnamed params ${[...unnamedInDest].join(', ')}`);}else{const{tokens:destTokens,error:destinationParseFailed}=tryParsePath(route.destination,true);if(destinationParseFailed){invalidParts.push('`destination` parse failed');}else{const sourceSegments=new Set(sourceTokens.map(item=>typeof item==='object'&&item.name).filter(Boolean));const invalidDestSegments=new Set();for(const token of destTokens){if(typeof token==='object'&&!sourceSegments.has(token.name)){invalidDestSegments.add(token.name);}}if(invalidDestSegments.size){invalidParts.push(`\`destination\` has segments not in \`source\` (${[...invalidDestSegments].join(', ')})`);}}}}}const hasInvalidKeys=invalidKeys.length>0;const hasInvalidParts=invalidParts.length>0;if(hasInvalidKeys||hasInvalidParts){console.error(`${invalidParts.join(', ')}${invalidKeys.length?(hasInvalidParts?',':'')+` invalid field${invalidKeys.length===1?'':'s'}: `+invalidKeys.join(','):''} for route ${JSON.stringify(route)}`);numInvalidRoutes++;}}if(numInvalidRoutes>0){if(hadInvalidStatus){console.error(`\nValid redirect statusCode values are ${[...allowedStatusCodes].join(', ')}`);}console.error();throw new Error(`Invalid ${type}${numInvalidRoutes===1?'':'s'} found`);}}async function loadRedirects(config){if(typeof config.redirects!=='function'){return[];}const _redirects=await config.redirects();checkCustomRoutes(_redirects,'redirect');return _redirects;}async function loadRewrites(config){if(typeof config.rewrites!=='function'){return[];}const _rewrites=await config.rewrites();checkCustomRoutes(_rewrites,'rewrite');return _rewrites;}async function loadHeaders(config){if(typeof config.headers!=='function'){return[];}const _headers=await config.headers();checkCustomRoutes(_headers,'header');return _headers;}async function loadCustomRoutes(config){const[headers,rewrites,redirects]=await Promise.all([loadHeaders(config),loadRewrites(config),loadRedirects(config)]);if(config.trailingSlash){redirects.unshift({source:'/:file((?:[^/]+/)*[^/]+\\.\\w+)/',destination:'/:file',permanent:true},{source:'/:notfile((?:[^/]+/)*[^/\\.]+)',destination:'/:notfile/',permanent:true});if(config.basePath){redirects.unshift({source:config.basePath,destination:config.basePath+'/',permanent:true,basePath:false});}}return{headers,rewrites,redirects};}
 //# sourceMappingURL=load-custom-routes.js.map

I think not being forced to opt-in into automatic trailingSlash url normalisation would be very helpful. My team wanted to upgrade to latest version of Next.js recently but this change keeps us from doing so.

Our use-case is international website that does not use trailing slashes at the end of the url except for home pages for different countries:

  • /gb-en/ <- home page /
  • /gb-en/page-1 <- page-1 /page-1

We would benefit from trailingSlashes: undefined (default)

related issue: #18164

And I agree, this should be optional

Definitely, the option of not doing any redirect should be implemented.

Was this page helpful?
0 / 5 - 0 ratings