2026-03-207 phút đọcVI
BlogPosting Schema Deep Dive
Mở đầu
Bài trước giới thiệu tổng quan Schema.org và JSON-LD. Bài này sẽ đi sâu vào type quan trọng nhất cho leduykhuong.com: BlogPosting. Mỗi blog post trên site đều inject một BlogPosting schema qua getBlogPostJsonLd() trong lib/seo.ts.
Tại sao cần hiểu chi tiết? Vì mỗi field trong BlogPosting có ý nghĩa riêng với Google. Một số fields là required cho rich results, một số là recommended, và một số optional nhưng giúp Google hiểu content tốt hơn. Biết field nào quan trọng → biết cần điền gì trong MDX frontmatter.
Mục tiêu: Phân tích từng field trong getBlogPostJsonLd(), hiểu Google dùng chúng thế nào, và best practices cho personal blog.
getBlogPostJsonLd() — Toàn bộ function
// lib/seo.ts
export function getBlogPostJsonLd(post: Post) {
return {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.seoTitle || post.title,
description: getSeoDescription(post),
author: {
"@type": "Person",
name: SITE_CONFIG.author.name,
url: SITE_CONFIG.author.url,
},
datePublished: post.date,
dateModified: post.date,
url: getCanonicalUrl(`/blog/${post.slug}`),
image: post.ogImage
? `${SITE_CONFIG.baseUrl}${post.ogImage}`
: `${SITE_CONFIG.baseUrl}/og-default.png`,
publisher: {
"@type": "Person",
name: SITE_CONFIG.author.name,
url: SITE_CONFIG.author.url,
},
mainEntityOfPage: {
"@type": "WebPage",
"@id": getCanonicalUrl(`/blog/${post.slug}`),
},
inLanguage: post.lang === "vi" ? "vi" : "en",
...(post.keywords ? { keywords: post.keywords.join(", ") } : {}),
};
}Phân tích từng field
headline — Required
headline: post.seoTitle || post.title,- Google dùng cho: Article rich result title
- Priority:
seoTitle(nếu tác giả khai báo riêng) >title(tiêu đề chính) - Best practice: Dưới 110 ký tự. Google cắt headline dài trong rich results.
Tại sao có seoTitle riêng? Vì đôi khi title cho reader (dài, descriptive) khác title cho search (ngắn, keyword-rich). Ví dụ:
title: "Tại sao tôi quyết định viết public và bài học sau 6 tháng"seoTitle: "Learning in Public — 6 tháng kinh nghiệm"
description — Recommended
description: getSeoDescription(post),Function getSeoDescription() có priority chain:
post.seoDescription— Tác giả viết riêng cho SEOpost.excerpt— Tóm tắt bài (truncate 155 ký tự)- Fallback: site-level description
Google dùng cho: Snippet text dưới title trong search results. 155 ký tự là soft limit — Google có thể cắt ngắn hơn hoặc tự generate snippet từ content.
author — Required cho rich results
author: {
"@type": "Person",
name: SITE_CONFIG.author.name, // "Le Duy Khuong"
url: SITE_CONFIG.author.url, // "https://leduykhuong.com"
},@type: "Person"— Tác giả là cá nhân (không phải Organization)name— Tên hiển thị trong rich resultsurl— Link tới profile page. Google dùng để build Knowledge Panel
Best practice cho personal blog: author và publisher cùng là Person (cùng người). Với blog công ty, publisher sẽ là Organization.
datePublished và dateModified — Required
datePublished: post.date,
dateModified: post.date,- Google dùng cho: Hiển thị "Mar 20, 2026" trong search results
- Format: ISO 8601 string (ví dụ:
"2026-03-20") - Hiện tại:
dateModified = datePublished(chưa track modified date riêng)
Cải thiện trong tương lai: Track lastModified trong MDX frontmatter. Google ưu tiên hiển thị dateModified nếu khác datePublished — cho thấy content được cập nhật.
url — Required
url: getCanonicalUrl(`/blog/${post.slug}`),URL canonical — phải match với <link rel="canonical"> trong metadata. Nếu khác nhau, Google sẽ confused về URL nào là chính.
image — Recommended mạnh
image: post.ogImage
? `${SITE_CONFIG.baseUrl}${post.ogImage}`
: `${SITE_CONFIG.baseUrl}/og-default.png`,- Phải absolute URL — bắt đầu
https:// - Fallback:
/og-default.pngkhi post không có ảnh riêng - Google: Bài có image có cơ hội hiển thị rich result cao hơn bài không có image
publisher — Required
publisher: {
"@type": "Person",
name: SITE_CONFIG.author.name,
url: SITE_CONFIG.author.url,
},Personal blog: Publisher = Author (cùng Person). Đây là pattern hợp lệ.
Organization blog: Publisher nên là:
{
"@type": "Organization",
"name": "Company Name",
"logo": { "@type": "ImageObject", "url": "https://example.com/logo.png" }
}mainEntityOfPage — Recommended
mainEntityOfPage: {
"@type": "WebPage",
"@id": getCanonicalUrl(`/blog/${post.slug}`),
},Nói cho Google: "Entity chính trên page này là BlogPosting kia." Nếu page có nhiều entities (sidebar articles, related posts), mainEntityOfPage chỉ rõ entity nào là main content.
@id — Identifier unique cho WebPage. Dùng canonical URL là best practice.
inLanguage — Recommended
inLanguage: post.lang === "vi" ? "vi" : "en",IETF language tag: "vi" hoặc "en". Giúp Google phân loại content theo ngôn ngữ — quan trọng cho multilingual sites.
keywords — Optional
...(post.keywords ? { keywords: post.keywords.join(", ") } : {}),Conditional spread: chỉ thêm keywords nếu post có khai báo trong frontmatter. Keywords là comma-separated string.
Lưu ý: Google KHÔNG dùng keywords cho ranking (từ 2009). Nhưng field này vẫn hữu ích cho:
- Các search engines khác (Bing, Yandex)
- Internal search/filtering
- Content organization
Google Rich Results Requirements
Google có danh sách required/recommended fields cho Article rich results:
| Field | Status | leduykhuong.com | Ghi chú |
|---|---|---|---|
headline | Required | ✅ | Từ seoTitle/title |
image | Required | ✅ | Từ ogImage/default |
datePublished | Required | ✅ | Từ frontmatter date |
author.name | Required | ✅ | Config constant |
author.url | Recommended | ✅ | Config constant |
dateModified | Recommended | ⚠️ | = datePublished |
publisher | Recommended | ✅ | Same as author |
description | Recommended | ✅ | Từ seoDescription/excerpt |
mainEntityOfPage | Recommended | ✅ | Canonical URL |
Status: leduykhuong.com đáp ứng tất cả required + hầu hết recommended fields. Cải thiện duy nhất: track dateModified riêng.
Author vs Publisher — Khi nào khác nhau?
Trên leduykhuong.com, cả hai đều là cùng một Person. Nhưng trong các context khác:
| Tình huống | Author | Publisher |
|---|---|---|
| Personal blog | Person (tác giả) | Person (cùng người) |
| Company blog | Person (nhân viên viết) | Organization (công ty) |
| News site | Person (phóng viên) | Organization (tòa soạn) |
| Guest post | Person (khách mời) | Person/Org (chủ blog) |
Google dùng cả hai: Author hiển thị trong rich results, Publisher dùng để verify source credibility.
Mapping MDX Frontmatter → Schema
Dữ liệu cho schema đến từ MDX frontmatter:
---
title: "Learning in Public"
seoTitle: "Learning in Public — 6 tháng kinh nghiệm"
date: "2026-03-20"
excerpt: "Tại sao viết giúp suy nghĩ rõ hơn..."
seoDescription: "Bài viết chia sẻ 6 tháng learning in public..."
ogImage: "/generated/learning-in-public.webp"
lang: vi
keywords: ["learning", "writing", "public"]
---| Frontmatter field | Schema field | Fallback |
|---|---|---|
seoTitle | headline | title |
seoDescription | description | excerpt → site description |
date | datePublished, dateModified | — |
ogImage | image | /og-default.png |
lang | inLanguage | "en" |
keywords | keywords | Omit field |
Thiết kế principle: Mỗi MDX field có fallback. Tác giả chỉ cần điền title và date tối thiểu — schema vẫn đầy đủ required fields nhờ defaults và fallbacks.
Thực hành
Bài tập 1: Đọc raw JSON-LD output
cd ACE-component/ACE-leduykhuong-site && npm run build
# Extract và format BlogPosting JSON-LD
grep -oP '(?<=ld\+json">)\{[^}]*BlogPosting[^<]*(?=</script>)' \
out/vi/blog/learning-in-public.html | python3 -m json.toolCâu hỏi: Output có bao nhiêu fields? datePublished và dateModified có giống nhau không?
Bài tập 2: So sánh blog posts có/không có ogImage
Tìm 2 blog posts: 1 có ogImage trong frontmatter, 1 không có. So sánh image field trong JSON-LD output.
Bài tập 3: Validate bằng Rich Results Test
- Deploy site (hoặc dùng
npx serve out) - Mở Rich Results Test
- Nhập URL blog post
- Kiểm tra: bao nhiêu detected items? Có warnings không?
Tóm tắt
- BlogPosting có ~12 fields; Google yêu cầu 4 required: headline, image, datePublished, author.name
getBlogPostJsonLd()centralize logic: MDX frontmatter → Schema object với fallbacks- Author = Publisher hợp lệ cho personal blog; company blog cần Publisher là Organization
seoTitlecho phép SEO title khác display title — dùng khi cần keyword optimizationdateModifiednên track riêng (hiện = datePublished) — Google ưu tiên updated contentkeywordsGoogle bỏ qua cho ranking, nhưng hữu ích cho Bing và internal search
Bài tiếp theo
Bài 7: Multi-Schema Strategy — leduykhuong.com inject nhiều schema types trên cùng page (BlogPosting + BreadcrumbList). Bài tiếp sẽ phân tích WebSite schema (homepage), Person schema (about page), và strategy kết hợp nhiều schemas.