2026-03-207 phút đọcVI
Schema.org & JSON-LD Basics
Mở đầu
Meta tags (title, description, OG) nói cho Google biết thông tin hiển thị — tiêu đề gì, mô tả gì, ảnh gì. Nhưng chúng không nói cho Google biết nội dung là gì theo ngữ nghĩa: Đây là bài blog hay trang sản phẩm? Ai là tác giả? Ngày xuất bản? Đây là tổ chức hay cá nhân?
Structured data giải quyết bài toán này. Nó cho phép anh khai báo: "Trang này chứa một BlogPosting, viết bởi Le Duy Khuong, xuất bản ngày 2026-03-20, thuộc website leduykhuong.com." Google đọc structured data và có thể hiển thị rich results — snippets đặc biệt trên kết quả tìm kiếm với ngày, tác giả, rating, breadcrumbs.
Mục tiêu: Hiểu Schema.org vocabulary, JSON-LD format, cách inject vào Next.js page, và component JsonLd.tsx trên leduykhuong.com.
Schema.org — Từ vựng chung
Schema.org là dự án hợp tác giữa Google, Microsoft, Yahoo, và Yandex (từ 2011). Nó định nghĩa một vocabulary — bộ từ vựng mô tả mọi loại entity trên web:
| Type | Mô tả | Ví dụ |
|---|---|---|
Person | Cá nhân | Author profile |
Organization | Tổ chức | Công ty, trường học |
BlogPosting | Bài blog | Blog article |
WebSite | Website | Homepage |
BreadcrumbList | Breadcrumb navigation | Home > Blog > Post |
Article | Bài báo | News article |
Product | Sản phẩm | E-commerce product |
FAQPage | FAQ | Q&A page |
HowTo | Hướng dẫn | Tutorial |
Mỗi type có properties riêng. Ví dụ BlogPosting có:
headline— Tiêu đề bàiauthor— Tác giả (typePerson)datePublished— Ngày xuất bảnimage— Ảnh đại diệnpublisher— Nhà xuất bản
Hierarchy: Schema.org có cấu trúc thừa kế. BlogPosting extends SocialMediaPosting extends Article extends CreativeWork extends Thing. Mỗi level thêm properties mới.
JSON-LD — Format khai báo
Schema.org chỉ là vocabulary. Để nhúng vào HTML, cần format. Có 3 formats:
| Format | Cách nhúng | Google khuyến nghị? |
|---|---|---|
| JSON-LD | <script type="application/ld+json"> | Có ✅ |
| Microdata | HTML attributes (itemscope, itemprop) | Hỗ trợ nhưng không khuyến nghị |
| RDFa | HTML attributes (typeof, property) | Hỗ trợ nhưng không khuyến nghị |
Google khuyến nghị JSON-LD vì:
- Tách biệt khỏi HTML — không cần sửa markup, chỉ thêm
<script>tag - Dễ generate bằng code — JSON object →
JSON.stringify()→ done - Dễ validate — copy JSON → paste vào validator
- Không ảnh hưởng rendering —
<script>tag invisible cho user
Cấu trúc JSON-LD
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "Learning in Public",
"author": {
"@type": "Person",
"name": "Le Duy Khuong"
},
"datePublished": "2026-03-20"
}@context— Luôn là"https://schema.org". Nói parser: "Dùng Schema.org vocabulary."@type— Loại entity. Phải match type trong Schema.org.- Properties — Key-value pairs theo type definition.
Nested types
Properties có thể là type khác:
{
"@type": "BlogPosting",
"author": {
"@type": "Person",
"name": "Le Duy Khuong",
"url": "https://leduykhuong.com"
}
}author không chỉ là string — nó là một Person entity với name và url. Google dùng thông tin này để link author tới Knowledge Panel.
Component JsonLd.tsx — Inject vào Next.js
leduykhuong.com dùng React Server Component đơn giản:
// components/seo/JsonLd.tsx
export function JsonLd({ data }: { data: object }) {
return (
<script
type="application/ld+json"
suppressHydrationWarning
// Safe: JSON.stringify produces valid JSON, not executable HTML/JS.
// Data source is trusted author-controlled frontmatter only.
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
);
}Phân tích:
-
type="application/ld+json"— Browser không execute script này (không phải JavaScript). Google crawler parse nó riêng. -
dangerouslySetInnerHTML+JSON.stringify— React thường escape HTML content. Ở đây cần inject raw JSON string vào script tag. Pattern này an toàn vì: (a)JSON.stringify()output luôn là valid JSON — nó escape special characters như<,>,", nên không thể inject HTML/script; (b) data source chỉ từ author-controlled frontmatter và hardcoded config, không bao giờ chứa user-generated content. -
suppressHydrationWarning— Tránh React warning khi server-rendered HTML khác client (date/time edge cases). -
Security boundary: KHÔNG BAO GIỜ truyền user input (comments, form data) vào
JsonLdcomponent. Chỉ dùng với trusted data từ code và frontmatter.
Sử dụng trong page
// app/[locale]/blog/[slug]/page.tsx
import { JsonLd } from "@/components/seo/JsonLd";
import { getBlogPostJsonLd, getBreadcrumbJsonLd } from "@/lib/seo";
export default async function BlogPostPage({ params }: Props) {
const post = getPostBySlug(slug);
return (
<article>
{/* JSON-LD cho BlogPosting */}
<JsonLd data={getBlogPostJsonLd(post)} />
{/* JSON-LD cho Breadcrumbs */}
<JsonLd data={getBreadcrumbJsonLd([
{ name: "Home", url: SITE_CONFIG.baseUrl },
{ name: "Blog", url: `${SITE_CONFIG.baseUrl}/blog` },
{ name: post.title, url: `${SITE_CONFIG.baseUrl}/blog/${post.slug}` },
])} />
{/* ...page content... */}
</article>
);
}Nhiều <script> tags: Mỗi JsonLd component render một <script> riêng. Google xử lý tốt multiple JSON-LD blocks — mỗi block mô tả một entity khác nhau trên cùng page.
HTML Output
Khi build, mỗi blog post HTML chứa:
<script type="application/ld+json">
{
"@context":"https://schema.org",
"@type":"BlogPosting",
"headline":"Learning in Public",
"description":"Tại sao viết giúp suy nghĩ rõ hơn...",
"author":{"@type":"Person","name":"Le Duy Khuong","url":"https://leduykhuong.com"},
"datePublished":"2026-03-20",
"dateModified":"2026-03-20",
"url":"https://leduykhuong.com/blog/learning-in-public",
"image":"https://leduykhuong.com/og-default.png",
"publisher":{"@type":"Person","name":"Le Duy Khuong","url":"https://leduykhuong.com"},
"mainEntityOfPage":{"@type":"WebPage","@id":"https://leduykhuong.com/blog/learning-in-public"},
"inLanguage":"vi"
}
</script>
<script type="application/ld+json">
{
"@context":"https://schema.org",
"@type":"BreadcrumbList",
"itemListElement":[
{"@type":"ListItem","position":1,"name":"Home","item":"https://leduykhuong.com"},
{"@type":"ListItem","position":2,"name":"Blog","item":"https://leduykhuong.com/blog"},
{"@type":"ListItem","position":3,"name":"Learning in Public","item":"https://leduykhuong.com/blog/learning-in-public"}
]
}
</script>Google đọc cả 2 blocks: "Trang này chứa một BlogPosting và một BreadcrumbList."
Rich Results — Kết quả Google
Khi Google đọc structured data hợp lệ, nó CÓ THỂ hiển thị rich results:
BlogPosting → Article rich result
Le Duy Khuong · Mar 20, 2026
Learning in Public
Tại sao viết giúp suy nghĩ rõ hơn — bài viết từ...
Google hiển thị thêm author name và ngày — thông tin từ author.name và datePublished.
BreadcrumbList → Breadcrumb trail
leduykhuong.com > Blog > Learning in Public
Thay vì hiển thị full URL (leduykhuong.com/vi/blog/learning-in-public), Google hiển thị breadcrumb trail dễ đọc hơn.
Lưu ý quan trọng: Structured data là gợi ý, không phải guarantee. Google tự quyết định có hiển thị rich results hay không dựa trên:
- Structured data hợp lệ (required)
- Site có đủ trust (domain authority)
- Content quality
- Query context
Validation — Kiểm tra JSON-LD
1. Schema Markup Validator
Google cung cấp Rich Results Test tại search.google.com/test/rich-results:
- Nhập URL hoặc paste HTML code
- Tool parse JSON-LD và validate theo Schema.org rules
- Báo cáo: errors (phải fix), warnings (nên fix), valid items
2. Grep local build output
# Extract JSON-LD từ HTML
grep 'application/ld+json' out/vi/blog/learning-in-public.html
# Pretty-print JSON (extract content between script tags)
grep -oP '(?<=application/ld\+json">).*?(?=</script>)' \
out/vi/blog/learning-in-public.html | python3 -m json.tool3. Chrome DevTools
Elements tab → tìm <script type="application/ld+json"> → expand → xem JSON.
Thực hành
Bài tập 1: Đọc JSON-LD output
cd ACE-component/ACE-leduykhuong-site && npm run build
# Tìm tất cả JSON-LD blocks trong 1 blog post
grep -c 'application/ld+json' out/vi/blog/learning-in-public.htmlCâu hỏi: Có bao nhiêu JSON-LD blocks? Mỗi block chứa @type gì?
Bài tập 2: So sánh helper output
Đọc lib/seo.ts:
getBlogPostJsonLd()return bao nhiêu fields?publishervàauthorcó cùng data không? Tại sao?mainEntityOfPagedùng để làm gì?
Bài tập 3: Tìm JsonLd usage
grep -rn "JsonLd" app/ components/ --include="*.tsx"Câu hỏi: Component JsonLd được dùng ở những page nào? Mỗi page inject bao nhiêu blocks?
Tóm tắt
- Schema.org — Vocabulary chung định nghĩa types (BlogPosting, Person, WebSite...) và properties
- JSON-LD — Format nhúng structured data vào HTML qua
<script type="application/ld+json"> - Google khuyến nghị JSON-LD vì tách biệt khỏi HTML, dễ generate, dễ validate
JsonLd.tsx— Server Component đơn giản: nhận object →JSON.stringify→ inject<script>(safe vì data trusted, JSON.stringify escapes special chars)- Multiple blocks — Mỗi entity (BlogPosting, BreadcrumbList) là một
<script>riêng - Rich results — Google CÓ THỂ hiển thị enhanced snippets, nhưng không đảm bảo
Bài tiếp theo
Bài 6: BlogPosting Schema Deep Dive — Phân tích chi tiết getBlogPostJsonLd() trên leduykhuong.com: mỗi field có ý nghĩa gì, Google dùng field nào cho rich results, và best practices cho author/publisher configuration.