/blog?page=1, /blog?page=2, /blog?page=3 như ba trang hoàn toàn khác nhau với cùng nội dung — duplicate content điển hình.
Metadata là thứ bạn không thấy khi dùng app, nhưng Google, Facebook và mọi crawler khác lại nhìn vào đó đầu tiên. Bài viết này đi qua toàn bộ những gì Next.js cung cấp cho metadata, từ cơ bản đến những chi tiết ít ai chú ý.
Metadata mặc định: đặt một lần, áp cho toàn bộ app
Điểm xuất phát làapp/layout.tsx. Export một object metadata ở đây và nó sẽ được áp dụng cho tất cả các trang trong app, trừ khi một trang nào đó tự override lại.
app/layout.tsx
Metadata tĩnh — override cho từng trang cụ thể
Mỗipage.tsx hoặc layout.tsx đều có thể export metadata riêng để ghi đè lên metadata của parent.
app/about/page.tsx
Title Template: trick bạn cần biết
Thay vì mỗi trang phải tự ghi tên thương hiệu"Tiêu đề trang trong | Marketer Vietnam", bạn có thể khai báo một template ở root layout:
app/layout.tsx
app/about/page.tsx
%s là placeholder — Next.js tự điền title của trang con vào đó. default là fallback dùng khi trang con không khai báo title (ví dụ trang chủ).
Tuy nhiên, đôi khi bạn muốn một trang nào đó bỏ qua template, ví dụ landing page campaign với title riêng hoàn toàn. Dùng absolute:
Metadata động: khi title phụ thuộc vào data
Với các trang như chi tiết bài viết, title và description phải được lấy từ database tùy theo slug. Next.js có hàmgenerateMetadata cho việc này:
app/post/[slug]/page.tsx
cache()?
Vì cả generateMetadata và component page đều cần gọi getPost với cùng slug. Nếu không cache, Next.js sẽ gọi DB hai lần trong một request. react/cache đảm bảo lần gọi thứ hai trả về kết quả đã có trong bộ nhớ, không tốn thêm một query nào.
Một lưu ý khác: bạn không thể vừa export metadata object vừa export generateMetadata trong cùng một file — chỉ được dùng một trong hai.
generateMetadata chạy hoàn toàn trên server, giống như một Server Component — nó có thể await, truy cập database, đọc cookies, nhưng không bao giờ expose code hay data xuống client.
OG Image: ảnh preview khi share link
Ảnh tĩnh cho toàn site
Đặt fileopengraph-image.jpg (hoặc .png) vào thư mục app/. Next.js tự nhận và inject vào thẻ <meta property="og:image"> mà không cần config gì thêm.
Ảnh theo từng route
Đặtopengraph-image.jpg vào thư mục của route đó. Ví dụ ảnh riêng cho trang blog:
Ảnh dynamic sinh từ tiêu đề bài viết
Đây là tính năng rất hay mà không phải ai cũng biết. Tạo fileopengraph-image.tsx trong thư mục route động:
app/post/[slug]/opengraph-image.tsx
/post/[slug]/opengraph-image). Mỗi bài viết có ảnh OG riêng, sinh tự động từ tiêu đề — không cần Figma, không cần upload tay.
Canonical URL: quan trọng hơn bạn nghĩ
Nếu bạn không khai báo canonical, Next.js sẽ không tự inject thẻ<link rel="canonical">. Google sẽ tự suy luận, nhưng đó là rủi ro — nhất là khi bạn có phân trang hay query params.
/blog?page=2, /blog?page=3), canonical nên trỏ về /blog — trang “gốc” không có query string. Nếu không, Google có thể coi mỗi trang là một URL riêng biệt với nội dung trùng lặp.
metadataBase cần được khai báo ở root layout để Next.js biết domain của bạn, từ đó resolve các path tương đối thành URL đầy đủ trong các thẻ meta.
Extend metadata từ parent — và tại sao bạn nên làm vậy
Khi khai báo OG image tronggenerateMetadata, có một pattern thường bị bỏ qua:
...previousImages thay vì ghi đè hoàn toàn? Vì parent layout có thể đã khai báo ảnh OG mặc định (logo site, ảnh fallback). Bằng cách spread previousImages vào sau, bạn đặt ảnh bài viết lên đầu (crawler lấy ảnh đầu tiên) nhưng vẫn giữ lại fallback nếu ảnh bài viết không tải được.
Robots — kiểm soát crawler
max-image-preview: "large" cho phép Google dùng ảnh full-size trong kết quả tìm kiếm. max-snippet: -1 có nghĩa là không giới hạn độ dài đoạn trích. Hai field này ít người để ý nhưng ảnh hưởng trực tiếp đến cách bài viết hiển thị trên Google Search.
Tóm tắt
| Tình huống | Dùng gì |
|---|---|
| Title mặc định cho toàn app | metadata trong app/layout.tsx |
| Append tên site vào title | title.template |
| Title không dùng template | title.absolute |
| Title/description từ database | generateMetadata + cache() |
| Ảnh OG mặc định | opengraph-image.jpg trong app/ |
| Ảnh OG sinh tự động từ data | opengraph-image.tsx |
| Chống duplicate content | alternates.canonical |
| Kiểm soát hiển thị trên Google | robots.googleBot |
app/sitemap.ts — cả hai đều được Next.js App Router hỗ trợ native.