JSON-LD 怎麼寫?ld+json 的語法、嵌套與驗證流程
JSON-LD(JSON for Linking Data)是撰寫 結構化資料 的建議格式。它的優勢是 與版面完全解耦,標記集中在一個獨立的 <script> 區塊裡,改版面不會動到它。
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "文章標題",
"datePublished": "2026-09-07"
}
</script>三個關鍵欄位
| 欄位 | 作用 | 必要性 |
|---|---|---|
@context | 宣告詞彙來自哪一套定義 | 必填 |
@type | 這是什麼類型的實體 | 必填 |
@id | 這個實體的唯一識別碼 | 選填 |
@context
值固定是 Schema.org 的網址。它告訴解析器「後面的 headline、author 這些詞來自 Schema.org 的定義」。
{ "@context": "https://schema.org" }要用 https 開頭
http://schema.org 仍能解析,但建議統一用 https://schema.org。
@type
決定這個實體是什麼,也決定了哪些欄位可用、哪些必填:
{ "@type": "Article" }
{ "@type": "Product" }
{ "@type": "BreadcrumbList" }@id
給實體一個 唯一識別碼,通常用網址。它的用途是讓不同區塊、甚至不同頁面的標記 指向同一個實體:
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "文章標題",
"publisher": { "@id": "https://example.com/#organization" }
}@id 讓組織資訊只寫一次
整站的組織資訊在首頁完整描述一次並給它一個 @id,其他頁面用同一個識別碼參照就好,不必每頁重複寫 logo、sameAs、聯絡資訊。做法見 組織與網站 Organization 標記 。
巢狀物件
欄位的值可以是另一個實體,這是描述關係的方式:
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "文章標題",
"author": {
"@type": "Person",
"name": "Away",
"url": "https://example.com/about"
},
"publisher": {
"@type": "Organization",
"name": "F2E 前端工程文件",
"logo": {
"@type": "ImageObject",
"url": "https://example.com/site-logo.png"
}
}
}巢狀物件也需要自己的 @type
// 錯誤:解析器不知道這是什麼
"author": { "name": "Away" }
// 正確
"author": { "@type": "Person", "name": "Away" }只有 @context 不必重複,它在最外層宣告一次就涵蓋整段。
陣列的寫法
一個欄位可以有多個值:
{
"@context": "https://schema.org",
"@type": "Article",
"image": [
"https://example.com/16x9.png",
"https://example.com/4x3.png",
"https://example.com/1x1.png"
],
"author": [
{ "@type": "Person", "name": "作者一" },
{ "@type": "Person", "name": "作者二" }
]
}單一值也可以寫成陣列
"image": ["url"] 與 "image": "url" 都合法。統一用陣列可以讓程式產生標記時不必判斷數量。
多個區塊 vs @graph
同一頁需要多種型別時,有兩種寫法。
多個獨立區塊
<script type="application/ld+json">
{ "@context": "https://schema.org", "@type": "Article", "headline": "..." }
</script>
<script type="application/ld+json">
{ "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [] }
</script>用 @graph 裝在一起
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{ "@type": "Article", "@id": "https://example.com/page#article", "headline": "..." },
{ "@type": "BreadcrumbList", "itemListElement": [] }
]
}
</script>| 多個區塊 | @graph | |
|---|---|---|
| 可讀性 | 好,一個型別一段 | 較差,全部擠在一起 |
| 維護 | 好,可以獨立增減 | 需要動整段 |
| 實體互相參照 | 靠 @id 跨區塊參照 | 同一段內直接參照 |
| 建議 | 多數情況選這個 | 實體關係複雜時 |
這個站用多個區塊
每篇文章的檔頭都有 Article 與 FAQPage 兩段獨立的區塊,骨幹頁面再各自加上 BreadcrumbList 與 ItemList。分開寫的好處正好體現在這裡,多一個型別只是多一段,不必動到既有的標記,也比較容易看出哪一段出問題。
常見語法錯誤
這三個是最常見的
1. JSON 尾隨逗號
JSON 不允許尾隨逗號,整段會解析失敗。這在手寫時特別容易發生。
// 錯誤:最後一個欄位後面多了逗號
{
"@type": "Article",
"headline": "標題",
}2. 角括號原字元
// 危險:直接寫 HTML 標籤的角括號
"description": "說明 <p> 元素的用法"角括號寫在 <script> 內容裡時,容易誤觸解析規則。一律改用純名稱敘述:
"description": "說明段落元素的用法"3. 日期格式
// 錯誤
"datePublished": "2026/09/07"
"datePublished": "Sep 7, 2026"
// 正確:ISO 8601
"datePublished": "2026-09-07"
"datePublished": "2026-09-07T14:30:00+08:00"用程式產生而非手寫
手寫容易出錯,而且無法保證與頁面內容一致。正確做法是 從頁面資料產生:
// 從 frontmatter 產生 Article 標記
function buildArticleSchema(page, domain) {
return {
'@context': 'https://schema.org',
'@type': 'Article',
headline: page.title,
description: page.description,
image: [`${domain}/images/${page.slug}-og.png`],
author: { '@type': 'Person', name: 'Away', url: `${domain}/about` },
datePublished: page.datePublished,
dateModified: page.dateModified,
inLanguage: 'zh-Hant-TW',
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `${domain}/${page.path}`,
},
};
}網域用 token,建置時替換
絕對網址在本機、測試機、正式機都不同。在標記裡放一個 token,建置時再換掉:
const SITE_TOKEN = /%SITE%/g;
const replaceSiteToken = (value) =>
typeof value === 'string' ? value.replace(SITE_TOKEN, domain) : value;替換函式要能遞迴處理巢狀物件與陣列,否則深層的網址會漏掉。
SPA 的動態注入
風險比伺服器端渲染高
搜尋引擎渲染頁面後讀得到動態注入的標記,所以 技術上有效。但有兩個風險:
- 渲染有延遲:排在佇列裡,通常幾秒,但沒有保證。
- 程式碼出錯就全沒了:靜態寫在 HTML 裡的標記不會有這個問題。
最穩的做法是在 伺服器端渲染或建置階段 就寫進 HTML。
若只能動態注入,注意這些:
// 在頁面渲染完成前注入,並記得路由切換時清掉舊的
function injectSchema(data) {
const existing = document.querySelector('script[data-schema="page"]');
if (existing) existing.remove();
const script = document.createElement('script');
script.type = 'application/ld+json';
script.dataset.schema = 'page';
script.textContent = JSON.stringify(data);
document.head.appendChild(script);
}務必用 網址檢查工具 確認渲染後真的讀到了。 做法見 SPA 動態更新 title 與 meta 。
驗證流程
| 順序 | 工具 | 看什麼 |
|---|---|---|
| 1 | Schema Markup Validator | 純語法與型別是否合法 |
| 2 | 複合式搜尋結果測試 | 能否取得複合式結果、缺哪些欄位 |
| 3 | Search Console 強化報告 | 全站範圍的錯誤統計 |
前兩者測單頁、給即時結果;第三者看全站、有延遲但能發現整批錯誤。
自動化的部分可以在建置後掃一遍產出的 HTML:
// 抽出所有 ld+json 做 JSON 解析與必填欄位檢查
const blocks = html.matchAll(
/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g,
);
for (const [, json] of blocks) {
const data = JSON.parse(json); // 語法錯誤會在這裡拋出
if (!data['@context'] || !data['@type']) {
throw new Error('缺少 @context 或 @type');
}
}檢查清單
| 項目 | 標準 |
|---|---|
有 @context 且值為 Schema.org 的網址 | 必備 |
每個實體都有 @type | 必備 |
| JSON 語法正確(無尾隨逗號) | 必備 |
| 沒有角括號原字元 | 必備 |
| 日期符合 ISO 8601 | 必備 |
| 網址是絕對網址 | 必備 |
| 標記內容與頁面實際內容一致 | 必備 |
| 由程式產生而非手寫 | 建議 |
| 通過語法驗證與複合式結果測試 | 建議 |
常見問題
一頁可以放幾個 ld+json 區塊?
沒有數量限制,多個區塊會被合併理解。實務上按型別分開寫比較好維護,例如:文章一個區塊、麵包屑一個區塊。需要讓多個實體互相參照時,才改用 @graph 把它們裝在同一個區塊裡。
@context 一定要寫嗎?
一定要,而且值固定是 Schema.org 的網址。它宣告後面用的詞彙來自哪一套定義,少了它整段標記就無法解析。要注意用的是 https 開頭的版本。
@id 是做什麼的?
它給實體一個唯一識別碼,讓不同區塊或不同頁面的標記可以指向同一個實體。例如:整站的組織資訊只在首頁完整描述一次,其他頁面用同一個識別碼參照它,就不必重複寫全部欄位。
最常見的語法錯誤是什麼?
三個:JSON 尾隨逗號、把角括號原字元直接寫進字串裡、以及日期格式不符 ISO 8601。前兩者會讓整段標記解析失敗,第三者會讓那個欄位被忽略。
動態注入的結構化資料有效嗎?
搜尋引擎渲染頁面後讀得到就有效,但風險較高:渲染有延遲、程式碼出錯就全沒了。最穩的做法是在伺服器端渲染或建置階段就寫進 HTML。若只能動態注入,務必用網址檢查工具確認渲染後真的讀到了。
延伸閱讀
參考資料:JSON-LD 1.1 規格