Dynamic content in Next.js with the router
How to setup dynamic content in a Next.js site
In the Linking two pages in Next.js using Link post we saw how to link the home to the blog page.
A blog is a great use case for Next.js, one we’ll continue to explore in this chapter by adding blog posts.
Blog posts have a dynamic URL. For example a post titled “Hello World” might have the URL /blog/hello-world
. A post titled “My second post” might have the URL /blog/my-second-post
.
This content is dynamic, and might be taken from a database, markdown files or more.
Next.js can serve dynamic content based on a dynamic URL.
We create a dynamic URL by creating a dynamic page with the []
syntax.
How? We add a pages/blog/[id].js
file. This file will handle all the dynamic URLs under the /blog/
route, like the ones we mentioned above: /blog/hello-world
, /blog/my-second-post
and more.
In the file name, [id]
inside the square brackets means that anything that’s dynamic will be put inside the id
parameter of the query property of the router.
Ok, that’s a bit too many things at once.
What’s the router?
The router is a library provided by Next.js.
We import it from next/router
:
import { useRouter } from "next/router";
and once we have useRouter
, we instantiate the router object using:
const router = useRouter();
Once we have this router object, we can extract information from it.
In particular we can get the dynamic part of the URL in the [id].js
file by accessing router.query.id
.
So let’s go on and apply all those things in practice.
Create the file pages/blog/[id].js
:
import { useRouter } from "next/router";
export default () => {
const router = useRouter();
return (
<>
<h1>Blog post</h1>
<p>Post id: {router.query.id}</p>
</>
);
};
Now if you go to the http://localhost:3000/blog/test
router, you should see this:
We can use this id
parameter to gather the post from a list of posts. From a database, for example. To keep things simple we’ll add a posts.json
file in the project root folder:
{
"test": {
"title": "test post",
"content": "Hey some post content"
},
"second": {
"title": "second post",
"content": "Hey this is the second post content"
}
}
Now we can import it and lookup the post from the id
key:
import { useRouter } from "next/router";
import posts from "../../posts.json";
export default () => {
const router = useRouter();
const post = posts[router.query.id];
return (
<>
<h1>{post.title}</h1>
<p>{post.content}</p>
</>
);
};
Reloading the page should show us this result:
But it’s not! Instead, we get an error in the console, and an error in the browser, too:
Why? Because.. during rendering, when the component is initialized, the data is not there yet. We’ll see how to provide the data to the component with getInitialProps in the next lesson.
For now, add a little if (!post) return <p></p>
check before returning the JSX:
import { useRouter } from "next/router";
import posts from "../../posts.json";
export default () => {
const router = useRouter();
const post = posts[router.query.id];
if (!post) return <p></p>;
return (
<>
<h1>{post.title}</h1>
<p>{post.content}</p>
</>
);
};
Now things should work. Initially the component is rendered without the dynamic router.query.id
information. After rendering, Next.js triggers an update with the query value and the page displays the correct information.
And if you view source, there is that empty <p>
tag in the HTML:
We’ll soon fix this issue that fails to implement SSR and this harms both loading times for our users, SEO and social sharing as we already discussed.
We can complete the blog example by listing those posts in pages/blog.js
:
import posts from "../posts.json";
const Blog = () => (
<div>
<h1>Blog</h1>
<ul>
{Object.entries(posts).map((value, index) => {
return <li key={index}>{value[1].title}</li>;
})}
</ul>
</div>
);
export default Blog;
And we can link them to the individual post pages, by importing Link
from next/link
and using it inside the posts loop:
import Link from "next/link";
import posts from "../posts.json";
const Blog = () => (
<div>
<h1>Blog</h1>
<ul>
{Object.entries(posts).map((value, index) => {
return (
<li key={index}>
<Link href="/blog/[id]" as={"/blog/" + value[0]}>
<a>{value[1].title}</a>
</Link>
</li>
);
})}
</ul>
</div>
);
export default Blog;
→ I wrote 17 books to help you become a better developer, download them all at $0 cost by joining my newsletter
→ JOIN MY CODING BOOTCAMP, an amazing cohort course that will be a huge step up in your coding career - covering React, Next.js - next edition February 2025