2026-03-208 phút đọcVI
Định dạng Case Study
Open Graph & Twitter Cards trong Next.js
Mở đầu
Khi ai đó share link leduykhuong.com lên LinkedIn, Facebook, hay Slack — họ thấy gì? Một URL trơn hay một "card" đẹp với tiêu đề, mô tả, và hình ảnh? Sự khác biệt nằm ở Open Graph (OG) và Twitter Card meta tags.
Open Graph là protocol do Facebook tạo ra (2010), cho phép website kiểm soát cách link hiển thị khi được share trên social media. Twitter Cards là phiên bản tương tự của Twitter/X. Cả hai đều hoạt động qua <meta> tags trong <head> — và Next.js Metadata API hỗ trợ native cho cả hai.
Mục tiêu bài này: Hiểu cách Next.js render OG và Twitter Card tags, tại sao kích thước image 1200×630 là tiêu chuẩn, cách leduykhuong.com centralize logic này trong lib/seo.ts, và cách debug social previews.
Open Graph — Cơ chế hoạt động
Khi một social platform nhận URL, nó gửi HTTP request đến URL đó (giống bot crawl), parse HTML, và tìm các <meta property="og:*"> tags. Các tag quan trọng nhất:
| Tag | Ý nghĩa | Bắt buộc? |
|---|---|---|
og:title | Tiêu đề hiển thị trên card | Có |
og:description | Mô tả ngắn (2-3 dòng) | Khuyến nghị |
og:image | URL hình ảnh preview | Khuyến nghị mạnh |
og:url | Canonical URL của trang | Có |
og:type | Loại content: website, article, profile | Có |
og:site_name | Tên website | Khuyến nghị |
og:locale | Ngôn ngữ: vi_VN, en_US | Khuyến nghị |
Trong Next.js Metadata API
Root layout khai báo OG defaults:
// app/layout.tsx
export const metadata: Metadata = {
openGraph: {
title: "Le Duy Khuong",
description:
"Engineering Leader × AI/Agentic Systems Builder — leduykhuong.com",
siteName: "Le Duy Khuong",
},
};Next.js render thành:
<meta property="og:title" content="Le Duy Khuong" />
<meta property="og:description" content="Engineering Leader × AI/Agentic Systems Builder — leduykhuong.com" />
<meta property="og:site_name" content="Le Duy Khuong" />Lưu ý: Layout chỉ set 3 fields cơ bản. Blog posts cần nhiều hơn — đó là lý do có getOgMetadata().
Helper Function getOgMetadata() — Centralized OG Logic
Thay vì mỗi page tự build OG object, leduykhuong.com dùng helper function trong lib/seo.ts:
// lib/seo.ts
export function getOgMetadata(post: Post, locale?: "vi" | "en") {
const imageUrl = post.ogImage
? `${SITE_CONFIG.baseUrl}${post.ogImage}`
: `${SITE_CONFIG.baseUrl}/og-default.png`;
const resolvedLocale = locale ?? (post.lang === "vi" ? "vi" : "en");
return {
title: `${post.seoTitle || post.title} | ${SITE_CONFIG.siteName}`,
description: getSeoDescription(post),
url: getCanonicalUrl(`/blog/${post.slug}`),
siteName: SITE_CONFIG.siteName,
type: "article" as const,
locale: resolvedLocale === "vi" ? "vi_VN" : "en_US",
images: [
{
url: imageUrl,
width: 1200,
height: 630,
alt: post.seoTitle || post.title,
},
],
publishedTime: post.date,
modifiedTime: post.date,
authors: [SITE_CONFIG.author.url],
};
}Phân tích từng field:
-
title— DùngseoTitle(nếu post khai báo) hoặcpost.title, append site name. Pattern"Post Title | Site Name"giúp brand nhận diện trên feed. -
description— Delegate chogetSeoDescription()— priority:seoDescription > excerpt > site default. Hàm này cũng truncate tại 155 ký tự (Google snippet limit). -
url— Canonical URL (không có locale prefix). Quan trọng vì social platforms dùng URL này để deduplicate shares. -
type: "article"— Nói cho Facebook: "Đây là bài viết, không phải homepage." Facebook sẽ hiển thị thêmpublishedTimevàauthors. -
locale: "vi_VN"— Formatlanguage_TERRITORY. Facebook dùng để route nội dung đúng audience. -
images— Array (có thể nhiều ảnh, Facebook chọn ảnh đầu). Width/height giúp platform biết aspect ratio mà không cần download ảnh. -
publishedTime/modifiedTime— ISO date strings. Facebook hiển thị "Published on..." trên article previews. -
authors— Array URLs, Facebook có thể link tới author profile.
Tại sao Image 1200×630?
Đây là kích thước tiêu chuẩn de facto cho social sharing images:
| Platform | Kích thước tối ưu | Aspect Ratio |
|---|---|---|
| 1200 × 630 | 1.91:1 | |
| 1200 × 627 | ~1.91:1 | |
| Twitter (large card) | 1200 × 628 | ~1.91:1 |
| Slack | 1200 × 630 | 1.91:1 |
Tất cả hội tụ quanh 1.91:1. Nếu dùng 1200×630, ảnh hiển thị đẹp trên mọi platform mà không bị crop.
Trong code:
images: [
{
url: imageUrl,
width: 1200,
height: 630,
alt: post.seoTitle || post.title,
},
],Khai báo width và height không bắt buộc nhưng rất khuyến nghị — social platforms có thể render placeholder đúng tỷ lệ ngay trước khi image load xong.
Fallback image: Nếu post không có ogImage, hệ thống dùng /og-default.png. Đây là ảnh generic với tên và branding — tốt hơn nhiều so với không có ảnh (card trống trông rất tệ trên feed).
Twitter Cards — summary_large_image
Twitter/X có hệ thống meta tags riêng dùng <meta name="twitter:*">. Có 4 loại card:
| Card Type | Mô tả | Khi nào dùng |
|---|---|---|
summary | Card nhỏ với ảnh vuông bên trái | Short content, news |
summary_large_image | Card lớn với ảnh full-width phía trên | Blog posts, articles |
player | Embedded video/audio | Media content |
app | App install card | Mobile apps |
leduykhuong.com dùng summary_large_image cho tất cả blog posts:
// lib/seo.ts
export function getTwitterMetadata(post: Post) {
return {
card: "summary_large_image" as const,
title: post.seoTitle || post.title,
description: getSeoDescription(post),
images: [
post.ogImage
? `${SITE_CONFIG.baseUrl}${post.ogImage}`
: `${SITE_CONFIG.baseUrl}/og-default.png`,
],
};
}Next.js render thành:
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Learning in Public" />
<meta name="twitter:description" content="Tại sao viết giúp suy nghĩ rõ hơn..." />
<meta name="twitter:image" content="https://leduykhuong.com/og-default.png" />Khác biệt với OG: Twitter tags dùng name attribute (không phải property), và format đơn giản hơn (images là array strings, không phải objects). Next.js handle sự khác biệt này tự động.
OG vs Twitter — Fallback Behavior
Hầu hết social platforms hỗ trợ fallback: nếu không tìm thấy Twitter-specific tags, chúng sẽ dùng OG tags thay thế.
Twitter crawl:
1. Tìm <meta name="twitter:title"> → dùng nếu có
2. Nếu không → fallback sang <meta property="og:title">
3. Nếu không → dùng <title> tag
Vậy tại sao cần cả hai? Vì:
twitter:card— Không có OG equivalent. Bắt buộc phải khai báo loại card.- Title/Description — Đôi khi muốn title ngắn hơn cho Twitter (character limit khác Facebook).
- Image — Twitter yêu cầu image URL absolute, OG cũng vậy nhưng format object khác.
Trong thực tế, leduykhuong.com khai báo cả hai vì chúng có cùng source data — chi phí thêm vài tags là rất nhỏ so với lợi ích kiểm soát chính xác hiển thị trên mỗi platform.
Tích hợp trong generateMetadata()
Blog post page kết hợp cả OG và Twitter:
// app/[locale]/blog/[slug]/page.tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale: localeParam, slug } = await params;
const locale = parseLocale(localeParam);
const post = getPostBySlug(slug);
if (!post) return { title: "Not Found" };
return {
title: post.seoTitle || getDisplayTitle(post, locale),
description: getSeoDescription(post) || getDisplayExcerpt(post, locale),
openGraph: getOgMetadata(post, locale), // ← OG tags
twitter: getTwitterMetadata(post), // ← Twitter tags
alternates: {
canonical: `${BASE_URL}/${locale}/blog/${slug}`,
languages: { /* ... */ },
},
};
}Tại sao quan trọng: Centralized helpers (getOgMetadata, getTwitterMetadata) đảm bảo:
- Mọi blog post có OG + Twitter tags đầy đủ
- Image fallback luôn hoạt động
- Thay đổi logic một lần trong
lib/seo.ts→ apply cho toàn bộ site - Không có risk quên field khi tạo page mới
Debugging Social Previews
Khi share link mà preview hiển thị sai, nguyên nhân thường là cache hoặc tag thiếu. Tools để debug:
1. Facebook Sharing Debugger
Truy cập developers.facebook.com/tools/debug/ → nhập URL → "Scrape Again". Facebook sẽ:
- Hiển thị tất cả OG tags tìm được
- Cảnh báo tags thiếu
- Cho phép clear cache và re-fetch
2. Twitter Card Validator
Truy cập cards-dev.twitter.com/validator → nhập URL. Tool hiển thị preview chính xác của card.
3. LinkedIn Post Inspector
Truy cập linkedin.com/post-inspector/ → nhập URL. Tương tự Facebook debugger nhưng cho LinkedIn.
4. Kiểm tra trực tiếp trong HTML
# Sau khi build, check OG tags trong static HTML output
grep 'property="og:' out/vi/blog/learning-in-public.html
# Check Twitter tags
grep 'name="twitter:' out/vi/blog/learning-in-public.html
# Check image URL đúng absolute
grep 'og:image' out/vi/blog/learning-in-public.htmlLỗi phổ biến nhất: Image URL là relative path (/images/og.png) thay vì absolute (https://leduykhuong.com/images/og.png). Social platforms cần absolute URL — chúng không có context domain để resolve relative path.
Thực hành
Bài tập 1: So sánh OG output giữa homepage và blog post
cd ACE-component/ACE-leduykhuong-site && npm run build
# Homepage OG tags (từ static metadata)
grep 'property="og:' out/index.html
# Blog post OG tags (từ generateMetadata + getOgMetadata)
grep 'property="og:' out/vi/blog/learning-in-public.htmlCâu hỏi: Homepage có bao nhiêu OG tags? Blog post có bao nhiêu? Tại sao khác nhau?
Bài tập 2: Verify image dimensions
# Check og:image:width và og:image:height trong output
grep -E 'og:image' out/vi/blog/learning-in-public.htmlCâu hỏi: Image width và height có được render không? Nếu post không có ogImage, URL trỏ tới đâu?
Bài tập 3: Đọc helper functions
Mở lib/seo.ts và trả lời:
getOgMetadata()return bao nhiêu fields?- Nếu
post.seoTitlekhông tồn tại, OG title lấy từ đâu? - Locale mapping:
"vi"→"vi_VN"— tại sao cần formatlanguage_TERRITORY?
Tóm tắt
- Open Graph (
og:*) kiểm soát preview trên Facebook, LinkedIn, Slack — dùngpropertyattribute - Twitter Cards (
twitter:*) kiểm soát preview trên Twitter/X — dùngnameattribute - Image 1200×630 là tiêu chuẩn cross-platform, khai báo
width/heightgiúp render nhanh - Fallback: Twitter dùng OG tags nếu không tìm thấy twitter-specific tags
- Centralized helpers (
getOgMetadata,getTwitterMetadata) đảm bảo consistency toàn site - Debug bằng Facebook Debugger, Twitter Validator, hoặc grep HTML output
Bài tiếp theo
Bài 3: Sitemap & Robots trong Static Export — Cách Next.js generate sitemap.xml và robots.txt, thách thức đặc biệt khi dùng static export (output: export), và cách leduykhuong.com giải quyết vấn đề route generation cho multilingual site.