Today, in my side-project, I got a problem relate to setup getStaticPaths of multi-locale dynamic pages in Next.js. I researched and find out that there are so many people stuck in this problems.
I have created a dynamic page [slug].js to handle all dynamic datas I got from a database. And my website I was working on is also multi-language website which is using next-translate for handle i18n.
In [slug].js, we have to setup a function getStaticPaths to handle all static url. It will be more easier if the website got 1 language, but with more than 2 languages we have to loop it.
Here is the code I have been used to handle it, I was working with Notion API and use it as a database for a multi-language website:
export async function getStaticPaths({ locales }) {
const notion = new Client({ auth: process.env.NOTION_API_OFFICIAL_KEYS });
const databaseId = process.env.NOTION_PAGE_ID_EMBVN_DATABASE_PAGE;
const response = await notion.databases.query({
database_id: databaseId,
});
let paths = [];
response.results.forEach((block) => {
for (const locale of locales) {
paths.push({
params: {
slug: block.properties.embcode.title[0].plain_text.toString(),
},
locale,
});
}
});
return {
paths,
fallback: false,
};
}
With forEach, we will add every pathName of each locale to paths array to return it in the final result of getStaticPaths.
If the answer help someone, I have reshaped a little the answer of Huu Phong
First i get all my post from sanity API then i map on it to get each element , then i map on each element to get only the slug and add on each slug a locale key
export async function getStaticPaths({ locales }) {
const query = encodeURIComponent(`{
"posts": *[_type == "post"] {
...,
categories[]->
}}
`)
const url = `https://${process.env.NEXT_PUBLIC_PROJECT_API}.api.sanity.io/v1/data/query/production?query=${query}`
const result = await fetch(url).then((res) => res.json())
const paths = []
result.result.posts.map((element) => {
return locales.map((locale) => {
return paths.push({
params: { slug: `${element.slug.current}` },
locale,
})
})
})
console.log("***---paths---***", paths)
return {
paths,
fallback: true,
}
}
Related
I am trying to use Nextjs 13's generateStaticParams to render static data into my web page. How do I generate the list of routes like /pokemon/[pokemonName] for every pokemon name using the generateStaticParams without providing hard-coded pokemon name (for example here I put "charmander" as argument for fetchData and then it generate route just for charmander)?
export const generateStaticParams = async (): Promise<PageParams[]> => {
const res = await fetchData("charmander");
return [
{
pokemonName: res?.data.pokemon.name,
},
];
};
Fetching pokemon moves from graphQL-pokeAPI:
const fetchData = async (pokemonName: string) => {
const POKEMON_MOVES = `
query pokemon($name: String!) {
pokemon(name: $name) {
id
name
sprites {
front_default
}
moves {
move {
name
}
}
}
}
`;
const res = await fetch("https://graphql-pokeapi.graphcdn.app/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: POKEMON_MOVES,
variables: { name: pokemonName },
}),
});
const moves = await res.json();
return moves;
};
export default function SpecificPokemon({ params }) {
const { pokemonName } = params;
const pokemonData = use(fetchData(pokemonName));
return (<h1>{pokemonName}</h1>...)
};
In the Nextjs13 beta docs, it said that generateStaticParams doesn't require any context parameters. So I can't pass pokemonName into generateStaticParams like this, right?
export const generateStaticParams = async (pokemonName: string) => {
const res = await fetchData(pokemonName);
I tried to just write fetchData("") and the page just blank. Also, it would be too many to write like this:
return [
{ pokemonName: "bulbasaur" },
{ pokemonName: "ivysaur" },
{ pokemonName: "venusaur" },
];
Also, is this due to my lack of JS / Next13 concept understanding?
They actually have a feature called getStaticPaths which was specifically designed for this. You can easily make a call to a pokemon api and parse the response into getStaticPaths at run to generate the static paths you want to use. I encourage you to play with the documentation more, that team did a killer job with them.
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({
slug: post.slug,
})
);
}
Here's an example for a posts table - but basically you get all the posts or in your case Pokemon, and map the names to an array.
I'm not sure if there's a better way of handling it but that should point you in the right direction. I'm still reading up on NextJS and especially the new Beta myself, so take with a pinch of salt.
I am trying to generate my blog details page but next keeps giving me the 404 page. I am fetching the data from a headless cms called contentful. Here is what my file structure and code looks like
And this is my [path]s.js page
import {createClient} from "contentful"
const client = createClient({
space: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
accessToken: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
});
export const getStaticPaths = async () => {
const res = await client.getEntries({
content_type: "blog"
})
const paths = res.items.map(item => {
return {
params: {slug: item.fields.slug}
}
})
return {
paths,
fallback: false
}
}
export const getStaticProps = async ({params}) => {
const {items} = await client.getEntries({
content_type: "blog",
"fields.slug": params.slug
})
return {
props: {blog: items[0]}
}
}
function blogDetails({blog}) {
console.log(blog)
return (
<div>
Blog details
</div>
)
}
export default blogDetails
I do not understand where I am going wrong. What could be my problem?.
Firstly, you cannot use the square brackets as a "wildcard" at the beginning of the path and add some other characters after. So you should rename your file as just [path].js (without the s after [path]).
Secondly, in your getStaticPaths function, when you return the object of params inside paths, name the keys as the dynamic path you've named your file as. In your case, the object you return should be params: {path: item.fields.slug}. Alternatively, you can rename your file as [slug].js and leave the code like it is.
// [path].js
export const getStaticPaths = async () => {
const res = await client.getEntries({
content_type: "blog"
})
const paths = res.items.map(item => {
return {
params: {path: item.fields.slug} //renamed the key as "path"
}
})
return {
paths,
fallback: false
}
}
You can read more about dynamic routes and getStaticPaths() in the official documentation.
I am building a Next.js app with internationalization using next-i18next. Pages are generated for all the pages of my site for both English and French, except for pages with dynamic routes: (i.e., blog/[id]/[blog-title]). For pages with dynamic routes, pages are generated for English, but not for French.
I should note that the blog entries are the same in both languages. So if the user click on a blog entry in the list, they will get the same blog entry.
When a French language user goes to a page with a dynamic route they get a 404. I am new to React and Next so I could be doing something dumb here.
// next-i18next.config.js
module.exports = {
i18n: {
locales: ['en', 'fr'],
defaultLocale: 'en',
localeDetection: true,
},
}
//
// blog\[id]\[title]
//
export async function getStaticPaths() {
const response = await axios.get('https://api.myappi.com/blog')
const posts = response.data
const paths = posts.map((post: Props) => ({
params: { id: post.Id, title: post.Title },
}))
return { paths, fallback: false }
}
export async function getStaticProps(props: IStaticProps) {
const { id, locale } = props.params
const response = await axios.get(`https://api.myappi.com/blog/${id}`)
const post = await response.data
if (!post) {
return {
notFound: true,
}
}
return {
props: {
Id: post.Id,
Title: post.Title,
Blog: post.Blog,
DatePosted: post.DatePosted,
PostedBy: post.PostedBy,
...(await serverSideTranslations(props.locale, ['common', 'blog']))
}
}
}
For dynamic routes, you have to explicitly return the locales you want to be pre-generated from the getStaticPaths function. If you don't, Next.js will only generate pages for the default locale.
From Internationalized Routing documentation:
For pages using getStaticProps with Dynamic Routes, all locale
variants of the page desired to be prerendered need to be returned
from getStaticPaths. Along with the params object returned for
paths, you can also return a locale field specifying which locale
you want to render.
This can be achieved by modifying your getStaticPaths function to generate a path for each slug/locale combination.
export async function getStaticPaths({ locales }) { // Get available locales from `context`
const response = await axios.get('https://api.myappi.com/blog')
const posts = response.data
const paths = posts
.map((post: Props) => locales.map((locale) => ({
params: { id: post.Id, title: post.Title },
locale // Pass locale here
})))
.flat() // Flatten array to avoid nested arrays
return { paths, fallback: false }
}
So I have this blog, with dynamic url for posts (blog/[postId]). Posts are being added through keystone.js cms. It worked fine via CSR, but i came to a necessity to add proper meta open graph tags for each post (for correct share snippets).
I added getStaticProps and getStaticPaths to my post page, and now my meta tags are working fine, but now i get 404 when trying to access any post, that was added after build. Meaning, now i have to rebuild after each new post. My question is - is it possible to set Next to render component as CSR, if it fails to find path for it with getStaticPaths? I've tried to use try catch, but that didn't work. Now i came up with this concept of using container, but it does not seem to be the right way to use fallback. Or is it? I am fairly new to Next, so i am a bit confused.
const PostContainer = (props) => {
const [postData, setPostData] = React.useState(null);
const router = useRouter();
React.useEffect(() => {
if (router.query?.id) {
getPost();
}
}, [router.query]);
const getPost = async () => {
const postData = await getPost(`${router.query.id}`);
if (postData) {
setPostData(postData);
}
};
if (router.isFallback) {
return <Post data={postData}
metaProps={defaultMetaProps}
/>
}
return (
<Post data={postData}
metaProps={props}
/>
);
};
export const getStaticPaths = async () => {
const list = await getPosts();
if (list?.length) {
const paths = list.map((post) => {
return {
params: {
id: post.id,
},
};
});
return {
paths,
fallback: true,
};
}
return {
paths: [],
fallback: true,
};
};
export const getStaticProps = async ({ params }) => {
const { id } = params;
const post = await getPost(id);
const {title, description, previewImage} = post
const props = {
props: {
title,
description,
url: `https://sitename./blog/${id}`,
image: previewImage,
},
revalidate: 1,
};
return props
};
export default PostContainer;
And one more thing - do i understand correctly that if fallback option is true, it takes only one visit to the page by ANY user, after which new path is generated on the server permanently and is accessible for anyone who comes next, therefore fallback always fires just once for this first visit of first user?
I have a little problem with the ISR. I have the revalidate prop equal 1s like here
export async function getStaticProps({ params }) {
const data = await client.getEntries({
content_type: "product",
"fields.name": params.slug,
});
if (!data.items[0]) {
return {
notFound: true,
};
}
return {
props: {
article: data.items[0],
revalidate: 1,
},
};
}
When I create product in Contentful, the page is created as I expected. When I want to entry to page that doesn't exist I get 404 error as expected. Problem starts when I change something in Contentful in existing product or I delete it.
When I delete the product in Contentful, the list of products in the products page is updated and the product disappears but I can still entry in page of that product. Also when I rename the product name the list of products is updated but I still can entry to the earlier page name.
Is there any solution to solve this problem?
getStaticPaths
export async function getStaticPaths() {
const data = await client.getEntries({
content_type: "product",
});
return {
paths: data.items.map((item) => ({
params: { slug: item.fields.name },
})),
fallback: true,
};
}
Product page
const Article = ({ article }) => {
const router = useRouter();
if (router.isFallback) return <p>Loading...</p>;
return (
<div>
<h1>Hello! {article.fields.name}</h1>
<Link href="/about">
<a>Back to about!</a>
</Link>
</div>
);
};
EDIT
When I change product name from "product77" to "product7" in Contentful after revalidate static page in my build for product77 still exist and I still can entry to that page.
Shouldn't it be removed after revalidation?
Removed pages can't be deleted, but served with a 404 instead using On-demand Revalidation, if getStaticPaths and getStaticProps are configured accordingly.
// /pages/blog/[...post].jsx
function Post({ postData }) {
return (
// ...
);
}
export async function getStaticPaths() {
const res = await fetch("https://.../posts");
const posts = await res.json();
// Prerender paths during build
const paths = posts.map((post) => ({
params: { post: post.id },
}));
return { paths, fallback: "blocking" };
}
export async function getStaticProps(context) {
const res = await fetch(`https://.../posts/${context.params.post}`);
const postData = await res.json();
if(!postData){
// The page doesn't exist in the CMS, return notFound to trigger a 404
return{
notFound: true,
revalidate: 30
}
}
// Return page props
return {
props: {
postData,
},
revalidate: 30,
};
}
The main takeaway from the above code is:
You can pre-render paths in getStaticPaths
getStaticPaths must use "blocking" or true as the fallback value
Handle unknown paths inside getStaticProps (thanks to fallback). If the page doesn't exist in the CMS, return notFound: true triggering the 404 page
Then, using the On-demand revalidation approach in a webhook:
// /pages/api/revalidate-post.js
export async function handler(req, res) {
try {
const reqBody = JSON.parse(req.body);
await res.revalidate(`/${reqBody.path}`);
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).send("Error revalidating");
}
}
The res.revalidate(/${reqBody.path}) invokes a fresh evaluation of a page using the logic from getStaticProps.
Now, if somebody would delete the page in the CMS, and trigger the above revalidation webhook handler for the deleted page path, then the path serves a 404 page instead of the original page.
The page itself, however, is not deleted from the disk.