Bạn deploy một trang web, share link lên Facebook — và thấy preview trống hoác, không tiêu đề, không ảnh.
Hoặc Google index trang của bạn với title “Untitled” vì bạn quên set metadata.
Những chuyện này xảy ra rất thường xuyên khi mới làm quen với Next.js.
Bài này sẽ đi qua toàn bộ hệ thống Metadata của Next.js — từ cách khai báo cơ bản, xử lý trang động, đến cách tạo OG image tự động cho từng bài viết.
Cách Next.js xử lý Metadata
Next.js tự động generate các <head> tag dựa trên metadata bạn khai báo. Có hai mặc định luôn có dù bạn không khai báo gì:
Ngoài ra, metadata hoạt động theo nguyên tắc nearest wins — Next.js lấy từ page.tsx hoặc layout.tsx gần nhất với route hiện tại. Nếu không có, leo lên cấp cha cho đến app/layout.tsx.
Static Metadata
Dành cho các trang có nội dung cố định, không phụ thuộc vào data:
title.template — đừng lặp lại tên site
Một pattern rất hay dùng ở root layout là title.template, giúp tự động format title cho toàn bộ trang con:
Khi đó trang con chỉ cần set:
Generated Metadata — Cho trang động
Với các trang có data thay đổi theo URL (bài viết, sản phẩm…), dùng generateMetadata():
generateMetadata() nhận cùng params với component, nên xử lý hoàn toàn giống nhau.
Tránh fetch trùng lặp với cache()
Cả generateMetadata() và component đều cần data từ cùng một bài viết — nếu fetch riêng lẻ sẽ gọi database hai lần. Dùng cache() của React để memoize:
Dù gọi getPost(slug) hai lần trong cùng một request, database chỉ thật sự được query một lần.
Lưu ý: Bạn không thể truyền data từ component vào generateMetadata() hay ngược lại. Hai hàm này chạy độc lập — cache() là cách duy nhất để chia sẻ kết quả fetch.
File-based Metadata
Một số metadata được khai báo qua file đặt trực tiếp trong thư mục app/, không cần code:
File đặt càng sâu thì có độ ưu tiên càng cao — ví dụ app/blog/opengraph-image.jpg sẽ override app/opengraph-image.jpg cho tất cả các trang trong /blog.
Sitemap động
Thay vì viết XML thủ công, dùng sitemap.ts để generate tự động:
Next.js tự serve file này tại /sitemap.xml.
robots.txt động
Tương tự, robots.ts cho phép generate theo môi trường:
OG Image động với ImageResponse
Đây là tính năng hay nhất nhưng hay bị bỏ qua. Thay vì một ảnh OG tĩnh cho toàn site, bạn có thể tạo ảnh riêng cho từng bài viết bằng JSX:
Khi share link bài viết lên mạng xã hội, preview sẽ hiển thị ảnh với tiêu đề bài viết thật sự — thay vì logo mặc định của site.
Lưu ý về CSS: ImageResponse chỉ hỗ trợ flexbox và một subset CSS cơ bản. display: grid, nhiều pseudo-selector, và animation sẽ không hoạt động.
Streaming Metadata
Từ Next.js 15, với các trang render động, metadata được stream riêng — tức là UI có thể hiển thị trước khi generateMetadata() resolve xong, thay vì chờ cả hai.
Tuy nhiên có một ngoại lệ quan trọng: bot và crawler (Googlebot, Twitterbot, Slackbot…) vẫn nhận metadata theo cách cũ — chờ đầy đủ rồi mới trả về. Next.js tự detect dựa vào User Agent, nên SEO không bị ảnh hưởng.
Best practices
Luôn set title.template ở root layout. Tránh phải lặp tên site ở mọi trang, đồng thời đảm bảo fallback khi trang con quên set title.
Dùng cache() cho mọi hàm fetch được dùng trong cả metadata lẫn component. Đây là pattern bắt buộc để tránh query database thừa.
OG image nên có kích thước 1200×630px. Đây là kích thước chuẩn được Facebook, Twitter, LinkedIn tối ưu.
Đừng hardcode URL trong sitemap. Dùng biến môi trường NEXT_PUBLIC_SITE_URL để sitemap hoạt động đúng ở cả staging lẫn production.
generateViewport tách riêng khỏi metadata. Từ Next.js 14+, nếu cần customize viewport (ví dụ theme color trên mobile), dùng generateViewport thay vì đặt trong metadata:
Tóm tắt
Next.js cung cấp hệ thống metadata khá đầy đủ cho production:
Static metadata — export metadata object từ layout.tsx hoặc page.tsx title.template — format title nhất quán cho toàn site generateMetadata() — metadata động cho trang có data thay đổi theo URL cache() — tránh fetch trùng lặp giữa metadata và component File-based — favicon, opengraph-image, robots.txt, sitemap.xml ImageResponse — OG image động render bằng JSX Bước tiếp theo có thể khám phá: kết hợp OG image với custom font (Google Fonts hoặc font local), hoặc tìm hiểu về generateStaticParams() để pre-render metadata cho toàn bộ bài viết lúc build time.