Getting 404 when first loading dynamic routes on nextjs - reactjs

I'm trying to create a blog page to test nextjs and created a dynamic route for the posts, which will be retrieved from Contentful. When navigating from Home page and clicking into a next/router <Link /> component, the blog post loads correctly, but if I get the URL and try loading the page directly from browser address bar, I'll get 404.
Steps to reproduce:
1. git clone https://github.com/zeit/next-learn-demo.git
2. cd next-learn-demo/8-deploying
3. yarn
4. next build && next export
5. cd out
6. serve
7. Navigate to http://localhost:5000/p/learn-nextjs
8. See 404
Is this a limitation of NextJS (didn't find anything related to it on documentation) or do we need to configure anything else?

The real issue is that exporting a next app will make it generate static HTML files. Even though it will still be able to request data before rendering the page, the set of available paths are not dynamic (they are generated during the next export command). See this docs and this example.
Based on this, I have 2 possible solutions:
generate a webhook to trigger a next build && next export command every time a new blog post is published in Contentful;
avoid exporting my next app and host a Node server that will handle the dynamic routes.

That's because when you directly access the link or refresh the page then it add's a slash at the end of route. An next.js doesn't recognize any route like that. To fix this, I hope there should be an easiest way to do that. However you can do this using custom server. Here is an example:
server.get("/about/", (req, res) => {
return app.render(req, res, "/about")
});
Hope this will help you.

To extend the answer provided by #Minoru, the official Next documentation covers this case in this example.
Using getStaticPaths and getStaticProps allows you to create dynamic pages at build time, avoiding the 404.
Example code for posts dynamic page:
import { GetStaticPaths, GetStaticProps } from 'next';
const Post = ({ post }) => {
const { id, content } = post;
return (
<div>
<h1>Post {id}</h1>
<p>{content}</p>
</div>
);
};
export const getStaticPaths: GetStaticPaths = async () => {
// Get all posts via API, file, etc.
const posts = [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }, { id: '5' }]; // Example
const paths = posts.map(post => ({
params: { id: post.id },
}));
return { paths, fallback: false };
};
export const getStaticProps: GetStaticProps = async context => {
const postId = context.params?.id || '';
// Get post detail via API, file, etc.
const post = { id: postId, content: `I'm the post with id ${postId}!` }; // Example
return { props: { post } };
};
export default Post;
When building the site with next build && next export we will see in the out folder that Next has created each post page
Then, when you navigate to /posts/3/ you will see the post with id 3
For reference, this docs page contains this case and many other use cases.

Don't want to infringe any old posts rules, but in case anyone else in my context I use vercel's feature webhook to new deploys and as I was using firebase I've created a simple firebase function whith is hooked to a new event creation of a page triggers the webhook. I've used fetch because we can make a GET request according to the docs
exports.newEventAdded = functions.region('us-central1').firestore.document('local_events/{localeventId}')
.onCreate((snap, context) => {
fetch('https://api.vercel.com/v1/integrations/deploy/process.env.NEXT_PUBLIC_VERCEL_WEBHOOK_ID')
.then(function (response) {
return response.json();
})
.then(function (myJson) {
console.log(JSON.stringify(myJson));
});
})

Related

Next.js SSG - Generate main blog page and internal blog pages with 1 API call

I am using an API to get articles that I want to use to then create a blog.
The blog will have one main blog page, which is a list of all the blog pages and shows a summary for each blog so that in my case is pages/blog/index.tsx
On this page I am using only getStaticProps to retrieve the articles from the API and generate the summary excerpts, which works just fine:
export const getStaticProps: GetStaticProps = async () => {
const body = {
...
};
try {
const postsRes = await axios.post(`someapi/article/getArticles`, body);
const articles: BlogPost[] = postsRes.data.articles.results.map(blogPost => ({
...createPostData...
}));
return { props: { articles } };
} catch (e) {
console.log(e);
}
return { props: { articles: null }, revalidate: 3600 * 4 };
};
So far so good.
I also have pages/blog/[slug].tsx which is the individual page for each blog, here I have to use getStaticPaths and getStaticProps to generate the pages, and I can just call the API again in each of these functions...
I don't want to do that because the API has limits and because there may be a discrepancy between the pages, (what if the API updates right at the second the blog page finishes updating, but before the article pages are generated entirely, so the articles will not match the main blog page).
How should I structure the files and the function calls so that I have to call the API for the data only once, and still generate all of these pages?

React-Native deeplinking if app is closed doesn't work ( React-Navigation v5)

