◆ wport | pm_33

SEO 結構優化 Mockup PRD(給後端 API 規格產生器使用)

SEO 結構優化

來源 doc/feature/pm_33/seo-structure-optimization-mockup.md

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)輸出:canonicalhreflang(含 x-default)、JobPosting JSON-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
  • 排除範圍
    • 不做 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})
JobPostingJsonLdJobPosting 結構化資料(JSON-LD)JobSeoPageModel 的輸出
JobSeoRedirect舊 URL/slug 歷史的 301 轉址對應路由層(Redirect)
JobSitemapEntrysitemap 條目(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-1Job Detail SSR Head輸出 canonical / hreflang / JobPosting JSON-LD(依 indexability)
SEC-2URL 正規化與 Redirecthash/slug/參數的 301 策略與 canonical 收斂
SEC-3Dynamic Sitemap後端產出 sitemap(切檔、lastmod)

6.2 各區塊資料需求

SEC-1 Job Detail SSR Head

  • 資料需求:JobSeoPageModel(含 indexability 判斷、canonical URL、JobPosting fields)
  • 輸出規則
    • indexable=true:輸出有效 JobPosting JSON-LD、canonical/hreflang(含 x-default)
    • indexable=false:回 200 + noindex,且 不得輸出可索引的 JobPosting

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)✅ 是JobSeoPageModelGooglebot/使用者請求職缺詳情頁 → 依 indexability 輸出 head/schema
ACT-2追蹤參數 Canonical 收斂路由/判斷(Read)✅ 是JobSeoPageModel任意 utm_*/第三方參數存在時,canonical 永指向乾淨職缺 URL
ACT-3舊 URL 301 正規化路由(Read)✅ 是JobSeoRedirect/job/[hash]/[lang]/job/{id}/{slug}
ACT-4Sitemap 生成/切檔排程/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
    • 不輸出有效的 JobPosting JSON-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 僅作裝飾/可讀性
    • 範例
      • 原職稱:Software Engineer (Java) - 台北市!
      • 轉換後:software-engineer-java-台北市
  • 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」取得來源(職缺、公司驗證、職缺狀態、語系資料)
  • 產出 JobPosting JSON-LD 欄位 mapping(依你們資料庫欄位能否對齊 Google 規範)
  • 與後端落地 SSR 首屏 head 輸出,並用 Google Rich Results 測試/網址檢查工具驗證
  • 待上線後在 Search Console 建立 baseline,追蹤 JobPosting 相關報表(富媒體搜尋結果/JobPosting 狀態)

文件結束