Like in my last lesson, there will be a reference image of my file structure and file content so you can compare at the end.
Adding links to each post
Inside of your /pages/index.js
file, we need to add links to each post that will take us to that specific post's page.
Start by adding NextJS's Link feature.
import Link from "next/link";
Now inside of our postsElements
variable, wrap the entire element inside of a Link tag.
const postsElements = posts.map((post, index) => (
<Link key={index} href={`/posts/[slug]`} as={`/posts/${post.slug}`}>
<div>
<p>{new Date(post.publishedAt).toDateString().slice(4)}</p>
<h3>{post.title}</h3>
</div>
</Link>
));
A few things are happening here.
We moved our
key={index}
from our div to ourLink
because it is now our outer most element.There is an
href
attribute which you should be familiar with. This tells the browser where to go to when the element is clicked. I have it taking us to a/posts/[slug]
file that we will create soon.There is also an
as
attribute. This is a cosmetic attribute that turns our url into one that displays theslug
value we added to each post. This will be used later to search for specific posts.
That's all the code needed to allow us to link to a post's page when clicked. Now it's time to add that page in our project.
Displaying a single post
It is time for us to create the file I just mentioned.
Add a folder named posts
in the pages folder, then add a file named [slug].js
to that new folder. Your pages
folder should now look like this.
The reason we add the brackets around our file name is so that we can reference it later.
This is called a Dynamic Route and will be referenced to multiple times in this lesson so I advise you to check out the docs for it. NextJS Dynamic Routes
Inside of our /pages/posts/[slug].js/
file we just created, we are going to add our parts one at a time.
Post component
export default function Post() {
return;
}
Standard React component. We will add the content of this component in a little bit.
getStaticProps
export async function getStaticProps({ params }) {
const query = `*[_type == "post" && slug.current == $slug] {
_id,
title,
publishedAt,
'slug': slug.current,
body
}`;
const options = { slug: params.slug };
const post = await client.fetch(query, options);
return {
props: { post: post[0] },
};
}
This is very similar to the getStaticProps we added in our /pages/index.js
file in the last lesson, but with a few additions.
-
We add our
{ params }
as an argument.- To explain this a little,
getStaticProps
has access to an argument calledcontext
. Within thiscontext
argument you have a few pieces of information to pull from. We usedestructuring
to pull out theparams
value. - Inside of this
params
object is the url associated with this file. This is why we added the brackets to[slug].js
.
- To explain this a little,
[_type == "post" && slug.current == $slug]
- Same query as before, except this time we remove our order option since we are only fetching one post, and instead we add
&& slug.current == $slug
- This will fetch an entry that has a type of
post
AND has aslug.current
(the url we provided in our post) that matches the current pages url (more on this in a bit).
- This will fetch an entry that has a type of
const options = { slug: params.slug }
- We assign the current url to an object.
- Above I mentioned how we are using
params
to get our current url and naming it ourslug
. This is where that comes into play. - The unique url for this post is inside of
params.slug
. We assign that value to the keyslug
within anoptions
object variable.
- Above I mentioned how we are using
const post = await client.fetch(query, options)
- Here we simply call our fetch like before, but now we add our
options
object we created.- This all works together because our
options
object has our url stored inside of it. - Our query matches our post's
slug.current
with this url we saved in options to fetch us the post for the page we are on.
- This all works together because our
return {
props: { post: post[0] },
};
- Finally we return our
post
. But since this fetched us an array, even though we only received 1 post, we make it easier on ourselves in our props by assigning the first (and only) object in our array topost
so we can import it.
getStaticPaths
By far the hardest section to explain. Honestly I don't think I can give this function justice in text, so I'm going to give the basics and provide links that will explain what's happening in greater detail.
I mentioned Dynamic Routes earlier when we created [slug].js
. From the NextJS getStaticProps Documentation it explains that...
If a page has Dynamic Routes and uses getStaticProps, it needs to define a list of paths to be statically generated.
Well, we have Dynamic Routes, and we used getStaticProps, so we must need this. The idea of this is NextJS will now pre-render every path that you specify inside of getStaticPaths.
export async function getStaticPaths() {
const query = `*[_type == "post"]{ 'slug': slug.current }`;
const posts = await client.fetch(query);
const paths =
posts?.map((post) => ({
params: {
slug: post.slug,
},
})) || [];
return {
paths,
fallback: false,
};
}
To see examples similar to this one you can review NextJS API-reference for getStaticPaths.
You might notice this seems very familiar to our getStaticProps
. We need to create our query that fetches every post but only returns the slug.current
value (the url we gave it), then give that query to our client.fetch
.
But what is that paths
variable? It looks more complicated than it is.
- First it checks if there are any posts in the first place, that's why we add the
?
to posts. If there isn't then we return an empty array with|| []
.- Essentially meaning
posts.map
if there are posts, if there aren't then return[]
.
- Essentially meaning
- Now inside of our
map
. For each post we create...{ params: { slug: post.slug } }
- This is fetching every single post's url and assigning it as a
param
for getStaticPaths to render at build time.
- Finally we return our
paths
and afallback: false
- Having fallback as false just means that other routes will 404 if you go to one that we didn't render in our map.
Rendering the post
We are now going to use the data from the post we fetched inside of our Post
component we created.
export default function Post({ post }) {
return (
<div>
<h1>{post.title}</h1>
<span>{new Date(post.publishedAt).toDateString()}</span>
</div>
);
}
Similar code to what we used in our homepage to display each post, but this time we are including the day of the week in the date.
You might have noticed we aren't returning the body
of our post, that is because this requires a little more effort to render (and to style) so I will be including that in the next short lesson.
Wrapping up
If you go to your homepage and click one of your posts it will take you to that post's url displaying the data of that specific post.
In the next lesson I will be showing you how to...
- Style a webpage with Styled-Components
- Style Rich Text components
We will also be including a back
button to navigate back to our homepage from a post.
References
File Structure
pages/posts/[slug].js
import client from "../../lib/sanity";
export default function Post({ post }) {
return (
<div>
<h1>{post.title}</h1>
<span>{new Date(post.publishedAt).toDateString()}</span>
</div>
);
}
export async function getStaticProps({ params }) {
const query = `*[_type == "post" && slug.current == $slug] {
_id,
title,
publishedAt,
'slug': slug.current,
body
}`;
const options = { slug: params.slug };
const post = await client.fetch(query, options);
return {
props: { post: post[0] },
};
}
export async function getStaticPaths() {
const query = `*[_type == "post"]{ 'slug': slug.current }`;
const posts = await client.fetch(query);
const paths =
posts?.map((post) => ({
params: {
slug: post.slug,
},
})) || [];
return {
paths,
fallback: false,
};
}
Top comments (0)