I have implemented deep links for my app with React Navigation V5.
I have a problem regarding Deep Linking. If the app is closed(killed) and it is opened via a deep link it will take me to the home screen rather than the screen it has to take me to.
Here's my linking config, from what I've read in the docs (here), i'm passing the URL from the getInitialUrl function to the subscribe and here
const onReceiveURL = ({ url }) => listener(url);
it should parse the URL to a valid navigation state and take me to the screen it has to. I might be mistaken with how subscribe works, though.
Any help is appreciated, thanks in advance!
const linking = {
prefixes: ['appName://', 'app.appName.com://', APP_WEB_DOMAIN],
async getInitialURL() {
// Check if app was opened from a deep link
const url = await Linking.getInitialURL();
if (url != null) {
return url;
}
},
subscribe(listener) {
const onReceiveURL = ({ url }) => listener(url);
Linking.addEventListener('url', onReceiveURL);
return () => {
// Clean up the event listener
Linking.removeEventListener('url', onReceiveURL);
};
},
config: {
screens: {
SignInScreen: 'login',
UnauthenticatedStack: '',
TrackListScreen: 'playlist/:id/' //:id gets parsed as a string, you have to specify it if you want a number.
}
}
};
I had a similar issue (deep link from push notification) due to a bug in react-native-splash-screen
have a look here https://github.com/spencercarli/react-native-splash-screen-demo/pull/11

Fetching Data from API using NextJS and Material UI React

I am trying to create dynamic pages that shows individual book details (i.e. title/author etc) on a separate page based on a query string of the "id" of each book. However, I am having difficulty in understanding how to make a request to a API endpoint using NextJS that will get the book details based on its "id". I would like to use Material UI as a UI Framework.
ISSUE: When I run npm run dev the book page loads but the book's "props" are not being passed along to the BookAttributes component. The console.log(book) I added in the book page is undefined and the console.log(title) in BookAttributes is undefined as well.
I've tested the API endpoint in POSTMAN and it appears to work.
When I refactor the same code using Semantic UI-React instead of Material UI, the book pages load correctly.
I am using the NextJS Material UI starter template from the Material UI website as a baseline.
I am fairly new to NextJS and Material UI so your assistance and guidance would be greatly appreciated. Thank you for your help on this!
Here is the code I have so. I have tried to keep in clean and simple.
BOOK PAGE (within 'pages' directory)
import axios from 'axios';
import BookAttributes from '../components/Book/BookAttributes';
function Book({ book }) {
console.log(book)
return (
<>
<h1>Book Page</h1>
<BookAttributes {...book} />
</>
)
}
Book.getInitalProps = async ({ query: { _id } }) => {
const url = 'http://localhost:3000/api/book';
const payload = { params: { _id }}
const response = await axios.get(url, payload)
return { book: response.data }
}
export default Book;
BOOK API ENDPOINT (within 'pages/api' directory)
import Book from '../../models/Book';
import connectDb from '../../utils/connectDb';
connectDb()
export default async (req, res) => {
const { _id } = req.query
const book = await Book.findOne({ _id })
res.status(200).json(book);
}
BOOK ATTRIBUTE COMPONENT (within 'components' directory)
import React from 'react';
function BookAttributes({ title }) {
console.log(title)
return (
<>
<h1>{title}</h1>
</>
)
}
export default BookAttributes;
You should be using dynamic routes here if you want to work with data-fetching methods like getStaticProps or getServerSideProps.
You can create a page like pages/book/[id].js. But to generate the page you have to decide what data-fetching method you want to run. If the data for the page doesn't change very often you can choose to use static-site-generation using getStaticProps which will generate the pages at build time. If the data will be changing a lot you can either do server-side-rendering using getServerSideProps or fetch the data client-side.
Here is an example for your use-case that you can use for server-side-rendering using getServerSideProps, keep in mind the API call inside getServerSideProps might fail so you should have appropriate error handling.
In pages/book/[id].js
import axios from 'axios';
import BookAttributes from '../components/Book/BookAttributes';
export const getServerSideProps = async (ctx) => {
const bookId = ctx.params?.id
const url = 'http://localhost:3000/api/book';
const response = await axios.get(url, { params: { _id: bookId} })
return {
props: {
book: response.data
}
}
}
function Book({ book }) {
return (
<>
<h1>Book Page</h1>
<BookAttributes {...book} />
</>
)
}
export default Book;
Using static-site-generation
Because the page is dynamic you have to provide a list of paths for which nextjs will generate the pages. You can do that by exporting an async function called getStaticPaths.
in pages/book/[id].js
import axios from 'axios';
import BookAttributes from '../components/Book/BookAttributes';
export const getStaticPaths = async () => {
// here you have two options if you know all the ids of the books
// you can fetch that data from the api and use all the ids to generate
// a list of paths or show a fallback version of page if you don't know all
// ids and still want the page to be static
// Pseudo code might look like this
const res = await axios.get('api-endpoint-to-fetch-all-the-books')
const paths = res.data.map(book => ({ params: { id: book.id }}))
return {
paths,
fallback: false
}
}
export const getStaticProps = async (ctx) => {
const bookId = ctx.params?.id
const url = 'http://localhost:3000/api/book';
const response = await axios.get(url, { params: { _id: bookId} })
return {
props: {
book: response.data
}
}
}
function Book({ book }) {
return (
<>
<h1>Book Page</h1>
<BookAttributes {...book} />
</>
)
}
export default Book;
The fallback property in the returned value of getStaticPaths is somewhat important to understand. If you know all the necessary id for the pages you can set the fallback to false. In this case nextjs will simply show a 404 error page for all the paths that were not returned from the function getStaticPaths.
If fallback is set to true nextjs will show a fallback version of page instead of a 404 page for the paths that were not returned from the getStaticPaths function. Now where should you set fallback to true? Let's suppose in your case new books are added to the database frequently, but the data for the books doesn't change very often so you want the pages to be static. In this case, you can set fallback to true and generate a list of paths based on avaliable book ids. For the new books nextjs will first show the fallback version of the page than fetch the data based on the id provided in the request and will send the data as JSON which will be used to render the page in the client.

