SEO 結構優化 Mockup PRD(給後端 API 規格產生器使用)
1. 文件資訊
- 文件類型:Mockup 需求規格書(前後端共用視圖)
- 適用對象:前端開發、後端開發、產品、QA
- 最後更新:2026/04/02
- 版本:1.0.0
- 對應 Storybook:無(本功能以 SSR 首屏 HTML 的結構化資料/Head 輸出與 Sitemap/Redirect 為主,無獨立 UI Mockup)
- 建立日期:2026/04/02
2. 開發進度與設計來源
開發進度
- 前端 PR #:TBD
- 後端 PR #:TBD
設計來源
- Figma 連結:無
- 既有畫面參照:
- 用於描述「URL/多語/參數」策略的參照頁(LinkedIn 作為對齊理解用)
- 以現有職缺頁作為 SEO baseline(Before/After)
3. 模擬頁面摘要(Mockup Summary)
3.1 功能概述
- 功能名稱:SEO 結構優化(JobPosting JSON-LD + Sitemap + Canonical/hreflang + Redirect)
- 主要使用者角色:Job Seeker(僑外生/外籍求職者)、Employer(刊登職缺企業)、系統(Googlebot/AI 爬蟲)
- 核心目標:
- 讓 Google 可正確抓取並理解「職缺」的結構化資料(JobPosting),提升自然流量與品牌詞曝光
- 使用多語系 + 正確 hreflang,避免五種語系被誤認重複內容,且能讓行銷針對不同url下廣告
- 以乾淨 canonical URL 收斂追蹤參數權重,避免「網址怪獸」
- 以 sitemap 動態產出可索引的職缺清單,提升收錄效率與一致性
3.2 範圍界定
- 包含範圍:
- 職缺詳情頁首屏 HTML(SSR)輸出:
canonical、hreflang(含x-default)、JobPostingJSON-LD - 多語系的 canonical/hreflang 策略(每語系 self-canonical + hreflang 互標)
- URL 正規化與 Redirect:
- 舊
/job/[hash]301 到新/[lang]/job/[id]/[slug](或既定最終路徑) - slug 變更:舊 slug 301 到最新 canonical slug(ID->canonical slug 對應)
- 追蹤參數(UTM/LinkedIn 等)存在時,內容與 canonical 不衝突(canonical 永指向乾淨職缺 URL)
- 舊
- Sitemap(後端動態生成、切分、lastmod)
- 不可索引職缺:回 HTTP 200 + noindex(且不得輸出有效 JobPosting)
- 刪除/下架職缺:回 HTTP 410(或 404,依實作選一),並確保不再輸出可被當成有效職缺的 JobPosting
- 職缺詳情頁首屏 HTML(SSR)輸出:
- 排除範圍:
- 不做 JobPosting 以外的其他 schema(本功能以 Google JobPosting 為主)
- 不做 UI 版型改版(只動結構:Head/JSON-LD/Sitemap/Redirect 與必要的路由輸出)
- 本階段不含 Indexing API(Future Scope)
- 不處理下架頁「推薦職缺 UI」(下架/刪除僅由 410/404 + SEO Head/Schema/索引一致性處理)
3.3 成功條件
- Search Console:在「職航站 / WPORT 職航站」相關查詢維度上曝光與點擊上升
- 在搜尋引擎打入「職航站」等關鍵字可找到對應的職缺頁
- AI/搜尋引擎更容易抓取:JobPosting Rich Result 驗證有效率提升(以 GSC 富媒體/JobPosting 相關報表為準)https://search.google.com/test/rich-results
4. 商業規則對齊(Business Rules Alignment)
供 backend-api-spec-generator 對齊
prd/business-rules.md。
4.1 本功能使用到的業務規則
| 規則 ID | 規則摘要 | 是否關鍵 | 備註 |
|---|---|---|---|
| BR-005 | 公司未驗證可增職缺但無法上架 | ✅ 是 | SEO 僅納入可上架公司頁/公開職缺 |
| BR-008 | 職缺發布條件(必填欄位)與公司驗證/Logo 限制 | ✅ 是 | Schema 與頁面可見內容需一致 |
| BR-009 | 職缺狀態管理(上架/下架/已刪除/已關閉)與可見性 | ✅ 是 | 不可索引與 410 規則依狀態落地 |
| BR-015 | 職缺搜尋規則(只顯示上架職缺) | ✅ 是 | Sitemap/可索引清單與搜尋可見邏輯一致 |
4.2 補充說明(可選)
- JobPosting JSON-LD 的欄位輸出只使用「頁面上實際可展示的對應資料」,避免與頁面內容不符導致 Manual Action 風險。
- 隱私:不得將應徵流程中的「聯絡人姓名、私人電話、內部備註」等寫入公開 Schema;只輸出公開可對應欄位(公司名稱、職務描述、地點、應徵資訊的公開部分等)。
5. 資料實體與欄位(Data Entities & Fields)
5.1 實體列表
| 實體名稱 | 說明 | 來源頁面/區塊 |
|---|---|---|
| JobSeoPageModel | 職缺 SEO 首屏輸出所需資料(SSR) | 職缺詳情頁(/{lang}/job/{id}/{slug}) |
| JobPostingJsonLd | JobPosting 結構化資料(JSON-LD) | JobSeoPageModel 的輸出 |
| JobSeoRedirect | 舊 URL/slug 歷史的 301 轉址對應 | 路由層(Redirect) |
| JobSitemapEntry | sitemap 條目(loc/lastmod) | sitemap 產出 |
5.2 實體欄位定義(TypeScript 介面)
JobSeoPageModel
type LangCode = string; // e.g. 'zh' | 'en' | ...
interface JobSeoPageModel {
job_id: string; // stable id
lang: LangCode;
canonical: {
url: string; // 乾淨 canonical(忽略 UTM/追蹤參數)
slug: string; // canonical slug
};
// 索引判斷(由既有 job/company 狀態計算)
indexability: {
indexable: boolean; // true => indexable (noindex=false)
reason: string; // 用於 log/除錯(不暴露給前端)
};
// JobPosting JSON-LD(僅在 indexable 時輸出有效值)
jobPosting?: JobPostingJsonLd;
}
JobPostingJsonLd(Google JobPosting 格式為主)
interface JobPostingJsonLd {
"@context": "https://schema.org/";
"@type": "JobPosting";
title: string;
description: string; // HTML string(需與頁面工作說明一致)
identifier?: {
"@type": "PropertyValue";
name: string; // e.g. company name
value: string; // e.g. job id or external id
};
datePosted?: string; // ISO 8601(依你們資料可得性)
validThrough?: string; // 下架/到期日前置(若無可不填)
employmentType?: string; // e.g. FULL_TIME / PART_TIME ...
hiringOrganization?: {
"@type": "Organization";
name: string;
logo?: string;
};
jobLocation?: {
"@type": "Place";
address?: {
"@type": "PostalAddress";
addressCountry?: string;
addressLocality?: string;
addressRegion?: string;
};
};
jobLocationType?: "TELECOMMUTE" | string;
applicantLocationRequirements?: {
"@type": "Country" | "AdministrativeArea";
name: string; // 對應 Google 規範,避免造假
} | Array<{
"@type": "Country" | "AdministrativeArea";
name: string;
}>;
baseSalary?: {
"@type": "MonetaryAmount";
currency: string;
value: {
"@type": "QuantitativeValue";
value: number;
unitText: "HOUR" | "DAY" | "WEEK" | "MONTH" | "YEAR";
};
};
}
JobSitemapEntry
interface JobSitemapEntry {
loc: string; // URL(乾淨 canonical)
lastmod: string; // ISO date
}
6. 畫面區塊與資料需求(UI Sections & Data Needs)
6.1 區塊列表
| 區塊 ID | 區塊名稱 | 說明 |
|---|---|---|
| SEC-1 | Job Detail SSR Head | 輸出 canonical / hreflang / JobPosting JSON-LD(依 indexability) |
| SEC-2 | URL 正規化與 Redirect | hash/slug/參數的 301 策略與 canonical 收斂 |
| SEC-3 | Dynamic Sitemap | 後端產出 sitemap(切檔、lastmod) |
6.2 各區塊資料需求
SEC-1 Job Detail SSR Head
- 資料需求:JobSeoPageModel(含 indexability 判斷、canonical URL、JobPosting fields)
- 輸出規則:
- indexable=true:輸出有效
JobPostingJSON-LD、canonical/hreflang(含 x-default) - indexable=false:回 200 +
noindex,且 不得輸出可索引的 JobPosting
- indexable=true:輸出有效
SEC-2 URL 正規化與 Redirect
- 資料需求:
- hash -> job_id -> canonical slug 查詢
- job_id -> canonical slug(slug 變更時)
SEC-3 Dynamic Sitemap
- 資料需求:
- 可索引職缺集合(對齊上架+公司驗證可見性邏輯)
- lastmod 時間來源(職缺/內容更新時間)
7. 使用者動作與後端需求(User Actions & Backend Needs)
| 動作 ID | 動作名稱 | 類型 | 需要後端 | 涉及實體 | 說明 |
|---|---|---|---|---|---|
| ACT-1 | 職缺頁 SSR 產出 Head | 查詢(Read) | ✅ 是 | JobSeoPageModel | Googlebot/使用者請求職缺詳情頁 → 依 indexability 輸出 head/schema |
| ACT-2 | 追蹤參數 Canonical 收斂 | 路由/判斷(Read) | ✅ 是 | JobSeoPageModel | 任意 utm_*/第三方參數存在時,canonical 永指向乾淨職缺 URL |
| ACT-3 | 舊 URL 301 正規化 | 路由(Read) | ✅ 是 | JobSeoRedirect | /job/[hash] → /[lang]/job/{id}/{slug} |
| ACT-4 | Sitemap 生成/切檔 | 排程/Scheduled | ✅ 是 | JobSitemapEntry | 動態產出 sitemap(依語系切分、lastmod) |
8. API Hints(提示用,非最終 API 設計)
8.1 資料讀取需求(Read)
-
HINT-GET-1:取得 JobSeoPageModel(含 indexability 與 JobPosting fields)
- 對應區塊:SEC-1
- 輸入:
{lang, job_id}或{hash} - 輸出:
JobSeoPageModel
-
HINT-GET-2:取得 job_id -> canonicalSlug / canonicalLang 的映射
- 對應區塊:SEC-2
- 輸入:
{hash}或{job_id} - 輸出:
JobSeoRedirect
8.2 資料寫入需求(Write)
- HINT-MUTATION-1:職缺狀態變更後更新 Sitemap 快取/切檔結果(如有快取層)
- 行為:不可改變 URL canonical 規則;僅影響 sitemap 檔案內容與 lastmod
9. 導航 / Storybook Map
- Storybook 標題:無
- Stories 路徑:TBD
- Storybook 連結:TBD
- 對應 production 路由:
/{lang}/job/{id}/{slug}- 舊:
/job/{hash}(301 到新規則)
10. Flowcharts(User / System / Navigation)
10.1 User Flow(Googlebot/使用者請求職缺頁)
flowchart TD
U1[Googlebot 或使用者開啟職缺 URL<br/>/job/[hash]?utm=... 或 /{lang}/job/{id}/{slug}?utm=...] --> U2{路由正規化?}
U2 -->|是| R[301 轉址到 canonical 乾淨職缺 URL]
U2 -->|否| V{職缺可索引?}
V -->|否| N[回 200 + noindex(不輸出有效 JobPosting)]
V -->|是| I[回 200 + SSR 首屏輸出 JobPosting JSON-LD + canonical/hreflang]
10.2 System Flow(SSR 首屏 Head 輸出)
sequenceDiagram
participant Req as Request(URL)
participant Router as Router/Redirect
participant SeoSvc as SEO Service
participant Render as SSR Renderer
Req->>Router: 解析語系/ID/Hash + 追蹤參數
Router->>SeoSvc: 取得 JobSeoPageModel(含 indexability & canonical)
SeoSvc-->>Router: JobSeoPageModel(或判斷 410/不可索引)
Router->>Render: 渲染首屏 HTML(含 head 規則)
Render-->>Req: HTTP 200(indexable => JobPosting;不可索引 => noindex)
10.3 Navigation / Redirect Flow(舊 hash 與 slug 變更)
flowchart TD
A[/job/[hash]] --> B[Lookup job_id + canonical slug]
B --> C[Build canonical URL: /{lang}/job/{id}/{canonicalSlug}]
C --> D[301 Redirect]
E[/{lang}/job/{id}/{oldSlug}] --> F{oldSlug 是否為 canonicalSlug?}
F -->|否| G[301 到最新 canonicalSlug]
F -->|是| H[200(僅忽略 UTM/追蹤參數對 canonical 的影響)]
11. Mock Data Schema(Mock 資料結構)
- 資料來源:本章僅提供 mock 示例(不呼叫真實 API)
- CRUD 行為:mock 僅展示輸出結構與判斷規則
Mock 範例(indexable=true):
{
"job_id": "123",
"lang": "zh",
"canonical": {
"url": "https://example.com/zh/job/123/job-slug-name",
"slug": "job-slug-name"
},
"indexability": { "indexable": true, "reason": "published+company_verified" },
"jobPosting": {
"@context": "https://schema.org/",
"@type": "JobPosting",
"title": "軟體工程師",
"description": "<p>工作內容...</p>",
"identifier": { "@type": "PropertyValue", "name": "WPORT", "value": "123" }
}
}
12. 實作備註(Implementation Notes)
-
SSR 為預期交付方式:JobPosting JSON-LD 與 canonical/hreflang 必須能在「首屏 HTML」被 Googlebot 讀到。
-
結構化資料主體:以 Google
JobPosting格式輸出。 -
不可索引(indexable=false)規則(D2 已確認):
- HTTP 200
- 輸出
noindex - 不輸出有效的
JobPostingJSON-LD(避免內容/狀態不符)
-
刪除/下架(已刪除/過期)規則(D2 已確認:410):
- HTTP 410(或 404;本功能先按 410 實作)
- 不輸出可被當成有效職缺的
JobPosting - Sitemap 中不包含不可索引職缺
-
Canonical 規則(偏好:參數 URL canonical 到乾淨職缺):
- 任意
utm_*/ 第三方搜尋參數存在時,rel="canonical"永指向乾淨職缺 URL
- 任意
-
hreflang 規則(含 x-default):
- 多語系各自 self-canonical
- hreflang=“x” 互相標註;並包含 x-default
-
Slug 規範與 Sanitization(供後端生成/清理):
- 允許字元(Format):Slug 僅允許「中文字、小寫英文字母、數字與連字號
-」 - 清理邏輯(Cleaning):
- 移除所有特殊符號(例如:
&,!,(,),[,],@,#,$等) - 將所有「空格」與其他分隔字元視為 word boundary,轉為「單一連字號
-」(例如多個空格/連續空白只保留一個-) - 移除或折疊重複連字號(
--->-),並避免 slug 頭尾出現-(必要時 trim) - 英文字母一律轉為小寫(大寫 -> 小寫),以符合允許字元規範
- 長度限制:建議截斷至前 20-30 個字元(避免 URL 過長;實作可採 24 或 28 為固定值)
- 移除所有特殊符號(例如:
- Encoding 偏好(URL 編碼):
- 後端需要知道是否要接受「原生中文網址(UTF-8)」或要轉成英文(例如 pinyin)
- 2026 年偏好:接受中文(UTF-8),但仍需確保完成上述 sanitization
- 唯一性說明(Collision Policy):
- 若兩個職稱 slug 相同(例如職稱一樣但不同職缺),不需要擔心,因為 URL 中仍包含
/job/{id}/...,id才是唯一識別;slug 僅作裝飾/可讀性
- 若兩個職稱 slug 相同(例如職稱一樣但不同職缺),不需要擔心,因為 URL 中仍包含
- 範例:
- 原職稱:
Software Engineer (Java) - 台北市! - 轉換後:
software-engineer-java-台北市
- 原職稱:
- 允許字元(Format):Slug 僅允許「中文字、小寫英文字母、數字與連字號
-
slug 變更規則(301 路由):
- 舊 slug 需保留 301 redirect 到最新 canonical slug
- 當 ID 對但 slug 不對時:自動 301 到「最新的正確 slug」(避免依 URL guess)
- canonical slug 由 id->canonical mapping 取得(而非僅使用 URL 內 slug 來反推)
-
隱私禁令:
- 公開 Schema 禁止包含應徵流程中的聯絡人姓名、私人電話、內部備註等
-
薪資揭露一致性:
- 若頁面顯示薪資或法規要求揭露,schema 的薪資欄位必須與頁面一致(避免 Manual Action)
13. 下一步(Next Steps)
- 與後端確認「JobSeoPageModel」取得來源(職缺、公司驗證、職缺狀態、語系資料)
- 產出
JobPostingJSON-LD 欄位 mapping(依你們資料庫欄位能否對齊 Google 規範) - 與後端落地 SSR 首屏 head 輸出,並用 Google Rich Results 測試/網址檢查工具驗證
- 待上線後在 Search Console 建立 baseline,追蹤 JobPosting 相關報表(富媒體搜尋結果/JobPosting 狀態)
文件結束