next.js getStaticPaths list every path or only those in the immediate vicinity? - static

Using Next.js to export static pages, I get that in a dynamic route like pages/[id].js any path I put in the getStaticPaths section will be created. Cool.
Is it better to list every page:
getStaticPaths(){
return (
// some function to spit out a list of every possible page
)
}
or
getStaticPaths(){
return (
// some function to return the next and previous page
)
}
or does it matter?

For dynamic routes example posts/[id].js getStaticPaths needs to define a list of paths so that Next.js pre-render all the specified paths at build time.
The function getStaticPaths needs to return an object with paths property which is an array with the route params and the property fallback which will be true or false. If fallback is set to false for any path that is not returned from the function getStaticPaths will not be pre-rendered hence resulting in a 404 page.
If you know all the paths that you need to render ahead of time you can set fallback to false.Here is an example..
// getStaticPaths for /category/[slug] where slug can only be -
// either 'category-slug-1', 'category-slug-2' or 'category-slug-3'
export const getStaticPaths = async () => {
return {
paths: [
{ params: { slug: 'category-slug-1'} },
{ params: { slug: 'category-slug-2'} },
{ params: { slug: 'category-slug-3'} }
],
fallback: false // fallback is set to false because we already know the slugs ahead of time
}
}
Let's says you have a route /posts/[id].js and ids coming from a database, and new posts are created every day. In this case, you can return the paths which already exist to pre-render some pages. and set fallback to true and on request, Next.js will serve a fallback version of the page instead of showing 404 page for the paths that are not returned from the function getStaticPaths, then in the background, nextjs will call getStaticProps for the requested path and serve the data as JSON which will be used to render the page in the browser.
Here is an example,
export const getStaticPaths = async () => {
const posts = await // your database query or fetch to remote API
// generate the paths
const paths = posts.map(post => ({
params: { id: post.id } // keep in mind if post.id is a number you need to stringify post.id
})
);
return {
paths,
fallback: true
}
}
P.S. - When using fallback set to true you need to render some sort of fallback component in your NextPage component otherwise when you try to access the data from props, it will throw an error like cannot read property ...x of undefined
You can render a fallback like this,
// in your page component
import {useRouter} from 'next/router';
const router = useRouter();
if (router.isFallback) {
return <div>loading...</div>
}

