I am trying to set up a next.js app, but I'm having trouble handling routes with a trailing slash. So, for example, if I have a pages structure like this:
pages
- index.js
- blog
- index.js
- [slug].js
then going to / gives me the base index.js, going to /blog gives me blog/index.js, and going to /blog/my-post gives me blog/[slug].js — so far so good.
But going to /blog/ gives me a 404 error, and there appears to be no way at all to handle this without entirely replacing the next.js router—I can't even redirect /blog/ to /blog. Is there any way around this, or do I need a custom router? Is there a way to extend the next.js router in a way that will let me handle these, without entirely replacing it?
There is an option with Next.js 9.5 and up.
In next.config.js, add the trailingSlash config:
module.exports = {
trailingSlash: true,
}
Source: Trailing Slash
UPDATE: If you are using next export than you can solve the issue by adding exportTrailingSlash to your next.config.js
As of this writing, there seems to be no way to solve this issue without defining your own custom server.
Previous answer:
You must create a new file blog.js shown bellow:
With the following server.js
const express = require('express')
const next = require('next')
const PORT = process.env.PORT || 3000;
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
app
.prepare()
.then(() => {
const server = express()
server.get('/blog', (req, res) => {
const actualPage = '/blog'
// const queryParams = { title: req.params.id }
app.render(req, res, '/blog', {})
})
server.get('/blog/:id', (req, res) => {
const actualPage = '/blog/[id]'
const queryParams = { title: req.params.id }
app.render(req, res, actualPage, queryParams)
})
server.get('*', (req, res) => {
return handle(req, res)
})
server.listen(PORT, err => {
if (err) throw err
console.log(`> Ready on http://localhost:${PORT}`)
})
})
.catch(ex => {
console.error(ex.stack)
process.exit(1)
})
node server.js should start your server and you would have the mapping that you need.
Note, blog/index.js is not used in this example.
I don't have a solution to this. This is still a massive problem. However, I do have a very horrible hack that I've been using to workaround this problem in Next.js 9.x.x
In my /pages/_error.js I've added the following code:
FourOhFour.getInitialProps = ({asPath, res}: any): any => {
if (asPath.endsWith('/')) {
res.writeHead(301, {Location: asPath.substring(0, asPath.length - 1)});
return res.end();
}
return {};
};
This will ensure that all routes that end with a trailing slash are redirected to the non-trailing slash path via a 301 redirect.
I`ve done that redirect by using nginx for my production app:
rewrite ^(.+)/+$ $1 permanent;
You can add this to your _app.js file
MyApp.getInitialProps = async ctx => {
const pathAndQueryDivided = ctx.ctx.req.url.split('?');
if (pathAndQueryDivided[0].endsWith('/')) {
const urlWithoutEndingSlash = pathAndQueryDivided[0].replace(/\/*$/gim, '');
ctx.ctx.res.writeHead(301, {
Location: urlWithoutEndingSlash + (pathAndQueryDivided.length > 1 ? `?${pathAndQueryDivided[1]}` : ''),
});
ctx.ctx.res.end();
return {};
}
const initialProps = await App.getInitialProps(ctx);
return {
...initialProps,
};
};
App.getInitialProps gets called first server-side
It divides splits the path and the query params because we only want to know if the url ends with a slash
It asks if the url ends with a slash
It replaces the last slash of the url with nothing
It redirects you to the url without the slash + the query params if there's any
Related
I have an app made with React, Node.js and Socket.io
I deployed Node backend to heroku , frontend to Netlify
I know that CORS errors is related to server but no matter what I add, it just cant go through that error in the picture below.
I also added proxy script to React's package.json as "proxy": "https://googledocs-clone-sbayrak.herokuapp.com/"
And here is my server.js file;
const mongoose = require('mongoose');
const Document = require('./Document');
const dotenv = require('dotenv');
const path = require('path');
const express = require('express');
const http = require('http');
const socketio = require('socket.io');
dotenv.config();
const app = express();
app.use(cors());
const server = http.createServer(app);
const io = socketio(server, {
cors: {
origin: 'https://googledocs-clone-sbayrak.netlify.app/',
methods: ['GET', 'POST'],
},
});
app.get('/', (req, res) => {
res.status(200).send('hello!!');
});
const connectDB = async () => {
try {
const connect = await mongoose.connect(process.env.MONGODB_URI, {
useUnifiedTopology: true,
useNewUrlParser: true,
});
console.log('MongoDB Connected...');
} catch (error) {
console.error(`Error : ${error.message}`);
process.exit(1);
}
};
connectDB();
let defaultValue = '';
const findOrCreateDocument = async (id) => {
if (id === null) return;
const document = await Document.findById({ _id: id });
if (document) return document;
const result = await Document.create({ _id: id, data: defaultValue });
return result;
};
io.on('connection', (socket) => {
socket.on('get-document', async (documentId) => {
const document = await findOrCreateDocument(documentId);
socket.join(documentId);
socket.emit('load-document', document.data);
socket.on('send-changes', (delta) => {
socket.broadcast.to(documentId).emit('receive-changes', delta);
});
socket.on('save-document', async (data) => {
await Document.findByIdAndUpdate(documentId, { data });
});
});
console.log('connected');
});
server.listen(process.env.PORT || 5000, () =>
console.log(`Server has started.`)
);
and this is where I make request from frontend;
import Quill from 'quill';
import 'quill/dist/quill.snow.css';
import { useParams } from 'react-router-dom';
import { io } from 'socket.io-client';
const SAVE_INTERVAL_MS = 2000;
const TextEditor = () => {
const [socket, setSocket] = useState();
const [quill, setQuill] = useState();
const { id: documentId } = useParams();
useEffect(() => {
const s = io('https://googledocs-clone-sbayrak.herokuapp.com/');
setSocket(s);
return () => {
s.disconnect();
};
}, []);
/* below other functions */
/* below other functions */
/* below other functions */
}
TL;DR
https://googledocs-clone-sbayrak.netlify.app/ is not an origin. Drop that trailing slash.
More details about the problem
No trailing slash allowed in the value of the Origin header
According to the CORS protocol (specified in the Fetch standard), browsers never set the Origin request header to a value with a trailing slash. Therefore, if a page at https://googledocs-clone-sbayrak.netlify.app/whatever issues a cross-origin request, that request's Origin header will contain
https://googledocs-clone-sbayrak.netlify.app
without any trailing slash.
Byte-by-byte comparison on the server side
You're using Socket.IO, which relies on the Node.js cors package. That package won't set any Access-Control-Allow-Origin in the response if the request's origin doesn't exactly match your CORS configuration's origin value (https://googledocs-clone-sbayrak.netlify.app/).
Putting it all together
Obviously,
'https://googledocs-clone-sbayrak.netlify.app' ===
'https://googledocs-clone-sbayrak.netlify.app/'
evaluates to false, which causes the cors package not to set any Access-Control-Allow-Origin header in the response, which causes the CORS check to fail in your browser, hence the CORS error you observed.
Example from the Fetch Standard
Section 3.2.5 of the Fetch Standard even provides an enlightening example of this mistake,
Access-Control-Allow-Origin: https://rabbit.invalid/
and explains why it causes the CORS check to fail:
A serialized origin has no trailing slash.
Looks like you haven't imported the cors package. Is it imported anywhere else?
var cors = require('cors') // is missing
Hi I'm using NextJs and I am having an issue that when my app is hosted on my local server the pages that are pre loaded with getStaticProps are loading in a few ms but when hosted on Vercel it is taking 300ms to load.
Does anyone have any suggestions on how I can get my pages to load quicker on Vercel?
My app is currently hosted at https://next-movie-delta.vercel.app/
and my github repo is https://github.com/lewisRotchell/next-movie
My getStaticPaths code looks like:
export async function getStaticPaths() {
const movies = await getMovies();
const bigMovieArray = [
...movies[0].results,
...movies[1].results,
...movies[2].results,
];
const paths = bigMovieArray.map((movie) => ({
params: { movieId: movie.id.toString() },
}));
return {
paths,
fallback: "blocking",
};
}
and the getMovies code looks like :
export async function getMovies() {
const urls = [newReleasesUrl, popularMoviesUrl, topRatedMoviesUrl];
try {
let data = await Promise.all(
urls.map((url) => fetch(url).then((response) => response.json()))
).catch((error) => console.log(error));
return data;
} catch (error) {
console.log(error);
}
}
Thanks :)
I've managed to fix the problem! I changed the linking from withRouter to a tag from next/link and it has fixed my issue!
I am generating an excel file and want it to be downloaded for the client by triggering an API route using the Next.js framework. I am having trouble triggering the download by using fetch. The download can be triggered by window.open(//urlhere, '_self') but the API call using fetch gives this response on request:
API resolved without sending a response for /api/download?Students= this may result in stalled requests.
The excel4node documentation says we can send an excel document through an API like this:
// sends Excel file to web client requesting the / route
// server will respond with 500 error if excel workbook cannot be generated
var express = require('express');
var app = express();
app.get('/', function(req, res) {
wb.write('ExcelFile.xlsx', res);
});
app.listen(3000, function() {
console.log('Example app listening on port 3000!');
});
Here is my backend download.js which lives in pages/api/:
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import Link from "next/link";
import getPartners from "../../components/main";
import excel from "excel4node";
export default function handler(req, res) {
const students = req.query.Students.split(",");
const partners = JSON.stringify(getPartners(students));
let workbook = createExcelList(partners);
workbook.write("PartnerList.xlsx", res);
}
const createExcelList = (partnersJSON) => {
const workbook = new excel.Workbook();
const partnersObject = JSON.parse(partnersJSON);
/* Using excel4node a workbook is generated formatted the way I want */
return workbook;
};
export const config = {
api: {
bodyParser: true,
},
};
And here is the function that is triggered on a button press in the front end.
const makePartners = async () => {
let queryStudents = studentList.join(",");
const url = "http://localhost:3000/api/download?Students=" + queryStudents;
if (studentList.length !== 0) {
try {
const res = await fetch(url, {
headers: {
"Content-Type":
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
},
});
console.log(res);
} catch (e) {
console.log(e);
}
}
};
Which does not trigger the download. But using window.open(url, '_self) does. So, I can trigger the download by changing the function to the following. However I don't think this is the correct way of doing things and would like to be able to understand how to use fetch correctly.
const makePartners = () => {
let queryStudents = studentList.join(",");
const url = "http://localhost:3000/api/download?Students=" + queryStudents;
if (studentList.length !== 0) {
window.open(url, "_Self");
}
};
I am not sure if this is a Next.js issue or not. Does anyone have any insight? Any help would be appreciated.
I'm using a custom server.js for routing which uses something like this:
app.prepare().then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url, true);
const rootStaticFiles = ['/robots.txt', '/sitemap.xml', '/favicon.ico'];
if (rootStaticFiles.indexOf(parsedUrl.pathname) > -1) {
const path = join(__dirname, 'static', parsedUrl.pathname);
app.serveStatic(req, res, path);
} else {
handler(req, res);
}
}).listen(3000);
});
And when I run it, there's this warning about the 'url' property, but I don't know ho to resolve it. It seems to me that withRouter doesn't apply here. Could someone help please?
Okay, it looks like they solved it with one of the updates. It does not appear anymore.
-component
---->sidebar.js
---->exampleTabOne.js
---->exampleTabTwo.js
---->exampleTabThree.js
--pages
---->setting(which include all sidebar and those exampletabs)
i do've above folder structure in my nextJS project.
here as per nextjs doc
on localhost/setting i can easily view my page
but what i want to achieve is something like below:
1.localhost/setting/exampleTabOne
2.localhost/setting/exampleTabTwo/EXAMPLEID
3.localhost/setting/exampleTabThree/EXAMPLEID#something#something
the last part Url with # is something like inside tab content i ve another tabs so i want to fix it with Hash url so that while ssr i can easily open that inside tab too..
So, will you guys please suggest me how to solve this?
Here , In Next JS, We can achieve this by defining in server.js file.
// This file doesn't go through babel or webpack transformation.
// Make sure the syntax and sources this file requires are compatible with the current node version you are running
// See https://github.com/zeit/next.js/issues/1245 for discussions on Universal Webpack or universal Babel
const { createServer } = require('http');
const { parse } = require('url');
const next = require('next');
const dev = process.env.NODE_ENV !== 'production';
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;
if (pathname === '/setting/exampleTabOne') {
app.render(req, res, '/setting', query);
} else if (pathname === '/setting/exampleTabTwo/EXAMPLEID') {
app.render(req, res, '/setting', query);
} else {
handle(req, res, parsedUrl);
}
}).listen(3000, err => {
if (err) throw err;
console.log('> Ready on http://localhost:3000');
});
});
Where In setting page we can dynamically load the respective component watching url pathname.