結構化資料驗證:複合式搜尋結果測試與 Schema 檢查工具
結構化資料 寫完不要憑感覺,一個尾隨逗號就能讓整段標記失效,而且 完全沒有錯誤提示。
三個工具各管一段。
三個工具的分工
| 工具 | 檢查什麼 | 範圍 | 速度 |
|---|---|---|---|
| 複合式搜尋結果測試 | 能不能取得複合式結果、缺哪些欄位 | 單頁 | 即時 |
| Schema Markup Validator | 純語法與型別是否合法 | 單頁 | 即時 |
| Search Console 強化報告 | 全站範圍的錯誤統計 | 全站 | 數天延遲 |
三個都要用
它們管不同的事,缺一個就會有盲區:
- 只用複合式結果測試 → 看不到全站有幾頁出錯。
- 只用強化報告 → 改完要等好幾天才知道對不對。
- 只用通用驗證器 → 語法對了,但可能不符合搜尋引擎的顯示條件。
複合式搜尋結果測試
Rich Results Test 是最常用的一個。
兩種輸入模式
| 模式 | 適合 |
|---|---|
| 網址 | 測線上的頁面(也能看到渲染後的結果) |
| 程式碼 | 測還沒上線的標記、或本機的內容 |
程式碼模式可以測本機
本機的網址(localhost)它連不到。把建置產出的 HTML 貼進「程式碼」分頁就能測,這是上線前驗證的方式。
# 把產出的 HTML 複製到剪貼簿(Windows)
cat dist/seo/schema-article.html | clip報告怎麼讀
| 區塊 | 意思 |
|---|---|
| 偵測到的項目 | 找到哪些型別、各幾個 |
| 有效項目 | 通過,可以取得複合式結果 |
| 無效項目 | 有錯誤,不會 取得複合式結果 |
| 警告 | 缺少建議欄位,仍可能顯示 |
| 預覽 | 部分型別可以預覽搜尋結果的外觀 |
錯誤與警告的差別很重要
| 分級 | 意思 | 要不要修 |
|---|---|---|
| 錯誤 | 缺少 必填 欄位 | 必須修,否則完全不會顯示 |
| 警告 | 缺少 建議 欄位 | 依情況補,不影響能否顯示 |
常見的警告是「缺少 image」或「缺少 author.url」,補上會讓呈現更完整,但不補也還是有效。
它也看得到渲染後的結果
用網址模式測試時,「檢視測試過的網頁」會顯示 渲染後的 HTML,這對 動態注入的結構化資料 是必要的驗證:
動態注入一定要用這個確認
搜尋引擎渲染後讀不到那段標記的話,不管寫得多正確都沒用。搜尋渲染後的 HTML 裡有沒有 application/ld+json 就知道了。
也可以看「JavaScript 主控台訊息」有沒有程式碼錯誤,詳見 純 CSR 的 SEO 問題 。
Schema Markup Validator
validator.schema.org 是 Schema.org 官方的驗證器。
| 複合式結果測試 | Schema Markup Validator | |
|---|---|---|
| 檢查標準 | 搜尋引擎的顯示條件 | Schema.org 的定義 |
| 涵蓋型別 | 只有支援複合式結果的 | 全部型別 |
| 會報告 | 缺少必填欄位 | 型別與屬性是否存在、值的型態 |
什麼時候需要它
| 情況 | 為什麼 |
|---|---|
| 用了不支援複合式結果的型別 | 前者不認識,它認識 |
| 想確認屬性名稱有沒有拼錯 | 它會標出不存在的屬性 |
| 想確認嵌套結構合不合法 | 它會檢查型別關係 |
例如:HowTo 已經停止顯示複合式結果,複合式結果測試可能就不報告它了,但通用驗證器仍會檢查它的語法。
通過通用驗證器不等於能取得複合式結果
兩者標準不同。合法的 Schema.org 標記不一定符合搜尋引擎額外要求的必填欄位。
順序是先過通用驗證器(語法對),再過複合式結果測試(符合顯示條件)。
Search Console 強化報告
Search Console 會依您標記的型別自動出現對應報告(文章、麵包屑、商品、常見問答等)。
| 特性 | 說明 |
|---|---|
| 範圍 | 全站 |
| 延遲 | 數天 |
| 分級 | 錯誤、警告、有效 |
| 額外資訊 | 可以看到 哪些網址 有問題 |
它才抓得到「整批頁面同一個錯誤」
單頁測試只驗一頁。模板改壞了會讓幾百頁同時出錯,那只有全站報告看得出來。
而且它會列出具體的網址清單,可以直接拿去修。
一份完整的驗證流程
| 順序 | 做什麼 | 用什麼 |
|---|---|---|
| 1 | 建置後檢查語法與必填欄位 | 自己寫的腳本(見下方) |
| 2 | 確認型別與屬性合法 | Schema Markup Validator |
| 3 | 確認符合顯示條件 | 複合式搜尋結果測試 |
| 4 | 上線後確認全站 | Search Console 強化報告 |
第 1 步最該做卻最常被跳過
前三步都是人工的,一定會有忘記的時候。寫成腳本放進建置流程,才能保證每次都跑。
自動化驗證
建置後掃過產出的 HTML,抽出所有 ld+json 檢查:
// scripts/validate-schema.mjs
import { readdir, readFile } from 'node:fs/promises';
import path from 'node:path';
const DIST = 'dist';
// 各型別的必填欄位
const REQUIRED = {
Article: ['headline'],
BreadcrumbList: ['itemListElement'],
FAQPage: ['mainEntity'],
ItemList: ['itemListElement'],
Product: ['name'],
VideoObject: ['name', 'description', 'thumbnailUrl', 'uploadDate'],
};
const files = (await readdir(DIST, { recursive: true })).filter((f) =>
f.endsWith('.html'),
);
const errors = [];
for (const file of files) {
const html = await readFile(path.join(DIST, file), 'utf8');
const blocks = [
...html.matchAll(
/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g,
),
];
for (const [, json] of blocks) {
let data;
// 1. 語法檢查
try {
data = JSON.parse(json);
} catch (e) {
errors.push(`${file}: JSON 語法錯誤 — ${e.message}`);
continue;
}
// 2. 必要欄位檢查
for (const entity of [data].flat()) {
if (!entity['@context']) errors.push(`${file}: 缺少 @context`);
if (!entity['@type']) {
errors.push(`${file}: 缺少 @type`);
continue;
}
for (const field of REQUIRED[entity['@type']] ?? []) {
if (!entity[field]) {
errors.push(`${file}: ${entity['@type']} 缺少必填欄位 ${field}`);
}
}
}
// 3. 角括號原字元檢查(CLAUDE.md 的硬規定)
if (/[<>]/.test(json.replace(/&[a-z]+;/g, ''))) {
errors.push(`${file}: ld+json 內含角括號原字元`);
}
}
}
if (errors.length > 0) {
console.error(`發現 ${errors.length} 個結構化資料問題:`);
for (const e of errors) console.error(` ✗ ${e}`);
process.exit(1);
}
console.log(`✓ ${files.length} 個頁面的結構化資料通過檢查`);{
"scripts": {
"validate:schema": "node scripts/validate-schema.mjs",
"postbuild": "npm run validate:schema"
}
}這個腳本能抓到的三類問題
| 問題 | 為什麼重要 |
|---|---|
| JSON 語法錯誤 | 整段標記失效,而且沒有錯誤提示 |
| 缺少必填欄位 | 不會取得複合式結果 |
| 角括號原字元 | 容易誤觸 <script> 的解析規則 |
第三項是這個站特有的規範,ld+json 裡不寫 <title> 這類角括號,一律改用純名稱敘述。
更進一步的檢查
除了語法,還可以檢查 一致性:
// 檢查 Article 的 headline 是否與頁面的 h1 一致
const h1 = html.match(/<h1[^>]*>(.*?)<\/h1>/s)?.[1]?.replace(/<[^>]+>/g, '').trim();
const article = entities.find((e) => e['@type'] === 'Article');
if (article && h1 && !h1.includes(article.headline.slice(0, 20))) {
errors.push(`${file}: Article 的 headline 與頁面 h1 不一致`);
}// 檢查 FAQPage 的問答數與頁面的收合區塊一致
const details = [...html.matchAll(/<summary>([^<]+)<\/summary>/g)];
const faq = entities.find((e) => e['@type'] === 'FAQPage');
if (faq && details.length !== faq.mainEntity.length) {
errors.push(
`${file}: FAQPage 標記 ${faq.mainEntity.length} 組,頁面上有 ${details.length} 組`,
);
}驗證要對建置產物做
結構化資料裡的網址常常寫成 token(例如 %SITE%/seo/...),由設定檔在建置時替換成當前環境的網域。
不要拿原始檔去驗證
原始檔裡的網址還是 token,貼進線上工具會被判定為無效網址。驗證一律對建置產物做。
正確的順序:
# 1. 建置(此時網址的 token 已被替換)
npm run build
# 2. 對產出的 HTML 跑自動化檢查
npm run validate:schema
# 3. 把某一頁的 HTML 貼進線上工具做最終確認
cat dist/seo/schema-article.html | clip檢查清單
| 項目 | 標準 |
|---|---|
| 建置後有自動檢查 JSON 語法 | 建議 |
| 檢查必填欄位 | 建議 |
檢查 ld+json 無角括號原字元 | 建議 |
| 通過 Schema Markup Validator | 建議 |
| 通過複合式搜尋結果測試(無錯誤) | 建議 |
| 動態注入的標記已確認渲染後讀得到 | 必備 |
| 標記與頁面內容一致(標題、問答數) | 必備 |
| 上線後看過 Search Console 強化報告 | 建議 |
| 驗證對象是建置產物而非原始檔 | 必備 |
常見問題
三個工具該用哪一個?
都要用,它們管不同的事。複合式搜尋結果測試看單頁能不能取得特殊呈現、通用驗證器看純語法與型別是否合法、Search Console 的強化報告看全站範圍的錯誤統計。前兩者即時,後者有延遲但涵蓋整站。
錯誤和警告差在哪?
錯誤表示缺少必填欄位,那個型別完全不會取得複合式結果;警告表示缺少建議欄位,仍可能顯示但資訊較少。錯誤必須修,警告依情況補。
驗證通過為什麼還是沒出現複合式結果?
驗證通過只代表語法與必填欄位正確,不保證會顯示。搜尋引擎仍會依內容品質、網站權重與該次搜尋的情境決定要不要用。此外部分型別已被官方縮減或停止顯示,例如:HowTo 。
為什麼通用驗證器通過但複合式結果測試失敗?
因為兩者的標準不同。通用驗證器只看 Schema.org 的定義是否合法,複合式結果測試還檢查搜尋引擎額外要求的必填欄位。合法的標記不一定符合特定搜尋引擎的顯示條件。
可以在本機驗證嗎?
官方工具需要公開網址,但可以用貼上原始碼的模式測試本機的內容。另外語法與必填欄位的檢查完全可以寫成腳本在建置後跑,那才是能擋在提交階段的做法。