Handle URL Parameters with Gatsby

I'm using React & Gatsby to create a website and I've got everything laid out the way I want, I'm just having some problems understanding how to use URL Route parameters to change content that is displayed.
For example, say I have a page categories.js which listens to http://website.com/categories but I want to be able to dynamically handle any URL parameters like such:
http://website.com/categories/animals
When using gatsby-link like so: <Link to="/categories/animals" /> it wants me to create a file called animals.js in a categories folder. Instead, I want categories.js to be able to handle the rendering for this page and select the content appropriate based on the category passed in the URL parameters.
Other than the items displayed, this page is exactly the same through all categories, so it doesn't make sense for every category to have it's own static page.
I think you are wrong when you say:
Other than the items displayed, this page is exactly the same through all categories, so it doesn't make sense for every category to have it's own static page.
In fact, that's exactly where I find GatsbyJS so useful, as it is a static site generator.
It means that you can give Gatsby a template component that will have the same layout for all of your categories, and then Gatsby will fetch data and create static pages for you at build time.
Gatsby is not limited to making pages from files like many static site generators. Gatsby lets you use GraphQL to query your data and map the data to pages—all at build time. (from Gatsby official tutorial)
The idea would be something like this:
in /gatsby-node.js
const path = require(`path`); // you will need it later to point at your template component
exports.createPages = ({ graphql, boundActionCreators }) => {
const { createPage } = boundActionCreators;
// we use a Promise to make sure the data are loaded
// before attempting to create the pages with them
return new Promise((resolve, reject) => {
// fetch your data here, generally with graphQL.
// for example, let say you use your data from Contentful using its associated source plugin
graphql(`
{
allContentfulCategories {
edges {
node {
id
name
description
# etc...
}
}
}
}
`).then(result => {
// first check if there is no errors
if (result.errors) {
// reject Promise if error
reject(result.errors);
}
// if no errors, you can map into the data and create your static pages
result.data.allContentfulCategories.edges.forEach(({ node }) => {
// create page according to the fetched data
createPage({
path: `/categories/${node.name}`, // your url -> /categories/animals
component: path.resolve('./src/templates/categories.js'), // your template component
context: {
// optional,
// data here will be passed as props to the component `this.props.pathContext`,
// as well as to the graphql query as graphql arguments.
id: node.id,
},
});
});
resolve();
});
});
};
And then you can simply fetch the data on your template component
in /src/templates/categories.js
import React from "react";
export default ({ data: { contentfulCategories: category } }) => {
return (
<div>
<h1>{category.name}</h1>
<div>{category.description}</div>
</div>
);
};
// we can query the needed category according to the id passed in the
// context property of createPage() in gatsby-node.js
export const query = graphql`
query CategoryQuery($id: String!) {
contentfulCategories(id: { eq: $id }) {
name
description
}
}
`;
If you insist in dynamically rendering your categories pages, you can take example of this question that is sort of similar to yours, but note that you will have to manually handle re-rendering if the page's props changes in the componentWillReceiveProps lifecycle method of the React class component.
But I really don't think this is a reliable solution.
EDIT:
I've found a better answer after searching forever! -
https://github.com/gatsbyjs/gatsby/issues/13965#issuecomment-617364363
the example for your case I imagine would be something like this:
gatsby-node.js:
createPage({
path: `/categories/:id`,
matchPath: `/categories/:id`,
component: path.resolve(`./src/pages/categories.js`),
})
categories.js:
import React from "react"
export default function Booking({ id }) {
return <div>categories #{id}</div>
}
OLD ANSWER:
I seem to have come across exactly the same issue too and couldn't find any answers. My idea was to also use the gatsby-node.js file, but I don't query anything using graphQL.
My version of /src/templates/categories.js:
const path = require(`path`)
exports.createPages = ({ graphql, actions }) => {
const { createPage } = actions
createPage({
path: `/categories/animals`,
component: path.resolve(`./src/pages/categories.js`),
// The context is passed as props to the component as well
// as into the component's GraphQL query.
context: {
id: `animals`, //use this context parameter to do the dynamic stuff in your page
},
})
}
I hope this is useful.

