Le Duy Khuong

Chuỗi: seo-nextjs-metadata · Phần 2

Năng suất & công cụ dev

Open Graph & Twitter Cards trong Next.js

OG protocol, Twitter Card types, 1200x630, social previews

2026-03-208 phút đọcVI

Định dạng Case Study

This post follows the Problem → Approach → Result → Lessons structure.

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ĩaBắt buộc?
og:titleTiêu đề hiển thị trên card
og:descriptionMô tả ngắn (2-3 dòng)Khuyến nghị
og:imageURL hình ảnh previewKhuyến nghị mạnh
og:urlCanonical URL của trang
og:typeLoại content: website, article, profile
og:site_nameTên websiteKhuyến nghị
og:localeNgôn ngữ: vi_VN, en_USKhuyế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:

  1. title — Dùng seoTitle (nếu post khai báo) hoặc post.title, append site name. Pattern "Post Title | Site Name" giúp brand nhận diện trên feed.

  2. description — Delegate cho getSeoDescription() — priority: seoDescription > excerpt > site default. Hàm này cũng truncate tại 155 ký tự (Google snippet limit).

  3. url — Canonical URL (không có locale prefix). Quan trọng vì social platforms dùng URL này để deduplicate shares.

  4. type: "article" — Nói cho Facebook: "Đây là bài viết, không phải homepage." Facebook sẽ hiển thị thêm publishedTimeauthors.

  5. locale: "vi_VN" — Format language_TERRITORY. Facebook dùng để route nội dung đúng audience.

  6. 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.

  7. publishedTime / modifiedTime — ISO date strings. Facebook hiển thị "Published on..." trên article previews.

  8. 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:

PlatformKích thước tối ưuAspect Ratio
Facebook1200 × 6301.91:1
LinkedIn1200 × 627~1.91:1
Twitter (large card)1200 × 628~1.91:1
Slack1200 × 6301.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 widthheight 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 TypeMô tảKhi nào dùng
summaryCard nhỏ với ảnh vuông bên tráiShort content, news
summary_large_imageCard lớn với ảnh full-width phía trênBlog posts, articles
playerEmbedded video/audioMedia content
appApp install cardMobile 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ì:

  1. twitter:card — Không có OG equivalent. Bắt buộc phải khai báo loại card.
  2. Title/Description — Đôi khi muốn title ngắn hơn cho Twitter (character limit khác Facebook).
  3. 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.html

Lỗ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.html

Câ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.html

Câ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.seoTitle không tồn tại, OG title lấy từ đâu?
  • Locale mapping: "vi""vi_VN" — tại sao cần format language_TERRITORY?

Tóm tắt

  • Open Graph (og:*) kiểm soát preview trên Facebook, LinkedIn, Slack — dùng property attribute
  • Twitter Cards (twitter:*) kiểm soát preview trên Twitter/X — dùng name attribute
  • Image 1200×630 là tiêu chuẩn cross-platform, khai báo width/height giú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.xmlrobots.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.

LDK

Le Duy Khuong

AI Transformation & Digital Strategy. Writing about agentic systems, engineering leadership, and building in public.