const paths = posts.map(post => ({
params: { id: post.id.toString() }
})

Related

How to render [slug].js page after fetching data Next.js

I am trying to create a logic for my blog/:post page in Next.js but I cannot seem to figure out how.
The idea is to:
Fetch the url (using useRouter)
Call API (it is a headless CMS) to get the info of the post
Render the post
What I have right now is:
[other imports ...]
import { useEffect, useRef, useState } from "react";
import { useRouter } from 'next/router'
const apikey = process.env.NEXT_PUBLIC_BUTTER_CMS_API_KEY;
const butter = require('buttercms')(apikey);
function BlogPost(props) {
const router = useRouter()
const { slug } = router.query
const [blogPost, setBlogPost] = useState({})
// Function to the blog post
function fetchBlogPost() {
butter.post.retrieve(slug)
.then(response => {
const blogPostData = response.data.data
setBlogPost(blogPostData)
})
}
useEffect(() => {
// We need to add this if condition because the router wont grab the query in the first render
if(!router.isReady) return;
fetchBlogPost()
}, [router.isReady])
return (
<>
# Render post with the data fetched
</>
)
}
export default BlogPost;
But this is not rendering everything (the image is not being rendered for example). I believe it is because of the pre-render functionality that Next.js has. Also I have been reading about the getStaticProps and getStaticPaths but I am unsure on how to use them properly.
Any guidance will be welcome. Thanks!
If you're using next.js then you are on track with getStaticProps being your friend here!
Essentially getStaticProps allows you to take advantage of ISR to fetch data on the server and create a static file of your page with all of the content returned from the fetch.
To do this you'll need to make an adjustment to your current architecture which will mean that instead of the slug coming in from a query param it will be a path parameter like this: /blogs/:slug
Also this file will need to be called [slug].js and live in (most likely) a blogs directory in your pages folder.
Then the file will look something like this:
import { useEffect, useRef, useState } from "react";
import { useRouter } from 'next/router'
const apikey = process.env.NEXT_PUBLIC_BUTTER_CMS_API_KEY;
const butter = require('buttercms')(apikey);
export const getStaticPaths = async () => {
try {
// You can query for all blog posts here to build out the cached files during application build
return {
paths:[], // this would be all of the paths returned from your query above
fallback: true, // allows the component to render with a fallback (loading) state while the app creates a static file if there isn't one available.
}
} catch (err) {
return {
paths: [],
fallback: false,
}
}
}
export const getStaticProps = async ctx => {
try {
const { slug } = ctx.params || {}
const response = await butter.post.retrieve(slug)
if(!response.data?.data) throw new Error('No post data found') // This will cause a 404 for this slug
return {
notFound: false,
props: {
postData: response.data.data,
slug,
},
revalidate: 5, // determines how long till the cached static file is invalidated.
}
} catch (err) {
return {
notFound: true,
revalidate: 5,
}
}
}
function BlogPost(props) {
const {isFallback} = useRouter() // We can render a loading state while the server creates a new page (or returns a 404).
const {postData} = props
// NOTE: postData might be undefined if isFallback is true
return (
<>
# Render post with the data fetched
</>
)
}
export default BlogPost;
In any case, though if you decide to continue with rendering on the client instead then you might want to consider moving your fetch logic inside of the useEffect.

How to get URL query string on Next.js static site generation?

I want to get query string from URL on Next.js static site generation.
I found a solution on SSR but I need one for SSG.
Thanks
import { useRouter } from "next/router";
import { useEffect } from "react";
const router = useRouter();
useEffect(() => {
if(!router.isReady) return;
const query = router.query;
}, [router.isReady, router.query]);
It works.
I actually found a way of doing this
const router = useRouter()
useEffect(() => {
const params = router.query
console.log(params)
}, [router.query])
As other answers mentioned, since SSG doesn't happen at request time, you wouldn't have access to the query string or cookies in the context, but there's a solution I wrote a short article about it here https://dev.to/teleaziz/using-query-params-and-cookies-in-nextjs-static-pages-kbb
TLDR;
Use a middleware that encodes the query string as part of the path,
// middleware.js file
import { NextResponse } from 'next/server'
import { encodeOptions } from '../utils';
export default function middleware(request) {
if (request.nextUrl.pathname === '/my-page') {
const searchParams = request.nextUrl.searchParams
const path = encodeOptions({
// you can pass values from cookies, headers, geo location, and query string
returnVisitor: Boolean(request.cookies.get('visitor')),
country: request.geo?.country,
page: searchParams.get('page'),
})
return NextResponse.rewrite(new URL(`/my-page/${path}`, request.nextUrl))
}
return NextResponse.next()
}
Then make your static page a folder that accepts a [path]
// /pages/my-page/[path].jsx file
import { decodeOptions } from '../../utils'
export async function getStaticProps({
params,
}) {
const options = decodeOptions(params.path)
return {
props: {
options,
}
}
}
export function getStaticPaths() {
return {
paths: [],
fallback: true
}
}
export default function MyPath({ options }) {
return <MyPage
isReturnVisitor={options.returnVisitor}
country={options.country} />
}
And your encoding/decoding functions can be a simple JSON.strinfigy
// utils.js
// https://github.com/epoberezkin/fast-json-stable-stringify
import stringify from 'fast-json-stable-stringify'
export function encodeOptions(options) {
const json = stringify(options)
return encodeURI(json);
}
export function decodeOptions(path) {
return JSON.parse(decodeURI(path));
}
You don't have access to query params in getStaticProps since that's only run at build-time on the server.
However, you can use router.query in your page component to retrieve query params passed in the URL on the client-side.
// pages/shop.js
import { useRouter } from 'next/router'
const ShopPage = () => {
const router = useRouter()
console.log(router.query) // returns query params object
return (
<div>Shop Page</div>
)
}
export default ShopPage
If a page does not have data fetching methods, router.query will be an empty object on the page's first load, when the page gets pre-generated on the server.
From the next/router documentation:
query: Object - The query string parsed to an object. It will be
an empty object during prerendering if the page doesn't have data
fetching
requirements.
Defaults to {}
As #zg10 mentioned in his answer, you can solve this by using the router.isReady property in a useEffect's dependencies array.
From the next/router object documentation:
isReady: boolean - Whether the router fields are updated
client-side and ready for use. Should only be used inside of
useEffect methods and not for conditionally rendering on the server.
you don't have access to the query string (?a=b) for SSG (which is static content - always the same - executed only on build time).
But if you have to use query string variables then you can:
still statically pre-render content on build time (SSG) or on the fly (ISR) and handle this route by rewrite (next.config.js or middleware)
use SSR
use CSR (can also use SWR)

Error: getStaticPaths is required for dynamic SSG pages and is missing for "xxx". NextJS

I am getting this error "Error: getStaticPaths is required for dynamic SSG pages and is missing for 'xxx'" when I try to create my page in NextJS.
I don't want to generate any static page on build time. So why do I need to create a 'getStaticPaths' function?
If you are creating a dynamic page eg: product/[slug].tsx then even if you don't want to create any page on build time you need to create a getStaticPaths method to set the fallback property and let NextJS know what to do when the page you are trying to get doesn't exist.
export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
return {
paths: [], //indicates that no page needs be created at build time
fallback: 'blocking' //indicates the type of fallback
}
}
getStaticPaths does mainly two things:
Indicate which paths should be created on build time (returning a paths array)
Indicate what to do when a certain page eg: "product/myProduct123" doesn't exist in the NextJS Cache (returning a fallback type)
Dynamic Routing Next Js
pages/users/[id].js
import React from 'react'
const User = ({ user }) => {
return (
<div className="row">
<div className="col-md-6 offset-md-3">
<div className="card">
<div className="card-body text-center">
<h3>{user.name}</h3>
<p>Email: {user.email} </p>
</div>
</div>
</div>
</div>
)
}
export async function getStaticPaths() {
const res = await fetch('https://jsonplaceholder.typicode.com/users')
const users = await res.json()
const paths = users.map((user) => ({
params: { id: user.id.toString() },
}))
return { paths, fallback: false }
}
export async function getStaticProps({ params }) {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${params.id}`)
const user = await res.json()
return { props: { user } }
}
export default User
For rendering dynamic route use getServerSideProps() instead of getStaticProps()
For Example:
export async function getServerSideProps({
locale,
}: GetServerSidePropsContext): Promise<GetServerSidePropsResult<Record<string, unknown>>> {
return {
props: {
...(await serverSideTranslations(locale || 'de', ['common', 'employees'], nextI18nextConfig)),
},
}
}
You can check here as well
if you are using getStaticPaths, you are telling next.js that you want to pregenerate that page. However since you used it inside a dynamic page, next.js does not know in advance how many pages it has to create.
with getStaticPaths, we fetch the database. If we are rendering blogs, we fetch the database to decide how many blogs we have, what would be idOfBlogPost and then based on this information, getStaticPath will pre-generate pages.
also, getStaticProps does not run only during the build time. If you add revalidate:numberOfSeconds, next.js will recreate new page with fresh data after "numberOfSeconds" time.
You're rendering a dynamic route so use getServerSideProps() instead of getStaticProps()

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.

Resources