Next.js - Error: only absolute urls are supported

I'm using express as my custom server for next.js. Everything is fine, when I click the products to the list of products
Step 1: I click the product Link
Step 2: It will show the products in the database.
However if I refresh the /products page, I will get this Error
Server code (Look at /products endpoint)
app.prepare()
.then(() => {
const server = express()
// This is the endpoints for products
server.get('/api/products', (req, res, next) => {
// Im using Mongoose to return the data from the database
Product.find({}, (err, products) => {
res.send(products)
})
})
server.get('*', (req, res) => {
return handle(req, res)
})
server.listen(3000, (err) => {
if (err) throw err
console.log('> Ready on http://localhost:3000')
})
})
.catch((ex) => {
console.error(ex.stack)
process.exit(1)
})
Pages - products.js (Simple layout that will loop the products json data)
import Layout from '../components/MyLayout.js'
import Link from 'next/link'
import fetch from 'isomorphic-unfetch'
const Products = (props) => (
<Layout>
<h1>List of Products</h1>
<ul>
{ props.products.map((product) => (
<li key={product._id}>{ product.title }</li>
))}
</ul>
</Layout>
)
Products.getInitialProps = async function() {
const res = await fetch('/api/products')
const data = await res.json()
console.log(data)
console.log(`Showed data fetched. Count ${data.length}`)
return {
products: data
}
}
export default Products
As the error states, you will have to use an absolute URL for the fetch you're making. I'm assuming it has something to do with the different environments (client & server) on which your code can be executed. Relative URLs are just not explicit & reliable enough in this case.
One way to solve this would be to just hardcode the server address into your fetch request, another to set up a config module that reacts to your environment:
/config/index.js
const dev = process.env.NODE_ENV !== 'production';
export const server = dev ? 'http://localhost:3000' : 'https://your_deployment.server.com';
products.js
import { server } from '../config';
// ...
Products.getInitialProps = async function() {
const res = await fetch(`${server}/api/products`)
const data = await res.json()
console.log(data)
console.log(`Showed data fetched. Count ${data.length}`)
return {
products: data
}
}
Similar to the #Shanker's answer, but if you prefer not to install the additional package for this, here is how to do it.
async getInitialProps({ req }) {
const protocol = req.headers['x-forwarded-proto'] || 'http'
const baseUrl = req ? `${protocol}://${req.headers.host}` : ''
const res = await fetch(baseUrl + '/api/products')
}
It sounds silly but worth mentioning. If you're using SSR in your webapp the fetch call will work with a relative link on the client but will fail on the server. Only the server needs an absolute link!
If you want to prevent the server from making the request just wrap it in logic
if(global.window){
const req = fetch('/api/test');
...
}
You could utilize environment variables if your project is hosted on a provider that supports it.
env.local
// Local
URL="http://localhost:3000"
// Production
URL="https://prod.com"
Then you can use the following.
const { URL } = process.env;
const data = await fetcher(URL + '/api');
This simple solution worked for me without having to add an additional config file,
Install
npm install --save next-absolute-url
Usage
import absoluteUrl from "next-absolute-url";
async getInitialProps({ req }){
const { origin } = absoluteUrl(req, req.headers.host);
console.log('Requested URL ->',origin);
// (or) other way
const host = absoluteUrl(req, req.headers.host);
console.log('Requested URL ->',host.origin);
}
Case 1. It's not an error. The isomorphic-unfetch is running by SSR mode, so Node.js needs to know the absolute url to fetch from it, because the back-end doesn't know your browser settings.
Case 2. Another scenario is to prevent the http host poisoning headers attack.
append secret keys and tokens to links containing it:
<a href="http://_SERVER['HOST']?token=topsecret"> (Django, Gallery, others)
....and even directly import scripts from it:
<script src="http://_SERVER['HOST']/misc/jquery.js?v=1.4.4">
Case 3. The isomorphic-unfetch it's the library we are going to use to fetch data. It's a simple implementation of the browser fetch API, but works both in client and server environments.
Read more about it:
Isomorphic unfetch - Switches between unfetch & node-fetch for client & server
Prevent http host headers attack
Fetching Data for Pages
In the NextJS 9.5, we can also use process.cwd().
process.cwd() will give you the directory where Next.js is being executed.
import path from 'path'
import fs from "fs";
export const getStaticProps: GetStaticProps = async () => {
const dataFilePath = path.join(process.cwd(), "jsonFiles", "data.json");
console.log(dataFilePath); // will be YourProject/jsonFiles/data.json
const fileContents = fs.readFileSync(dataFilePath, "utf8");
const data: TypeOfData[] = JSON.parse(fileContents);
return { props: { data } };
};
Ref: https://nextjs.org/docs/basic-features/data-fetching#reading-files-use-processcwd
Putting this out there because this showed up in google results for my problem, even though the question itself isn't really related (outside of the fact that the same dependency is throwing the same error message, albeit in a different context for a different reason).
I got this issue from using hardhat while attempting to verify (verify:verify) my contract on etherscan. The problem was that in the hardhat config, I didn't have a full url under rinkeby (since I was verifying on rinkeby, would be mainnet, etc.). Copy/pasting some config stuff quickly into a project I cloned from someone else, they had a full URL in their .env, while I had the url in the config and stored only my api key in my .env.
To figure this out, though, was straightforward--go into node_modules, then find the node-fetch folder, then lib, (this is from memory--just find the line that is vomitting in your stack trace) then the line number, and put a console log there to see what the "bad" url you're seeing is. Usually that's enough of a clue; in my case, it was an API key, obviously not a URL, and that made it straightforward to solve.
If you have an absolute path issues. Try to use swr to access data.
Notice: This is a React hooks so you must call inside the component.
import useSWR from 'swr';
// optionally you can use unfetch package from npm or built yours to handle promise.
const fetcher = (...args) => fetch(...args).then(res => res.json())
export const AllProducts = () => {
const { data, error } = useSWR('/api/products', fetcher)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return (
<div>{data}</div>
);
};
Export or deploying in production
Whenever you are trying to deploy on Vercel you might encounter an error. For instance `
warn - Statically exporting a Next.js application via `next export` disables API routes`.
It means you are trying to export data and NextJS does not support fetching data from pages/api/* directory. To avoid errors, its better to separate build and export command.
// package.json
{
"scripts": {
"dev": "next",
"build": "next build", // No next export command
"start": "next start"
},
}
Thanks folks for great contribution and I hope the answer shared will help somebody too.
Make sure what the value of your API url is
In my case, I was using POST but my url was somewhat undefined.
Use console.log to see where is your request going.
this is a way to get the base hostname to fetch data from external endpoint
without getting that error
function return_url(context) {
if (process.env.NODE_ENV === "production") {
return `https://${context.req.rawHeaders[1]}`;
} else if (process.env.NODE_ENV !== "production") {
return "http://localhost:3000";
}
}
and on the getServerSideProps or getStaticProps functions you use
export async function getServerSideProps(context) {
let url = return_url(context);
const data = await fetch(`${url}/yourEndPoint`).then((res) => res.json());
return {
props: {
data: data,
},
};
}
If you are using next environment config prefix your variables with NEXT_PUBLIC_ as mentioned here Exposing Environment Variables to the Browser.
USE: NEXT_PUBLIC_STRAPI_URL="http://localhost:1337" instead of
NEXT_PUBLIC_STRAPI_URL=http://localhost:1337
use .log(console.log) after nock , so you will get exact unmatched and expected url .
Example:
nock("http://localhost")
.log(console.log)
.persist()
.get("/api/config")
.reply(200, { data: 1234 })

Resources