Trong thế giới phát triển web hiện đại, việc tạo ra các trang có nội dung thay đổi linh hoạt dựa trên dữ liệu là một yêu cầu tất yếu. Bạn không thể tạo thủ công hàng ngàn trang cho sản phẩm, bài viết blog, hay hồ sơ người dùng. Đây chính là lúc Dynamic Routes (Định tuyến động) trong Next.js tỏa sáng, mang đến một giải pháp mạnh mẽ, linh hoạt và tối ưu cho SEO.
Bài viết này sẽ dẫn bạn từ những khái niệm cơ bản nhất đến các kỹ thuật nâng cao, giúp bạn hoàn toàn làm chủ Dynamic Routes trong dự án Next.js của mình.
Dynamic Routes là gì? Tại sao nó lại quan trọng?
Hãy tưởng tượng bạn đang xây dựng một trang thương mại điện tử. Thay vì phải tạo ra từng file riêng biệt cho mỗi sản phẩm (san-pham-1.js
, san-pham-2.js
,...), bạn chỉ cần tạo một "khuôn mẫu" trang duy nhất và nội dung của nó sẽ tự động thay đổi dựa trên ID hoặc "slug" (chuỗi định danh thân thiện với URL) của sản phẩm.
Đó chính là sức mạnh của Dynamic Routes.
Dynamic Routes là các tuyến đường (URL) không được định nghĩa trước một cách cứng nhắc, mà được tạo ra một cách linh hoạt dựa trên dữ liệu - ví dụ như ID sản phẩm, tiêu đề bài viết, hoặc tên người dùng.
Lợi ích chính:
- Tái sử dụng component: Chỉ cần một layout trang cho vô số các trang con có cấu trúc tương tự.
- Dễ dàng quản lý: Không cần phải xử lý hàng trăm, hàng ngàn file riêng lẻ.
- URL thân thiện với SEO: Tạo ra các URL có ý nghĩa, dễ đọc (ví dụ:
/blog/cach-su-dung-nextjs
thay vì/blog?id=123
), giúp cải thiện thứ hạng trên các công cụ tìm kiếm. - Linh hoạt với dữ liệu: Dễ dàng hiển thị nội dung từ bất kỳ nguồn dữ liệu nào (API, database, file markdown,...).
Cú pháp và Cách hoạt động trong App Router
Với sự ra đời của App Router, cách định nghĩa Dynamic Routes trở nên trực quan hơn bao giờ hết, dựa trên cấu trúc thư mục.
1. Phân đoạn động cơ bản (Basic Dynamic Segments)
Để tạo một route động, bạn chỉ cần đặt tên thư mục trong dấu ngoặc vuông: [folderName]
. Tên bên trong dấu ngoặc vuông sẽ trở thành tham số (parameter) cho route của bạn.
Ví dụ: Tạo một trang chi tiết cho bài blog.
Cấu trúc thư mục của bạn sẽ như sau:
app/
└── blog/
└── [slug]/
└── page.js
- Với cấu trúc này, bất kỳ URL nào có dạng
/blog/bai-viet-a
,/blog/nextjs-la-gi
, hay/blog/123
đều sẽ được xử lý bởi fileapp/blog/[slug]/page.js
. - Giá trị "bai-viet-a", "nextjs-la-gi", hay "123" chính là tham số
slug
.
Làm thế nào để lấy giá trị của tham số?
Trong page.js
(là một Server Component mặc định), Next.js sẽ tự động truyền các tham số của route vào props của component.
// app/blog/[slug]/page.js
// Hàm fetch dữ liệu ví dụ
async function getPostData(slug) {
// Giả sử bạn fetch dữ liệu từ một API
const res = await fetch(`https://api.example.com/posts/${slug}`)
if (!res.ok) return { title: 'Bài viết không tồn tại', content: '' }
return res.json()
}
export default async function BlogPostPage({ params }) {
// params sẽ là một object: { slug: 'bai-viet-a' }
const { slug } = params
const post = await getPostData(slug)
return (
<div>
<h1>{post.title}</h1>
<p>
Đây là nội dung cho bài viết có slug là: <strong>{slug}</strong>
</p>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</div>
)
}
2. "Bắt tất cả" các phân đoạn (Catch-all Segments)
Nếu bạn muốn một route có thể "bắt" được nhiều phân đoạn URL hơn? Ví dụ như một trang tài liệu có cấu trúc /docs/getting-started/installation
.
Hãy sử dụng cú pháp "spread" bên trong dấu ngoặc vuông: [...folderName]
.
Ví dụ:
Cấu trúc thư mục:
app/
└── docs/
└── [...slug]/
└── page.js
- Route này sẽ khớp với
/docs/a
,/docs/a/b
,/docs/a/b/c
, và cứ thế tiếp tục. - Tham số
slug
giờ đây sẽ là một mảng các phân đoạn.
Cách lấy giá trị:
// app/docs/[...slug]/page.js
export default function DocsPage({ params }) {
// Nếu URL là /docs/getting-started/installation
// params sẽ là: { slug: ['getting-started', 'installation'] }
const { slug } = params
return (
<div>
<h1>Tài liệu cho: {slug.join(' / ')}</h1>
{/* Logic để render nội dung dựa trên mảng slug */}
</div>
)
}
3. Phân đoạn "Bắt tất cả" tùy chọn (Optional Catch-all Segments)
Một biến thể của Catch-all là Optional Catch-all, cho phép route đó khớp cả với URL gốc (không có tham số). Cú pháp là sử dụng hai dấu ngoặc vuông: [[...folderName]]
.
Ví dụ:
Cấu trúc thư mục:
app/
└── shop/
└── [[...filters]]/
└── page.js
- Route này sẽ khớp với
/shop
(không có tham số). - Và nó cũng khớp với
/shop/laptops
,/shop/laptops/apple
,...
Cách lấy giá trị:
// app/shop/[[...filters]]/page.js
export default function ShopPage({ params }) {
// Nếu URL là /shop, params sẽ là: {}
// Nếu URL là /shop/laptops/apple, params sẽ là: { filters: ['laptops', 'apple'] }
const { filters } = params
return (
<div>
{filters ? (
<h1>Đang lọc sản phẩm theo: {filters.join(' > ')}</h1>
) : (
<h1>Hiển thị tất cả sản phẩm</h1>
)}
</div>
)
}
Tối ưu hóa hiệu năng với generateStaticParams
Một trong những tính năng mạnh mẽ nhất của Next.js là khả năng tạo trang tĩnh (Static Site Generation - SSG). Với Dynamic Routes, bạn có thể tạo trước các trang tĩnh tại thời điểm xây dựng (build time) cho tất cả các tham số có thể có. Điều này giúp trang web của bạn tải nhanh như chớp và giảm tải cho server.
Hàm generateStaticParams
cho phép bạn làm điều đó.
Cách hoạt động:
- Trong file
page.js
của route động, bạn export một hàmasync
tên làgenerateStaticParams
. - Hàm này sẽ trả về một mảng các object, trong đó mỗi object đại diện cho các
params
của một trang cần được tạo tĩnh.
Ví dụ: Tạo tĩnh tất cả các trang blog.
// app/blog/[slug]/page.js
// 1. Fetch tất cả các slug bài viết
async function getAllPostSlugs() {
const res = await fetch('https://api.example.com/posts')
const posts = await res.json()
// Trả về định dạng mà generateStaticParams yêu cầu
return posts.map((post) => ({
slug: post.slug,
}))
}
// 2. Export hàm generateStaticParams
export async function generateStaticParams() {
const slugs = await getAllPostSlugs()
return slugs
}
// Hàm fetch dữ liệu và component Page giữ nguyên như ví dụ đầu tiên
async function getPostData(slug) {
/* ... */
}
export default async function BlogPostPage({ params }) {
/* ... */
}
Khi bạn chạy npm run build
, Next.js sẽ:
- Gọi hàm
generateStaticParams
. - Nhận được danh sách
[{ slug: 'bai-viet-a' }, { slug: 'bai-viet-b' }]
. - Lần lượt render các trang
/blog/bai-viet-a
và/blog/bai-viet-b
thành các file HTML tĩnh.
Khi người dùng truy cập vào các URL này, họ sẽ nhận được file HTML ngay lập tức mà không cần chờ server render, mang lại trải nghiệm tốc độ vượt trội.
Lưu ý: Nếu người dùng truy cập một slug
không được tạo ra bởi generateStaticParams
, hành vi mặc định của Next.js là sẽ render trang đó theo yêu cầu (on-demand), giống như một Server Component thông thường.
Kết luận: Sức mạnh nằm trong tay bạn
Dynamic Routes trong Next.js không chỉ là một tính năng kỹ thuật, mà là một công cụ nền tảng cho phép bạn xây dựng các ứng dụng web phức tạp, giàu dữ liệu một cách hiệu quả và có khả năng mở rộng. Bằng cách kết hợp cấu trúc thư mục trực quan với sức mạnh của Server Components và khả năng tạo trang tĩnh qua generateStaticParams
, bạn có thể tạo ra những trải nghiệm người dùng nhanh chóng, mượt mà và được tối ưu hóa tốt nhất cho SEO.
Hy vọng rằng qua bài viết này, bạn đã có một cái nhìn sâu sắc và toàn diện về Dynamic Routes. Hãy bắt đầu áp dụng vào dự án Next.js tiếp theo của mình và khám phá những khả năng vô tận mà nó mang lại!