Le Duy Khuong

Chuỗi: seo-json-ld · Phần 1

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

Schema.org & JSON-LD Basics

Schema.org vocabulary, JSON-LD format, rich results

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:

TypeMô tảVí dụ
PersonCá nhânAuthor profile
OrganizationTổ chứcCông ty, trường học
BlogPostingBài blogBlog article
WebSiteWebsiteHomepage
BreadcrumbListBreadcrumb navigationHome > Blog > Post
ArticleBài báoNews article
ProductSản phẩmE-commerce product
FAQPageFAQQ&A page
HowToHướng dẫnTutorial

Mỗi type có properties riêng. Ví dụ BlogPosting có:

  • headline — Tiêu đề bài
  • author — Tác giả (type Person)
  • datePublished — Ngày xuất bản
  • image — Ảnh đại diện
  • publisher — 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:

FormatCách nhúngGoogle khuyến nghị?
JSON-LD<script type="application/ld+json">Có ✅
MicrodataHTML attributes (itemscope, itemprop)Hỗ trợ nhưng không khuyến nghị
RDFaHTML attributes (typeof, property)Hỗ trợ nhưng không khuyến nghị

Google khuyến nghị JSON-LD vì:

  1. Tách biệt khỏi HTML — không cần sửa markup, chỉ thêm <script> tag
  2. Dễ generate bằng code — JSON object → JSON.stringify() → done
  3. Dễ validate — copy JSON → paste vào validator
  4. 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:

  1. type="application/ld+json" — Browser không execute script này (không phải JavaScript). Google crawler parse nó riêng.

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

  3. suppressHydrationWarning — Tránh React warning khi server-rendered HTML khác client (date/time edge cases).

  4. Security boundary: KHÔNG BAO GIỜ truyền user input (comments, form data) vào JsonLd component. 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.namedatePublished.

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:

  1. Nhập URL hoặc paste HTML code
  2. Tool parse JSON-LD và validate theo Schema.org rules
  3. 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.tool

3. 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.html

Câ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?
  • publisherauthor có cùng data không? Tại sao?
  • mainEntityOfPage dù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.

LDK

Le Duy Khuong

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