I have searched many articles and posts regarding this topic, but they don't seem to make a lot of sense to me. I am new to Nextjs and dynamic routes in general, so I'm hoping someone can explain this clearly to me for my situation.
My page structure looks something like below. And what I want to do is combine the paths into one. So I would have something like this:
-Page
--Category
---[slug].js
--Post
---[slug].js
I understand how this structure works. I would end up with two different routes, one that would be domain/category/[slug], and one that would be domain/post/[slug].
This is not what I want. I want it to be domain/category/[slug]/post/[slug].
I have also tried nesting the folder structure like below, though I'm not sure if that would work, or if I'm just not doing is correctly. Not to mention many variations and structures. I'm tired of beating my head against the wall.
-Page
--Category
---index.js
---[slug].js
---Post
----[slug].js
I'm fairly certain that the answer lies with using a catch all like [...params], I'm just not sure of the correct way to do, that makes sense to me.
An example of the code I'm using can be found here:https://github.com/adrianhajdin/project_graphql_blog/tree/main/pages
My structure is exactly the same, just folder and names may be different.
Edit: I should also note, I have no issue with changing the current site folder structure to accomplish this. I just don't want to change my CMS structure I currently have set up.
Edit 2: Based on feedback from @maxeth my page/[category]/index.js page looks like this minus the HTML:
export async function getStaticProps({ params }) {
const posts = await getCategoryPost(params.category);
return {
props: { posts },
revalidate: 60 * 20,
};
}
export async function getStaticPaths() {
const categories = await getCategories();
return {
paths: categories.map((el) => ({ params: { category: el } })),
fallback: false,
};
}
Although I'm not sure if getStaticProps/const posts = await getCategoryPost(params.category)
is correct. Should it be (params.category)
?
It should also be noted that getCategoryPost and getCategories queries are being imported if that wasn't obvious already. I'm sure it is though. They can be found here:
https://github.com/adrianhajdin/project_graphql_blog/blob/main/services/index.js
I'm not sure if the slug in the CMS also needs to be renamed to reflect the [category]
directory. Currently, it's just named Slug.
Edit 3: The path up through pages/category/[category]
works fine. It's when I get to pages/category/[category]/post/[slug]
that I get a 404.
Post/[slug] getstaticprops and getstaticpaths looks like the following:
export async function getStaticProps({ params }) {
const data = await getPostDetails(`${params.category}/${params.slug}`);
return {
props: {
post: data,
},
};
}
export async function getStaticPaths() {
const posts = await getPosts();
return {
paths: posts.map(({ node: { slug } }) => ({
params: { category: slug.category, slug: slug.slug },
})),
fallback: true,
};
}
The Post queries look like this:
export const getPosts = async () => {
const query = gql`
query MyQuery {
postsConnection(orderBy: createdAt_DESC) {
edges {
cursor
node {
content {
html
raw
text
}
author {
bio
name
id
photo {
url
}
}
createdAt
slug
title
excerpt
featuredImage {
url
}
categories {
name
slug
}
}
}
}
}
`;
const result = await request(graphqlAPI, query);
return result.postsConnection.edges;
};
export const getPostDetails = async (slug) => {
const query = gql`
query GetPostDetails($slug: String!) {
post(where: { slug: $slug }) {
content {
html
text
raw
}
title
excerpt
featuredImage {
url
}
author {
name
bio
photo {
url
}
}
createdAt
slug
content {
html
raw
text
}
categories {
name
slug
}
}
}
`;
const result = await request(graphqlAPI, query, { slug });
return result.post;
};
What the heck am I missing? It has to be something really simple. I'm just not smart enough to see it.
Edit 4: The 404 was caused by a casing mistake. Where I was typing /post/[slug].js instead of /Post/, since my directory was named Post.
You can achieve this with the following structure:
└── pages
└── category
├── index.js // this is the "overview page" of all categories
└── [category]
└── index.js // this is the "overview page" of a specific category
└── post
└── [slug].js // this is an actual article page for that specific category
Note that you cannot use the dynamic-route name [slug]
twice inside the folder category
, because we will have to return objects with the path names as keys, e.g. [category] -> {category: ...}, [slug] -> {slug: ...}, so they have to be unique for next to tell for which folder you are returning the slugs inside getStaticPaths
.
Now if you use getStaticPaths
, you need to return the specific slug for the page, as well as the "slugs" of the potential parent page inside params: {...}
in order for the pages to be pre-generated.
So inside category/[category]/index.js
, you would need to do something like this:
export async function getStaticPaths(context){
// [...] fetch all categories from CMS
const allCategories = await getAllCategoriesFromCMS();
// allCategories includes all of the categories from your CMS:
// e.g. ["category-A", "category-B", "category-C", ...]
return {
paths: allCategories.map(el => ({ params: { category: el } }))
};
}
Which is then accessible within getStaticProps
like this:
export async function getStaticProps({params}){
console.log(params.category) // can be any of the dynamic categories you fetched and returned inside getStaticPaths
}
...
And the same concept applies to category/[category]/post/[slug].js
, just that you now have to return/handle 2 "slugs" [category]
and [slug]
inside getStaticPaths
and getStaticProps
:
export async function getStaticPaths(context){
// [...] fetch all posts and the specific categories they belong to from CMS
const allPosts = await getAllPostsFromCMS();
// allPosts includes all of the posts/articles from your CMS, assuming the posts have a relationship with their respective category:
/* e.g. [
{category: "category-A", postSlug:"some-article-for-category-A", author: "some author", articleContent: "..."},
{category: "category-B", postSlug:"some-article-for-category-B", author: "some author", articleContent: "..."},
]
*/
return {
paths: [ // once again, we need to call the keys 'category' and 'slug' because we called the dynamic routes '[category]' and '[slug].js'
allPosts.map(el => ({ params: { category: el.category, slug: el.postSlug } }))
],
};
}
and in getStaticProps
:
export async function getStaticProps({ params }) {
console.log(params);
// You are guaranteed to have matching categories and slugs in here, for example
// {category: category-A, slug:some-article-for-category-A}
// or {category: category-B, slug:some-article-for-category-B}
// You can use these params to fetch the article entry from the CMS
const article = await fetchFromCMS(`${params.category}/${params.slug}`);
// [...]
}
Now, for example, one of the articles will be accessible under localhost:3000/category/category-A/post/some-post-for-category-A