Routing is the most fundamental feature for any web application framework, allowing the users to navigate within the app by mapping URLs to webpages.
Next.js offers a file based routing system, where the folders and files under the app
directory automatically translates to the routes for your application.
For example, pay attention to the page.jsx
files, each of them corresponds to a webpage, and their location indicates the route to access these pages.
1app
2├── blog
3│ └── page.jsx <===== /blog
4├── dashboard
5│ └── users
6│ └── page.jsx <===== /dashboard/users
7├── favicon.ico
8├── fonts
9├── globals.css
10├── layout.js
11└── page.js <===== /
Creating your first page (page.jsx)
Before we move on, let's take a closer look at the page.jsx
. There are several special files in Next.js, and page.jsx
is one of them. As we've mentioned above, each of them defines a webpage in your application.
Each page is a React component, which means it should export a function that returns a piece of JSX code, which will then be translated into HTML during rendering.
app/blog/page.jsx
1export default function Blog() {
2 return <div>This is the blog page.</div>;
3}
And to access this page, open the browser and go to http://localhost:3001/blog
.
Dynamic routes with parameters
Next.js allows us to create dynamic routes as well.
Imagine you are creating a blog with different articles. Each of these articles corresponds to a different route. Of course, you can hardcode each page, but there is an easier way.
Instead of manually creating individual routes for each article, you can define a dynamic route to capture a segment of the URL as a parameter, such as slug
, and then use the slug
to dynamically fetch and display the content for that specific article.
In Next.js, you can use the square brackets to define dynamic routes:
1src
2├── app
3│ ├── blog
4│ │ └── [slug] <===== Captures the URL segment
5│ │ └── page.jsx <=====
6│ ├── dashboard
7│ │ └── users
8│ │ └── page.jsx
9│ ├── favicon.ico
10│ ├── fonts
11│ ├── globals.css
12│ ├── layout.js
13│ └── page.js
14└── components
With this configuration, the last segment of the URL will be captured and assigned to slug
, which will then be passed to page.jsx
.
For example, when you visit http://localhost:3001/blog/article1
, the URL segment article1
will be captured as the parameter slug
, and passed to the page.jsx
file, which can be accessed via the params
prop:
app/blog/[slug]/page.js
1export default async function BlogPost({ params }) {
2 const slug = (await params).slug;
3 return <div>Displaying Blog Post: {slug}</div>;
4}
Note that the params
prop is a promise, which means you should use the async await
syntax to access it.
This feature is recently introduced in Next.js 15, and for now, in order to maintain backward compatibility, you can still access it synchronously as a regular variable like this:
1export default function BlogPost({ params }) {
2 const slug = params.slug;
3 return <div>Displaying Blog Post: {slug}</div>;
4}
But this behavior is not recommended as it will be deprecated in the future.
Catch multiple segments with dynamic routes
In some cases, you might need to catch more than one URL segments. This can be done by creating a nested folder structure.
1app
2├── blog
3│ └── [category] <===== Captures the URL segment category
4│ └── [slug] <===== Captures the URL segment slug
5│ └── page.jsx <===== Webpage
6├── dashboard
7├── favicon.ico
8├── fonts
9├── globals.css
10├── layout.js
11└── page.js
app/blog/[category]/[slug]/page.jsx
1export default async function BlogPost({ params }) {
2 const slug = (await params).slug;
3 const category = (await params).category;
4 return (
5 <div>
6 Displaying Blog Post {slug} under category {category}.
7 </div>
8 );
9}
Both category
and slug
can be accessed using the same syntax.
You can also capture all subsequent URL segments by adding an ellipsis, which is very similar to the spread syntax in JavaScript.
1app
2├── blog
3│ └── [...slug] <===== Captures all subsequent URL segments and assign to slug
4│ └── page.jsx <===== Webpage
5├── dashboard
6├── favicon.ico
7├── fonts
8├── globals.css
9├── layout.js
10└── page.js
In this case, this route will match /blog/a
, /blog/a/b
, /blog/a/b/c
, and so on. The parameter slug
will return an array of matched URL segments.
app/blog/[...slug]/page.jsx
1export default async function BlogPost({ params }) {
2 const slug = (await params).slug;
3
4 console.log(typeof slug);
5 console.log(slug);
6
7 return <div>Displaying Blog Post {slug}.</div>;
8}
Open the browser and visit http://localhost:3001/blog/author1/category2/article3/comment4
, and the following output should be logged to the console:
1object
2[ 'author1', 'category2', 'article3', 'comment4' ]
This catch all syntax can also be made optional by wrapping the parameter name inside double square brackets.
1app
2├── blog
3│ └── [[...slug]] <===== Optional catch all, matches /blog as well
4│ └── page.jsx <===== Webpage
5├── dashboard
6├── favicon.ico
7├── fonts
8├── globals.css
9├── layout.js
10└── page.js
app/blog/[[...slug]]/page.jsx
1export default async function BlogPost({ params }) {
2 const slug = (await params).slug;
3
4 console.log(typeof slug);
5 console.log(slug);
6
7 return <div>Displaying Blog Post {slug}.</div>;
8}
The difference is that it matches /blog
as well, in which case slug
will return undefined
.
Open the browser and visit http://localhost:3001/blog/author1/category2/article3/comment4
, and the same output should be logged to the console:
1object
2[ 'author1', 'category2', 'article3', 'comment4' ]
But when you visit http://localhost:3001/blog
, the same route will be matched, and the following output will be logged:
1undefined
2undefined