# teacher.place 公開課程 API v1

讀取已上架課程、章節與公開試閱教材，用來製作自訂課程頁。API 根網址是 `https://你的教學站台/api/v1`；請使用教學站台的網域，不是 teacher.place 平台首頁。

公開資料可直接使用 GET 讀取，CORS 為 `Access-Control-Allow-Origin: *`。fetch 請使用 `credentials: 'omit'`，不需加入 Authorization、X-API-Key 或 Content-Type 等額外 header。

## 1. 取得課程

```js
const base = 'https://school.example.com/api/v1';
const courseId = 'COURSE_ID'; // 從後台課程編輯網址取得
async function read(path) {
  const response = await fetch(base + path, { credentials: 'omit' });
  const result = await response.json();
  if (!response.ok) throw new Error(result.error.message);
  return result.data;
}
const course = await read('/courses/' + encodeURIComponent(courseId));
document.querySelector('#title').textContent = course.name;
document.querySelector('#price').textContent = course.price === 0
  ? '免費' : 'NT$ ' + course.price.toLocaleString('zh-TW');
```

`GET /courses/{courseId}` 回傳 `{data: Course}`：

- id、name、description、introductionHtml：課程基本資訊與介紹。
- coverUrl、hero.type、hero.imageUrls、hero.videoUrl：封面與介紹影片。
- price：TWD 整數金額，0 為免費；currency 固定 TWD。
- paymentType：one_time 或 subscription。
- learningPoints、includedItems：課程重點及包含內容。
- links：course、login、register、checkout、learning，均為原站台的完整連結。

每次載入重新取得資料，不要把課程名稱、售價與章節寫死在產生的 HTML。只有已上架課程可讀取；未上架或不存在回 404。

### 一頁式頁面的整合資料

`GET /courses/{courseId}/page-data` 一次回傳站台、課程、章節與操作連結，格式固定為 `course-page.v1`：

```json
{
  "data": {
    "schemaVersion": "course-page.v1",
    "site": { "id": "SITE_ID", "name": "教學網站", "logoUrl": "" },
    "course": { "id": "COURSE_ID", "name": "課程名稱", "price": 1200, "priceText": "NT$ 1,200", "isFree": false },
    "chapters": [],
    "links": { "course": "...", "login": "...", "register": "...", "checkout": "...", "learning": "..." }
  }
}
```

這是平台 Liquid 銷售頁使用的相同公開資料格式，也適合外部工程師製作一頁式頁面。`course` 另包含 `introductionText`、`chapterCount`、`unitCount`；`chapters` 的章節與單元會補上由 1 開始的 `index`。完整欄位定義見 `/api/openapi.json`。

## 2. 取得章節

`GET /courses/{courseId}/chapters` 回傳 `{data: Chapter[]}`，依後台排序，僅包含已發布章節／單元，排除 review_only 單元。

```json
{"data":[{"id":"CHAPTER_ID","name":"第一章","units":[{"id":"UNIT_ID","name":"工具介紹","preview":true},{"id":"PAID_UNIT_ID","name":"進階技法","preview":false}]}]}
```

preview=true 表示公開試閱；false 只提供單元名稱，不會提供付費教材。

## 3. 點擊後取得試閱教材

`GET /courses/{courseId}/units/{chapterId}/{unitId}` 僅提供已發布、preview=true 的教材。直接指定非試閱單元回 403；學生是否登入、是否購課都不會改變這個公開接口的結果。

回應 `{data: {id, chapterId, name, contentHtml, video}}`。video 可以是 null，或以下格式：

- `{provider: "url", url}`：直接影片網址或 YouTube/Vimeo 連結，使用對應播放器。
- `{provider: "vimeo", videoId}`：Vimeo ID，可包含 `?h=...` 私人分享參數。
- `{provider: "mux", playbackId, signed, expiresAt?, tokens?}`：signed=true 時使用 tokens.playback 作播放憑證；約一小時後重新 GET。不能把短效播放憑證存進 HTML。一般瀏覽器播放 HLS 可能需要相容播放器。

使用教材前清理 HTML，勿執行其中的 script 或 on* 事件。未開放試閱的內容請連至原站台 learning 頁面。

## 4. 登入與購課使用連結

從課程回應取得 links，放進按鈕即可：

```js
for (const action of ['login', 'register', 'checkout', 'learning']) {
  const link = document.querySelector('#' + action);
  if (!link) continue;
  link.href = course.links[action];
  link.target = '_blank';
  link.rel = 'noopener noreferrer';
}
```

login/register 開啟原站台課程頁的登入／註冊介面。checkout 將課程帶入原結帳頁購物車，仍由學生確認後建單；訂閱型課程回原課程頁選方案。learning 開啟原教學頁，使用既有的登入與購課驗證。

API 不提供註冊、登入、學生 Token、訂單查詢、下單或金流回呼。不要呼叫 POST，也不要把使用者回到銷售頁當成已登入或已付款。

## 回應與錯誤

成功固定為 `{data: ...}`，錯誤固定為 `{error: {code, message}}`。請檢查 HTTP status，顯示錯誤與重試入口，不以假資料掩蓋。

| HTTP | 說明 |
| --- | --- |
| 400 | 識別碼格式錯誤。 |
| 403 | preview_required：單元未開放試閱。 |
| 404 | 站台、課程、章節或單元不存在／未發布，或接口已移除。 |
| 405 | method_not_allowed：只支援 GET（另有 HEAD、OPTIONS）。 |
| 409 | invalid_price：課程售價設定有誤。 |
| 429 | rate_limited：依 Retry-After 秒數稍後重試。 |
| 503 | video_unavailable：試閱影片播放設定尚未完成。 |
| 500 | internal_error：服務暫時無法完成請求。 |

目前每個服務程序按站台與連線來源限制 240 次／分鐘；不需要在後台設定。HEAD 不回傳 body，OPTIONS 用於瀏覽器預檢，不會寫入資料。

完整欄位與回應範例見 `/api/openapi.json`。新舊呼叫者都應使用上述 GET 接口，移除舊 key/header 及來源設定流程。
