trailing slash in link for legit page works for client side navigation but leads to not found bundle and 404 on hard refresh (ssr)
let me know if title needs further clarification.
all relevant issues has been closed with reasoning that its been fixed in 6-canary (I believe it is not) or by improved serve (which is true only in perhaps production static export).
I'm rewriting my existing blog to next.js and i previously used trailing slashes. Latest serve
can help with it once i build my next.js powered blog. But in order to fix dev env i need either to get rid of trailing slashes and utilize 301 Moved Permanently
in prod; or live with broken trailing slash support in dev.
Here is minimal reproducible case (link to repro repo is below snippet):
// pages/index.js
import Link from "next/link";
export default () => (
<Link href="/about/">
<a>About</a>
</Link>
);
// pages/index.js
export default () => "about";
Minimal reproducible repo https://github.com/iamstarkov/next.js-trailing-slash-bug-demo
git clone https://github.com/iamstarkov/next.js-trailing-slash-bug-demo
cd next.js-trailing-slash-bug-demo
yarn
yarn dev
http://localhost:3000/_next/static/development/pages/about.js
being 200edhttp://localhost:3000/_next/on-demand-entries-ping?page=/about/
being 200edhttp://localhost:3000/about/
being 404edhttp://localhost:3000/about/
Client pings, but there's no entry for page: /about/
/about/
shouldnt be resolved as 404 not found
/about/
should be resolved as 200 ok
Client pings, but there's no entry for page: /about/
/about
and /about/
should work the same wayN/A
Add any other context about the problem here.
If you change this code in https://github.com/zeit/next.js/blob/459c1c13d054b37442126889077b7056269eeb35/server/on-demand-entry-handler.js#L242-L249
or node_modules/next/dist/server/on-demand-entry-handler.js
locally
const { query } = parse(req.url, true)
const page = normalizePage(query.page)
+ console.log('query.page', query.page);
+ console.log('page', page);
+ console.log('Object.keys(entries)', Object.keys(entries));
const entryInfo = entries[page]
// If there's no entry.
// Then it seems like an weird issue.
if (!entryInfo) {
const message = `Client pings, but there's no entry for page: ${page}`
and restart next dev
and open http://localhost:3000/ and click about link then:
/about
query.page /about
page /about
Object.keys(entries) [ '/', '/about' ]
/about/
:
query.page /about/
page /about/
Object.keys(entries) [ '/', '/about' ]
Client pings, but there's no entry for page: /about/
I think the problem (at least part of it) is in inability of onDemandEntryHandler's middleware to find page in entries if page has trailing slash.
I hope my 2 hours of investigation and preparation can help with fixing this issue.
most relevant and notable issues are #1189 and #3876
Looking forward to this being finally resolved! @timneutkens What's the status of trailing slash issues for Next 7?
@NathanielHill I could reproduce it on next@7
I'm using nextjs 7 and trailing slash is producing a 404 for me on both dev and prod:
And affects:
Simply removing the trailing slash fixes the issue.
Trailing slashes are often added by browsers, servers and/or other services where links might be pasted so while I can control internal links, it's hard to control what at what links external users might be arriving on
I'm also seeing this issue in version 7. Not sure if this is relevant but, I'm aliasing one Next.js project to a subfolder of another Now deployment. So our base url is primer.style
and we are aliasing our primer-components.now.sh
Next.js app to primer.style/components
. On production, primer.style/components
's index page works fine, but primer.style/components/
produces a 404.
I had to search around a bit to find this issue. I use static deployments on Netlify so it's not an issue on prod, but on development (Next 7) the compilation just freezes if there was a trailing slash, and it was hard to figure out why. I don't think this (not handling trailing slash on dev environment) is a good DX.
Im also having this issue and it is really annoying, i hope it is fixed soon.
If you want trailing slash, you can just do this. <Link href='/about' as='/about/'><a>about</a></Link>
but if you're using fridays/next-routes this is not possible. So I have a fork where you can add trailingSlash
as prop. Hope this helps
If you want trailing slash, you can just do this.
<Link href='/about' as='/about/'><a>about</a></Link>
but if you're using fridays/next-routes this is not possible. So I have a fork where you can addtrailingSlash
as prop. Hope this helps
@aluminick I'm sorry, I just tried this and it doesnt work for me. I still get to traling-slashed page (latest release), which is not found after refresh (current behavior).
also neither #6664 nor #6752 help with these, because experimental.exportTrailingSlash
doesnt help because it is for next export
only, I believe
there was a promising pull request #6421 by @Janpot which didn't reach any consensus, unfortunately
@iamstarkov What's the status of this issue? Any solutions beside the server.js
hook?
@dryleaf status: it is still opened
A similar issue... redirect when multiple forward slashes are added. Example: https://github.com/zeit/next.js////////////issues/5214
GitHub urls are irrelevant
@iamstarkov Not sure what you mean. But after rereading my original post, it looks like I could have been more clear.
The GitHub url is meant to be a simple demonstration of how urls should (preferably) work when an app is built with Next.js. In other words, if a user adds an extra slash, the url should still work.
Any update for nextjs 9 ?
I'm new to Next but what is the workaround you folks are using for this issue?
@iamstarkov What's the status of this issue?
I'm shocked that this issue not solved in any way for about year!
Do Next.js team need any other reasons to start fixing this?
URL's should work regardless of trailing slash. Check any site on the web.
If this is out of scope of Next.js, give us ability to configure this in Now.
I'm really confused that Zeit team ignores such critical issues for years.
@exentrich This is easily configurable in Zeit Now by simply 301 redirecting all trailing slashes to the same route without slashes:
now.json
:
"routes": [
{
"src": "/(.*)/",
"status": 301,
"headers": { "Location": "/$1" }
},
...
]
However, I also don't understand why this is not handled by Next.js itself and why the team has ignored this issue.
This, along with a public/
(in the works) are the main issues I see CRA converts running in to.
@rauchg
@NathanielHill thanks!
I tried this solution, but query parameters are stripped out. For example /some/?query=1
will redirect to /some
without query. Do you know how to fix it?
Yeah, that sounds like a problem @exentrich
I wouldn't have guessed that behavior as I've been told there's an implicit ^
and $
wrapped around the regex (meaning your example wouldn't match). Maybe there's a way to access the query string on it's own to add it back :man_shrugging: Good luck
Trying to get it working using a custom express server and avinoamr/connect-slashes but seem to be running into the same issue
This is certainly a massive issue especially because /
routes throw Error pages and that hurts SEO (which is one of the primary draws of Next).
The 301 redirects and the custom express servers all seem to be hacks rather than fixes. In my case, I have a full working application built on Next with no custom Express server - everything else works perfectly, but now I'm having to create a new Express server only because of the trailing slash issue. The effort required seems to be disproportionate considering this is a hack. I would love if this could be bumped up in priority! Due to this reason, I'm hearing grumblings in my team about having used Next as opposed to something like vanilla React/Angular and it certainly weakens the case for Next.
PS: I absolutely love working with Next ❤️
This is certainly a massive issue especially because
/
routes throw Error pages and that hurts SEO
It doesn't hurt your SEO. google treats trailing slash as a different page. Having it 404 doesn't impact SEO any more than any other non existing page in your site. Besides, as long as you never link to it with a trailing slash, google won't try to crawl it in the first place. This issue, while still being a valid issue, is way less critical than you all make it to be.
@nik-john @NathanielHill @dkrish @exentrich
You shouldn't have to use an Express server to do a 301 Redirect. Depends on your requirements, but I've been able to meet mine with a custom server.js
.
A 301 redirect is also the best way to go for SEO, as you won't get duplicate content penalties for the slash and non-slash route.
I love ❤️ Next.js, but I vote for this to be handled without this work around.
// server.js
const { createServer } = require('http');
const { parse } = require("url");
const next = require("next");
const dev = process.env.NODE_ENV !== 'production'
const port = parseInt(process.env.PORT, 10) || 3000;
const app = next({ dev, quiet: false });
const handle = app.getRequestHandler();
(async () => {
await app.prepare();
const server = createServer();
server.on('request', async (req, res) => {
const parsedUrl = parse(req.url, true);
const { pathname, query } = parsedUrl;
if (pathname.length > 1 && pathname.slice(-1) === "/") {
console.log('server.js - redirect on "/"...', pathname, query);
const queryString = await Object.keys(query).map(key => key + '=' + query[key]).join('&');
res.writeHead(301, { Location: pathname.slice(0, -1) + (queryString ? '?'+ queryString : '') });
res.end();
}
handle(req, res, parsedUrl);
});
await server.listen(port);
console.log(`🚀 Ready on http://localhost:${port}`);
})();
@Janpot
It doesn't hurt your SEO. google treats trailing slash as a different page. Having it 404 doesn't impact SEO any more than any other non existing page in your site.
I take your point that it doesn't particularly hurt SEO innately. But it does put additional pressure on developers to get the URL definitions right every time, which is subject to human errors. A developer that is new to Next wouldn't necessarily know that the following (perfectly normal looking) URL will lead to a 404 page. <Link href='/people/'>
A mature framework shouldn't be subject to such human errors ideally imo.
Besides, as long as you never link to it with a trailing slash, google won't try to crawl it in the first place.
Again - there exists the issue of people accidentally linking to the _www.mysite.com/people/_ instead of _www.mysite.com/people_ (both of which seem to be exactly the same for users - even most developers).
Both of these scenarios _can_ affect SEO.
Now, not considering the SEO impact, there's also the semantic meaning of the URL - what _does_ _www.mysite.com/people/_ point to? Ideally because it is pointing to a directory, Next should return whatever is in pages > people > index.js
(as opposed to pages > people.js
for _www.mysite.com/people_) but instead it returns nothing, which is a very high level flaw in how the routing works.
Major routing libraries already have some provision for this - like isExact
in the case of React Router
While I understand where you're coming from, I still think this is a glaring issue that needs to be bumped up
This is also completely unavoidable in the case of next export
there exists the issue of people accidentally linking...
There exists the issue of people accidentally linking to any non-existing url, why would/some/path/
be less non-existing than /some/path/dhgfiuwo
?
there's also the semantic meaning of the URL
This is highly subjective, as far as I know there is no spec out there that dictates what is the semantic difference. According to the URL spec, with and without trailing slash are considered different urls. I can think of at least 7 different valid behaviours:
Pair this with the possibility of having either /pages/some-page.js
and /pages/some-page/index.js
(or both).
Should next.js support all those use cases? Should it pick a default behaviour?
I'm not against this, but after trying to implement this before, I just think there's more nuance to it than it initially seems.
There exists the issue of people accidentally linking to any non-existing url, why would/some/path/ be less non-existing than /some/path/dhgfiuwo?
For case /some/path/dhgfiuwo
- people expect tha dhgfiuwo
route can to be missing. (For example, user dhgfiuwo
cannot be found in system and way users/dhgfiuwo
is wrong. The absence of a user in the system is an expected occurrence.)
For case /some/path/
- people expect this path to be same as /some/path
, because this is default behavior on other sites.
Therefore, a failure in would/some/path/
is less non-existing than /some/path/dhgfiuwo
.
I see others have posted their solutions, so I wanted to share my approach : https://github.com/DevSpeak/next-trailingslash
Some improvements and support for dynamic routed pages when it comes to ?= should be done IMO, but this is only for showing the idea.
For a quick solution, you can replace the default _error
page (as in @DevSpeak's example).
@DevSpeak, I'd recommend a few changes for your repo:
if (typeof window === 'undefined') { ... }
to tree-shake it from the client bundleHere's what I'm using in a Typescript project (based on the built-in error page):
/pages/_error.tsx
(or remove the TypeScript types and name it /pages/_error.jsx
):
import React from 'react';
import Head from 'next/head';
import { NextPageContext } from 'next';
const statusCodes: { [code: number]: string } = {
400: 'Bad Request',
404: 'This page could not be found',
405: 'Method Not Allowed',
500: 'Internal Server Error'
};
export type ErrorProps = {
statusCode: number;
title?: string;
};
/**
* `Error` component used for handling errors.
*/
export default class Error<P = {}> extends React.Component<P & ErrorProps> {
static displayName = 'ErrorPage';
static getInitialProps({
req,
res,
err
}: NextPageContext): Promise<ErrorProps> | ErrorProps {
const statusCode =
res && res.statusCode ? res.statusCode : err ? err.statusCode! : 404;
if (typeof window === 'undefined') {
/**
* Workaround for: https://github.com/zeit/next.js/issues/8913#issuecomment-537632531
* Test vectors:
* `/test/test/` -> `/test/test`
* `/test/////test////` -> `/test/test`
* `/test//test//?a=1&b=2` -> `/test?a=1&b=2`
* `/test///#test` -> `/test#test`
*/
const correctPath = (invalidPath: string) =>
invalidPath
.replace(/\/+$/, '')
.replace(/\/+#/, '#')
.replace(/\/+\?/, '?')
.replace(/\/+/g, '/');
if (req && res && req.url && correctPath(req.url) !== req.url) {
res.writeHead(302, {
Location: correctPath(req.url)
});
res.end();
}
const reqInfo = req
? `; Url: ${req.url}; IP: ${req.headers['x-forwarded-for'] ||
(req.connection && req.connection.remoteAddress)};`
: '';
console.log(`Error rendered: ${statusCode}${reqInfo}`);
}
return { statusCode };
}
render() {
const { statusCode } = this.props;
const title =
this.props.title ||
statusCodes[statusCode] ||
'An unexpected error has occurred';
return (
<div style={styles.error}>
<Head>
<title>
{statusCode}: {title}
</title>
</Head>
<div>
<style dangerouslySetInnerHTML={{ __html: 'body { margin: 0 }' }} />
{statusCode ? <h1 style={styles.h1}>{statusCode}</h1> : null}
<div style={styles.desc}>
<h2 style={styles.h2}>{title}.</h2>
</div>
</div>
</div>
);
}
}
const styles: { [k: string]: React.CSSProperties } = {
error: {
color: '#000',
background: '#fff',
fontFamily:
'-apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", "Fira Sans", Avenir, "Helvetica Neue", "Lucida Grande", sans-serif',
height: '100vh',
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
},
desc: {
display: 'inline-block',
textAlign: 'left',
lineHeight: '49px',
height: '49px',
verticalAlign: 'middle'
},
h1: {
display: 'inline-block',
borderRight: '1px solid rgba(0, 0, 0,.3)',
margin: 0,
marginRight: '20px',
padding: '10px 23px 10px 0',
fontSize: '24px',
fontWeight: 500,
verticalAlign: 'top'
},
h2: {
fontSize: '14px',
fontWeight: 'normal',
lineHeight: 'inherit',
margin: 0,
padding: 0
}
};
Note, this also logs an error when the page is hit, so you can check your logs to fix any links/other issues.
@DevSpeak @bitjson Thanks for your suggestions. That's certainly one way to go about this and certainly solves the issue very well. But considering that the _error.jsx
is originally meant to handle _errors_ and not house routing logic, in my opinion having all of this code there is hacky and quite declarative. Expecting every user to do this in every code base shouldn't be a requirement - this should come out of the box. = I am of the opinion that this condition needs to be built in with the routing logic, with an option to opt out like React Router.
@NathanielHill
This is also completely unavoidable in the case of next export
Wait - I understood from reading the documentation that there is specific code to handle the trailing slash condition:
The pages will be exported as html files, i.e. /about will become /about.html.
It is possible to configure Next.js to export pages as index.html files and require trailing slashes, i.e. /about becomes /about/index.html and is routable via /about/. This was the default behavior prior to Next.js 9. You can use the following next.config.js to switch back to this behavior:
// next.config.js
module.exports = {
exportTrailingSlash: true,
}
Even if this isn't really an option for static HTML export via next export
, I don't agree with the logic that just because Next supports this (amazing) feature, other modes need to suffer (I don't know the usage stats but I would assume more people use the regular with-server mode as opposed to serverless) especially when this has been known to be such a common use case
FYI: There's an RFC that might interest you https://github.com/zeit/next.js/issues/9081
// next.config.js
module.exports = {
async redirects() {
return [
{
source: "/:path*/",
destination: "/:path",
statusCode: 301
}
];
}
};
@Janpot Love it - this will bring us half-way i.e. have some sort of support for redirects without having to create a custom server. This is still going to be imperative because for every route the user adds, they would have to set up a redirect in the next.config.js
- or maybe we could just use a regex to catch all cases like @bitjson mentioned:
.replace(/\/+$/, '')
.replace(/\/+#/, '#')
.replace(/\/+\?/, '?')
.replace(/\/+/g, '/')
In either case, if the core team is prioritizing this RFC, I would highly recommend we go one step further and make it a built in _config_ that one can _opt out_ of like so
// next.config.js
module.exports = {
ignoreStrictRoutes: false, // default value: true
};
All in all, I think this is a great step forward - good stuff @Timer!! 🔥
@nik-john The path I specified in "/:path*/"
Should catch all (:path
catches a single segment, *
makes it catch 0 to n instances.)
@Janpot Ah my bad 🤦♂ I am guessing we would also need to consider any trailing query params in that regex
Also, I still stand by the second part though:
In either case, if the core team is prioritizing this RFC, I would highly recommend we go one step further and make it a built in config that one can opt out of like so
// next.config.js
module.exports = {
ignoreStrictRoutes: false, // default value: true
};
If you are using a custom server and want to ignore strict routes you can also use a custom route handler instead of making a redirect.
app.render(req, res, urlWithoutTrailingSlash, query);
This way we are able to support both /path
and /path/
and resolve to the same page.
Oauth federation providers often require trailing forward slashes so this behavior makes a simple flow very complicated. What is the technical challenge in implementing this behavior? Or is this a design decision from next?
I haven't seen it mentioned thus far in this thread, but I am not experiencing this issue after deployment with Now, I'm only experiencing it locally when testing with now dev
.
const removeTrailingSlashes = (req, res, expressNext) => {
if (req.path.substr(-1) === '/' && req.path.length > 1) {
const query = req.url.slice(req.path.length);
res.redirect(301, req.path.slice(0, -1) + query);
} else {
expressNext();
}
};
got this from stackoverflow and worked perfectly. this solution work with express.
@GaneshKathar I don't see how this will work if you account for Next.js not using express
I think we can't agree on this and it should be configurable.
I actually want the trailing slash always, the relative urls are easier to reason about when all pages end with trailing slash.
For instance it makes no sense that /about/index.tsx
is /about
instead of /about/
, but understandable now that next expects without trailing slash. If all pages were to end in slash it would allow pages to contain subpages in the future, which I think is more extensible way for pages.
Making relative links inside /about/index.tsx
file is now cumbersome. If you make a link ./mysubpage/
it points to root of the site instead. This makes the subpages non-renameable. I can't make a directory /about/
full of pages that I can just rename, because I should go and edit the relative links too.
Also the wget -r
site produces sensible results with having always trailing slashes, producing index.html files.
However changing this setting is massively breaking change since all sites expect non-trailing slashes, so it must be configurable.
I'm using version 9 and this issue is still not resolved
I was able to make it work by using something like the following on my next.config.js
:
exportPathMap: async function() {
const paths = {
'/': { page: '/' },
'/authors/index.html': { page: '/authors' },
};
return paths;
},
Accessing /authors
gives 302 pointing location
to /authors/
. I'm testing with http-serve
, not sure if this behavior is server-specifiy.
when I faced this issue I came up with this solution
in my _error.js
page
Error.getInitialProps = ({ res, err, asPath }) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
const checkForTrailingSlashes = () => {
if (asPath.match(/\/$/)) { // check if the path ends with trailing slash
const withoutTrailingSlash = asPath.substr(0, asPath.length - 1);
if (res) {
res.writeHead(302, {
Location: withoutTrailingSlash
})
res.end()
} else {
Router.push(withoutTrailingSlash)
}
}
}
if (statusCode && statusCode === 404) {
checkForTrailingSlashes();
} else {
//
}
return { statusCode };
}
is it a good way to overcome the issue ?
How about this?
pages/_app.jsx
```import React from 'react';
import App from 'next/app';
export default class MyApp extends App {
render() {
const { Component, pageProps, router: { asPath } } = this.props;
// Next.js currently does not allow trailing slash in a route.
// This is a client side redirect in case trailing slash occurs.
if (asPath.length > 1 && asPath.endsWith('/')) {
const urlWithoutEndingSlash = asPath.replace(/\/*$/gim, '');
if (typeof window !== 'undefined') {
window.location.replace(urlWithoutEndingSlash);
}
return null;
}
return <Component {...pageProps} />;
}
}
```
@cnblackxp thanks for the suggestion. That helped me. Here is how I implemented it in order to keep the default behavior for non-trailing 404s (i.e. I am simply re-exporting the default Error
implementation):
import Error from "next/error";
import Router from "next/router";
export default Error;
Error.getInitialProps = ({ res, err, asPath }) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
if (statusCode && statusCode === 404) {
if (asPath.match(/\/$/)) {
const withoutTrailingSlash = asPath.substr(0, asPath.length - 1);
if (res) {
res.writeHead(302, {
Location: withoutTrailingSlash
});
res.end();
} else {
Router.push(withoutTrailingSlash);
}
}
}
return { statusCode };
};
yep that'll do @cansin as long as nothing else is decided :) cheers!
Small improvement to @AlexSapoznikov's workaround:
render() {
const { Component, pageProps, router: { asPath } } = this.props;
// Next.js currently does not allow trailing slash in a route.
// This is a client side redirect in case trailing slash occurs.
if (pageProps.statusCode === 404 && asPath.length > 1 && asPath.endsWith('/')) {
The only difference here is checking that the status code is 404. I ran into issues using Link for dynamic routes where they were always rendering on the server because of the redirect. If you want client-side routing to work, you can't add a trailing slash to the Link href
prop, but then you need to make sure you don't redirect in this case.
The problem with implementing a workaround in Error component is that it will throw notifying error in development which bothers me. Some improvement to my previous client side redirect:
What improved is that now it uses next/router in client side and the url replacement happens without a reload.
pages/_app.jsx
import App from 'next/app';
import Router from 'next/router';
export default class MyApp extends App {
render() {
const { Component, pageProps, router: { asPath, route } } = this.props;
// Next.js currently does not allow trailing slash in a route.
// This is a client side redirect in case trailing slash occurs.
if (pageProps.statusCode === 404 && asPath.length > 1 && asPath.endsWith('/')) {
const routeWithoutEndingSlash = route.replace(/\/*$/gim, '');
const asPathWithoutEndingSlash = asPath.replace(/\/*$/gim, '');
if (typeof window !== 'undefined') {
Router.replace(routeWithoutEndingSlash, asPathWithoutEndingSlash);
}
return null;
}
return <Component {...pageProps} />;
}
}
also thanks to @mbrowne for 404 fix :)
Took @cansin 's solution and added the ability to handle query parameters
MyError.getInitialProps = async ({ res, err, asPath }) => {
// Capture 404 of pages with traling slash and redirect them
const statusCode = res
? res.statusCode
: (err ? err.statusCode : 404);
if (statusCode && statusCode === 404) {
const [path, query = ''] = asPath.split('?');
if (path.match(/\/$/)) {
const withoutTrailingSlash = path.substr(0, path.length - 1);
if (res) {
res.writeHead(302, {
Location: `${withoutTrailingSlash}${query ? `?${query}` : ''}`,
});
res.end();
} else {
Router.push(`${withoutTrailingSlash}${query ? `?${query}` : ''}`);
}
}
}
@pinpointcoder can you provide examples of a url with trailing slash and query parameters happen at the same time? Are you thinking along the line of /blog/?123
?
Thanks everyone for some of your workarounds above. They worked!
However, do we have any official way to fix this issue from Next's team? This issue has been here for years.
Directory pages are not served with trailing slash in next export
@pinpointcoder can you provide examples of a url with trailing slash and query parameters happen at the same time? Are you thinking along the line of
/blog/?123
?
@coodoo Not him, but yes, unfortunately this happens a lot. I'm currently in the process of incrementally migrating a WordPress site onto Next.js, and for some reason, the original "developers" decided to force a trailing slash on every single URL, so we currently have tons of requests with both a trailing slash AND query parameters.
As we're about to migrate tons of blog posts for which the canonical URL currently includes a trailing slash, this is a giant pain in my ass right now.
I decided to implement a custom server to handle this and it turns out it's easy to do, and you can still use next.js's file-based routing system. That way you can rewrite the URL that next.js sees and the real URL still has a slash at the end:
const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')
const conf = require('./next.config.js')
const PORT = process.env.PORT || 5000
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev, conf })
const handle = app.getRequestHandler()
app.prepare().then(() => {
createServer((req, res) => {
// If there is a slash at the end of the URL, remove it before sending it to the handle() function.
// This is a workaround for https://github.com/zeit/next.js/issues/5214
const url =
req.url !== '/' && req.url.endsWith('/')
? req.url.slice(0, -1)
: req.url
// 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(url, true)
handle(req, res, parsedUrl)
}).listen(PORT, err => {
if (err) throw err
console.log(`> Ready on http://localhost:${PORT}`)
})
})
@mbrowne We actually have a bunch of reasons to use a custom server, but the main thing that has prevented me from implementing one so far is the fact that you lose Automatic Static Optimization. Do you know if it's possible to manually specify static routes?
We don't need automatic static optimization for our app at the moment, so I haven't looked into it.
I am also using custom server but when you pass modified (without leading slash) url to handle
, SSR sees different url from client side.
I would prefer next
router to match url with leading slash without those nasty hacks.
2020 and this bug still hapens. Unbelievable
This is a bad bug that really needs to be fixed. /products
works, but /products/
doesn't. With this link
<Link href="/products">
<a>Products</a>
</Link>
I get
index.js:1 Warning: Prop `href` did not match. Server: "/products" Client: "/products/"
However, if I point the link to /products/
, visit the link, and refresh the page during development, I get a 404. This is quite a painful development experience.
This issue was first reported 1.5 years ago; can we please get an official fix? It's still present in 9.3.4.
I made redirection to non-trailing slash url instead of showing contents, for SEO reason.
app.prepare().then(() => {
createServer((req, res) => {
if (req.url !== '/' && req.url.endsWith('/')) {
res.writeHead(301, { Location: req.url.slice(0, -1) })
res.end()
}
handle(req, res, parse(req.url, true))
}).listen(PORT, err => {
if (err) throw err
console.log(`> Ready on http://localhost:${PORT}`)
})
})
For SEO, rel="canonical"
may help, but still need to fix this 404 issue.
This is a bad bug that really needs to be fixed.
/products
works, but/products/
doesn't. With this link<Link href="/products"> <a>Products</a> </Link>
I get
index.js:1 Warning: Prop `href` did not match. Server: "/products" Client: "/products/"
However, if I point the link to
/products/
, visit the link, and refresh the page during development, I get a 404. This is quite a painful development experience.This issue was first reported 1.5 years ago; can we please get an official fix? It's still present in 9.3.4.
I am also currently getting this issue.
Here's how I fixed it, https://medium.com/@thisisayush/handling-404-trailing-slash-error-in-nextjs-f8844545afe3
Here's how I fixed it, https://medium.com/@thisisayush/handling-404-trailing-slash-error-in-nextjs-f8844545afe3
Thank you, though this requires a custom server when developing locally, and one shouldn't be required.
@timneutkens Any chance a fix for this issue can be worked into the development schedule?
More importantly, the redirect solution doesn't work for those who are maintaining sites that area already set up to add a slash rather than remove one in production. I don't think the framework should be dictating this choice arbitrarily.
@AlexSapoznikov 's solution worked well for us with Netlify (which adds a trailing slash by default). Here is an advanced version that adds support for query params:
import App from "next/app";
export default class MyApp extends App {
render() {
const { Component, pageProps, router, router: { asPath } } = this.props;
// Next.js currently does not allow trailing slash in a route, but Netlify appends trailing slashes. This is a
// client side redirect in case trailing slash occurs. See https://github.com/zeit/next.js/issues/5214 for details
if (asPath && asPath.length > 1) {
const [path, query = ""] = asPath.split("?");
if (path.endsWith("/")) {
const asPathWithoutTrailingSlash = path.replace(/\/*$/gim, "") + (query ? `?${query}` : "");
if (typeof window !== "undefined") {
router.replace(asPathWithoutTrailingSlash, undefined, { shallow: true });
return null;
}
}
}
return <Component {...pageProps} />;
}
}
I apologize because I'm a Next JS newbie, although I have software development experience on other SDKs and platforms.
I think this "bug" surprised me the most. For me, it violated the "principle of least astonishment." I simply expected my /about/ and /about to work the same, since I placed an index.tsx into my /pages/about/ folder.
I first started making web sites in the late 1990s with HTML FTP'd to my server, and later moved on to PHP & Apache, and eventually Java servers. Now I specialize in mobile apps. It just feels weird to me that this behavior is not the default, and that I'd have to write a custom server page to fix it on my dev server.
I plan to do a static export, so it won't show up in production even if I don't write the custom server. It does make dev and debugging slightly more annoying though.
Can we get a "next dev" flag that fixes this so we lazy developers don't need to write extra routing logic just for dev/debug time?
Thanks!
p.s.: Yes, I do know that /about
and /about/
are completely different URLs. I just got really confused when I put an index.tsx
file inside my /pages/about/
folder, and discovered that it only works with the /about
path but does not work with /about/
. I would be less surprised if it was the other way around.
p.p.s.: It was extra confusing when I have a <Link></Link>
component that points to /about/
and it works as expected. Then when I hit refresh on my browser, it immediately 404s, even though the URL didn't change. That was very surprising. :-D
But wait, it gets worse! We added a custom checkForTrailingSlash
function inside _error.js
that would strip the trailing slash and redirect. This worked okay for a while until we (finally) added a custom 404 page and found that with a custom 404 page, Next.js completely bypasses Error
. This means none of your custom logic inside Error.getInitialProps
will work anymore - including a check for trailing slashes.
Guess I'll try the _app.js
solution others mentioned, as a custom server is just not a possibility quite yet.
@AlexSapoznikov 's solution worked well for us with Netlify (which adds a trailing slash by default). Here is an advanced version that adds support for query params:
import App from "next/app"; export default class MyApp extends App { render() { const { Component, pageProps, router, router: { asPath } } = this.props; // Next.js currently does not allow trailing slash in a route, but Netlify appends trailing slashes. This is a // client side redirect in case trailing slash occurs. See https://github.com/zeit/next.js/issues/5214 for details if (asPath && asPath.length > 1) { const [path, query = ""] = asPath.split("?"); if (path.endsWith("/")) { const asPathWithoutTrailingSlash = path.replace(/\/*$/gim, "") + (query ? `?${query}` : ""); if (typeof window !== "undefined") { router.replace(asPathWithoutTrailingSlash, undefined, { shallow: true }); return null; } } } return <Component {...pageProps} />; } }
There is a critical error in your code sample: requests to the index route with a query parameter will throw an error, since you end up attempting to pass just the query string to Next.js as the asPath
.
This fixes it:
if (asPath && asPath.length > 1) {
const [path, query = ''] = asPath.split('?');
if (path.endsWith('/') && path.length > 1) {
const asPathWithoutTrailingSlash =
path.replace(/\/*$/gim, '') + (query ? `?${query}` : '');
if (typeof window !== 'undefined') {
router.replace(asPathWithoutTrailingSlash, undefined, {
shallow: true,
});
return null;
}
}
}
To make this work with SSR I had to add the following to the @pjaws & @AlexSapoznikov solution:
static async getInitialProps({ Component, ctx, router }) {
/* Fixes the trailing-slash-404 bug for server-side rendering. */
const { asPath } = router;
if (asPath && asPath.length > 1) {
const [path, query = ""] = asPath.split("?");
if (path.endsWith("/") && path.length > 1) {
const asPathWithoutTrailingSlash =
path.replace(/\/*$/gim, "") + (query ? `?${query}` : "");
if (ctx.res) {
ctx.res.writeHead(301, {
Location: asPathWithoutTrailingSlash,
});
ctx.res.end();
}
}
}
return {
pageProps: Component.getInitialProps
? await Component.getInitialProps(ctx)
: {},
};
}
Probably it's a good idea to somehow generalize this functionality into a function that works both during SSR and during CSR and call it in both places (getInitialProps
and render
).
by
this will fix but the tittle wrong. Hmm
@AlexSapoznikov @pjaws
Your solution puts us in infinite loop:
if (asPath && asPath.length > 1) {
const [path, query = ''] = asPath.split('?');
if (path.endsWith('/') && path.length > 1) {
const asPathWithoutTrailingSlash =
path.replace(/\/*$/gim, '') + (query ? `?${query}` : '');
if (typeof window !== 'undefined') {
router.replace(asPathWithoutTrailingSlash, undefined, {
shallow: true,
});
return null;
}
}
}
Due to reasons beyond our control, we have to use the exportTrailingSlash
option in next.config.js
.
We want to have a link to another page but we want the link to be /somepage?param=whatever
.
It seems that next link converts this to /somepage/?param=whatever
and we get page not found.
Using the solution above solve the params problem, but then when going to a deployed page like /somepage/
it enters an infinite loop.
I think @ronyeh had made a really good point here, so I really want an official solution for this issue :(
To make this work with SSR I had to add the following to the @pjaws & @AlexSapoznikov solution:
static async getInitialProps({ Component, ctx, router }) { /* Fixes the trailing-slash-404 bug for server-side rendering. */ const { asPath } = router; if (asPath && asPath.length > 1) { const [path, query = ""] = asPath.split("?"); if (path.endsWith("/") && path.length > 1) { const asPathWithoutTrailingSlash = path.replace(/\/*$/gim, "") + (query ? `?${query}` : ""); if (ctx.res) { ctx.res.writeHead(301, { Location: asPathWithoutTrailingSlash, }); ctx.res.end(); } } } return { pageProps: Component.getInitialProps ? await Component.getInitialProps(ctx) : {}, }; }
Probably it's a good idea to somehow generalize this functionality into a function that works both during SSR and during CSR and call it in both places (
getInitialProps
andrender
).
This has worked for pages with getServerSideProps and now urls with trailing slashes are returning same page without 404.
But there is one glitch, I have few pages that use dynamic routes and getStaticPaths, I can't use getServerSideProps onto them and thus when these dynamic routes are browsed with a trailing slash, they first return a 404 and then they redirect to the page.
I am working with an /api/test folder
it works for
and I just discovered that this doesn't work
not sure if this is related issue
P/D exportTrailingSlash = true does not solve it
This is a very old issue, is there a reason it is not addressed for so long?
I'm not sure what is not working anymore.
My understating is that the requirements are as follows:
| | exportTrailingSlash: false | exportTrailingSlash: true |
|-------------------------|----------------------------|---------------------------|
| url ends with / | Shouldn't work | Should work |
| url does not end with / | Should work | Shouldn't work |
This works as expected where:
exportTrailingSlash: false
exportTrailingSlash: true
and an nginx converts url/
to url/index.html
From what I can see in @andrescabana86 This works where it shouldn't: GET /api/test/123/
whereas GET /api/test/
doesn't work and it shouldn't.
@Izhaki I tried both, deploying on prod... and for me is not working
and I am using exportTrailingSlash: true
I can try creating a public repo if you want, maybe I forgot something in the middle.
thank you for your answers
@andrescabana86 I'm not sure how much a public repo will help here - this may well be some configuration on the server you deploy onto.
We are testing our production builds (with exportTrailingSlash: true
) locally using this script in package.json
:
"serve:out": "docker run --rm -v $(pwd)/out:/static -p 5000:80 flashspys/nginx-static"
Please let me know if going in your browser to http://localhost:5000/api/test/
works.
(Note that $(pwd)
is on Mac/Linux - see this for windows)
@Izhaki the problem was about the fact that (as the initial report suggests) "trailing slash in link for legit page works for client side navigation but leads to not found bundle and 404 on hard refresh (ssr)". So there was a mismatch between the behavior of a client-side route change, versus a hard refresh. I am not sure if the problem persists with the latest version of Next.js. I can report back here once I test it.
Just tested with 9.4.1 and exportTrailingSlash: true
.
Going to http://localhost:6500/admin/
returns 404 when developing locally.
But the same path works when you export.
Note that exportTrailingSlash
hints this is for _exports_ only.
What we do is use:
exportTrailingSlash: process.env.NODE_ENV === 'production'
That means things work as intended when we develop locally. And work properly when deployed (via export).
Isn't that the correct and viable solution for this?
If a URL does not work on development but does work on production, don't you think that is against the principle of least surprise? I think this should still be considered a bug.
^ That said, I am pretty sure previously on production there was a conflicting behavior between a page refresh vs a router.push event. I don't know if it is still the case.
@andrescabana86 @Izhaki exportTrailingSlash
is unrelated to this. That option relates to static exporting of Next.js applications. When true, example/index.html
is generated, whereas when it is false, example.html
is generated. My understanding is that exportTrailingSlash
has nothing to do with development mode.
I think one source of confusion is that when you have exportTrailingSlash
next.js adds a trailing slash to links. This happens in development as well I'm not sure it should do this? But anyhow, this is not only about example/index.html
vs example.html
- you need links to be modified as well.
If a URL does not work on development but does work on production, don't you think that is against the principle of least surprise? I think this should still be considered a bug.
I may be wrong, but exportTrailingSlash option was for nginx servers that are not configured to serve /something.html
when the url is /something
.
This is not the case with the next server used for local dev. So what works and what doesn't depends on what serves your app.
You can make a case that when exportTrailingSlash
is true, the next server should support routes ending with a trailing slash (although this will make the export
in exportTrailingSlash
somewhat irrelevant).
FWIW this is being worked on already #13333
I am not very experienced coder, using Next.js primarily for multi-paged landings. Apparently, I've been using the following workaround almost all of the time, unbeknownst to its effect. Here's stripped down version of it:
// In your server.js
server.get('/:id', (req, res) => {
const actualPage = `/${req.params.id}`
app.render(req, res, actualPage)
})
In my case the code is a little more complicated, because I am using it to support additional static url prefixes, etc. But this stripped down version seems to be working for the discussed issue just fine, regardless of the exportTrailingSlash
setting and its effect on Link
s. E.g. URLs /about
and /about/
work just fine.
In present form It essentially mimics the native routing of Next.js. The downside: it requires custom server.js
, and you will have to manually support it for "deeper" URLs (with additional "subfolders"), e.g. /company/about/
. But it seems to be relatively simple solution for those who already use custom server.js
in their project.
To make this work with SSR I had to add the following to the @pjaws & @AlexSapoznikov solution:
static async getInitialProps({ Component, ctx, router }) { /* Fixes the trailing-slash-404 bug for server-side rendering. */ const { asPath } = router; if (asPath && asPath.length > 1) { const [path, query = ""] = asPath.split("?"); if (path.endsWith("/") && path.length > 1) { const asPathWithoutTrailingSlash = path.replace(/\/*$/gim, "") + (query ? `?${query}` : ""); if (ctx.res) { ctx.res.writeHead(301, { Location: asPathWithoutTrailingSlash, }); ctx.res.end(); } } } return { pageProps: Component.getInitialProps ? await Component.getInitialProps(ctx) : {}, }; }
Probably it's a good idea to somehow generalize this functionality into a function that works both during SSR and during CSR and call it in both places (
getInitialProps
andrender
).This has worked for pages with getServerSideProps and now urls with trailing slashes are returning same page without 404.
But there is one glitch, I have few pages that use dynamic routes and getStaticPaths, I can't use getServerSideProps onto them and thus when these dynamic routes are browsed with a trailing slash, they first return a 404 and then they redirect to the page.
@gauravkrp This is actually an extremely important addition, since @AlexSapoznikov solution will actually still return a 404 for the page to Google (since the redirect happens on the client). I imagine SEO is a major reason a lot of us are using Next.js in the first place.
I also think that putting this in getInitialProps
should just work all around, and the piece inside the main function is unnecessary at this point. The major caveat here is that you are losing Automatic Static Optimization by having this - probably better than a bunch of 404s, though.
For some sharing...
My project is Express
+ Next.js
.
express 4.17.1
next 9.4.5-canary.7
Dynamic Runtime
// next.config.js
module.exports = {
exportTrailingSlash: false,
};
// app.js
const Next = require('next').default;
const NextApp = Next({ dev });
const NextHandler = NextApp.getRequestHandler();
NextApp.prepare();
app.get('*', (req, res) => NextHandler(req, res));
Static Export
Run next build
and next export -o dist/
// next.config.js
module.exports = {
exportTrailingSlash: true,
};
// app.js
app.use('/_next', express.static('dist/_next', { etag: true, index: false, maxAge: '365d', redirect: false, dotfiles: 'ignore' }));
app.use('/fonts', express.static('dist/fonts', { etag: true, index: false, maxAge: '365d', redirect: false, dotfiles: 'ignore' }));
app.use('/img', express.static('dist/img', { etag: true, index: false, maxAge: '365d', redirect: false, dotfiles: 'ignore' }));
app.use(express.static('./dist', { index: ['index.html'] }));
app.use((req, res) => {
res.Redirect('/404'); // <- Express will auto handle both /404 or /404/
});
I have no issue when redirect by clicking on client app,
also hard refresh is working on static route
.
But it will 404 when hard refresh on dynamic route
,
like /album/[id].jsx
or /album/123
,
So I'm looking forward to fix this issue by using the following mechanism.
e.g.
When hit 404 at /album/123
,
server should continue to provide html content,
browser will continue to load the page without issue,
when Next.js boot up then next/router
should auto handle it.
is there any temporary solution to this issue on production?
We're about to land a feature fixing this—a day or so!
is there any temporary solution to this issue on production?
There are many in this thread, but I'm currently using what @gauravkrp posted recently, and it's working well for me.
You can keep track of the PR here: #13333
This has now been resolved in next@^9.4.5-canary.17
!
How long does it take for feature to get from canary to master?
This has now been resolved in
next@^9.4.5-canary.17
!
And how exactly it is resolved? just removing the trailing slash? if i access "www.site.com/help/" i get redirected to: "www.site.com/help" , can we have an option there we opt for leaving ending slash? accessing "www.site.com/help/" or "www.site.com/help" will leave or redirect or add "/" at the end to have: "www.site.com/help/"
@Valnexus see #13333, it includes an experimental option:
module.exports = {
experimental: {
trailingSlash: true
}
}
How long does it take for feature to get from canary to master?
When it's ready. There are still edge cases in the handling that are being solved. Once those have been fixed it can go to stable.
@timneutkens @Janpot
I tried the latest next canary (9.4.5-canary.27) but when I create test
page and I access www.example/test/
it redirects to www.example/test
I think behavior for both cases should be the same.
When access www.example/test/
it should stay on www.example/test/
.
When access www.example/test
it should stay on www.example/test
.
I test it on Nuxt.js, it works the same behavior that I describe above.
I think behavior for both cases should be the same.
The reason for a redirect is to make sure search engines don't see duplicate content. What's your exact use-case?
I don't see why it's a closed issue if it isn't merged to a stable release yet. If I understood correctly it's only fixed in the canary release for now, right?
Issues are closed when their associated pull request lands, as they're available for immediate use on canary. If you need this feature, please upgrade to the canary channel.
Sounds good. Thanks, @Timer!
@Janpot I saw https://github.com/issues/
and https://github.com/issues
can access the same behavior without a redirect.
https://twitter.com/explore/
and https://twitter.com/explore
, this one too.
If it has a problem with search engines, Why Github and Twitter did not fix it?
I think it is the default behavior for any website.
There is no specific use-case, it just my opinion that it should work that way.
If it has a problem with search engines, Why Github and Twitter did not fix it?
@armspkt It is not a problem since there are several ways to solve it. For example Twitter uses <link rel="canonical">
attribute to tell search bots which page they should crawl and other versions should be marked as duplicated.
So redirect is a viable way to make SEO on your website. You can read more info here.
@ziserman If we have several way to solve it, we should keep the same url without redirect for user experience.
@Janpot https://github.com/nuxt-community/nuxt-i18n/issues/422
Nuxtjs have several options to choose (undefined, true, false)
Should Nextjs have serveral options to choose too?
The reason for a redirect is to make sure search engines don't see duplicate content. What's your exact use-case?
@Janpot Our API has trailing slashes in a lot of places. The Latest release raises a lot of 404's on the backend since the Urls with trailing slashes (/api/test/ -> /api/test) do not match
I don't know if it will work for everyone, but I have found this solution that works for me. Put it in the _app.js
file.
static async getInitialProps(ctx) {
const appProps = await App.getInitialProps(ctx);
// Remove trailing slash
const path = ctx.router.asPath,
res = ctx.ctx.res;
if (path.length > 1 && /\/$/.test(path)) {
res.writeHead(301, {Location: path.slice(0, -1)})
res.end();
}
return {...appProps};
}
@mlbonniec I've minimized your comment because it causes severe performance regressions in a Next.js app.
The latest next@canary
version fixes this bug, please upgrade instead!
@mlbonniec I've minimized your comment because it causes severe performance regressions in a Next.js app.
The latest
next@canary
version fixes this bug, please upgrade instead!
No problem!
However, I updated earlier, and that did not solve the problem.
With npm update
If the latest Next.js canary doesn't fix the bug for you, please open a new issue so we can take a look. 🙏
Quick question, how will projects with next export
handle this change? By creating an entirely new page for each page for the trailing slash? I don't think an exported app can specify HTTP redirects (or rewrites).
Projects that use next export
will have all of their <Link />
s on the client-side correctly updated, but the server-side redirect will require manual configuration. Projects deployed with the serverless target or next start
will configure these settings automatically.
@Timer once this reaches a full release, would we still need to use the experimental option?
@Timer once this reaches a full release, would we still need to use the experimental option?
No, would just be available as-is.
I guess the trailingSlash
option won't work for next export
? What's the best way to redirect /page/
to /page
(or vice versa) in, say, github pages?
I guess the
trailingSlash
option won't work fornext export
? What's the best way to redirect/page/
to/page
(or vice versa) in, say, github pages?
As far as I'm aware github pages does not have a redirects feature. This does work out of the box on vercel.com though which is also free for hobby projects (like github pages is).
Projects that use
next export
will have all of their<Link />
s on the client-side correctly updated, but the server-side redirect will require manual configuration. Projects deployed with the serverless target ornext start
will configure these settings automatically.
Hi @Timer Can you explain more? How can I config manually? So here is my situation. On my website, I use next-i18next
. After I deployed with next build && next export
, all of the internal links work but when manually enter URL, NONE of them works and lead to 404 error. From here I decided to use trailingSlash:true
and so manually enter /pricing
will work now but /zh/pricing
leads to 404 errors.
Most helpful comment
We're about to land a feature fixing this—a day or so!