議程 difficulty / track / tags metadata 調查與補齊提案
整理一下 2025 怎麼處理「議程難度(difficulty)/議程軌(track)/議程標籤(tags)」,以及 2026 目前缺什麼、要怎麼補。檔案行號以目前 main 為準。
一、2025 的做法(COSCUP/2025)
2025 是 VitePress + defineLoader,在 build time 直接打 Pretalx API 把資料 merge 好。核心檔案:
loaders/allSubmissions.zh-tw.data.ts / allSubmissions.en.data.ts:merge 進場
loaders/pretalx/client.ts:Pretalx API client,負責 fetch + 正規化
loaders/pretalx/pretalx-types.ts:question id 對應表、difficulty 正規化表、翻譯表
loaders/pretalx/types.ts:資料型別
loaders/pretalx/utils.ts:getAnswer()、extractLocalizedStructure()
1. track(議程軌)— 來自 Pretalx tracks 端點
client.ts 的 getTracks() 打 /api/events/{event}/tracks/,回傳 { id, name, description }(name 為多語):
// loaders/pretalx/client.ts getTracks()
const url = this.#client.buildUrl<TracksListData>({
url: '/api/events/{event}/tracks/',
path: { event: this.event }, // coscup-2025
query: { page_size: PAGE_SIZE, page: 1 },
})
const pretalxTracks = await this.#getPaginatedResources<TrackReadable>(url)
return pretalxTracks.map((track) => ({
id: track.id,
name: formatMultiLingualString(track.name),
description: track.description ? formatMultiLingualString(track.description) : undefined,
}))
merge 發生在 allSubmissions.zh-tw.data.ts:把 tracks 做成 Map<id, Track>,再用 submission.track(id)查回完整 track 物件塞進每筆 session:
// loaders/allSubmissions.zh-tw.data.ts
const allTracks = toMapById(await pretalxClient.getTracks())
...
const track = allTracks.get(submission.track)
if (!track) throw new BadServerSideDataException(`Track ${submission.track} not found`)
const localizedTrack = extractLocalizedStructure(track, 'zh-tw')
return ({ ...extractLocalizedStructure(submission, 'zh-tw'), room, speakers, track: localizedTrack })
UI 端 components/Session/SessionModal.vue 把 session.track.name 當成主標籤顯示(L132–135),SessionsSchedule.vue 也用 track 當「社群(Community)」篩選器(L62–68、L174–176)。
2. difficulty(難度)— 來自 submission 的 answers(自訂問題)
難度不是獨立端點,而是投稿者在 CfP 回答的自訂問題。pretalx-types.ts 定義 question id:
// loaders/pretalx/pretalx-types.ts
export const coscupSubmissionsQuestionIdMap = {
Language: 269,
LanguageOther: 300,
EnTitle: 257,
EnDesc: 259,
Difficulty: 270, // ← 難度
...
}
client.ts 的 getSubmissionsOf() 在抓 submissions 時帶 expand: ['answers', 'slots'],再用 getAnswer(answers, 270) 取出原始字串,接著做兩段處理:正規化(把各種寫法收斂成 4 個 enum)與多語在地化:
// loaders/pretalx/client.ts getSubmissionsOf()
const difficulty = getAnswer(submission.answers, coscupSubmissionsQuestionIdMap.Difficulty)
const generalizedDifficulty = difficulty ? difficultyGeneralizeMap[difficulty] : undefined
const localizedDifficulty = getLocalizedDifficulty(
generalizedDifficulty, tagTranslations, { 'zh-tw': '未知', 'en': 'Unknown' },
)
// → 輸出 difficulty: MultiLingualString
正規化/翻譯表(pretalx-types.ts):
export const difficultyGeneralizeMap = {
初學者:'Elementary', 入門:'Elementary', 中階:'Intermediate', 進階:'Advanced', 專業:'Professional',
Beginner:'Elementary', Intermediate:'Intermediate', Advanced:'Advanced', Professional:'Professional', ...
}
export const tagTranslations = {
'zh-tw': { Elementary:'入門', Intermediate:'中階', Advanced:'進階', Professional:'專業', ... },
'en': { Elementary:'Elementary', ... },
}
3. tags(議程標籤)— 2025 其實是 language + difficulty 的合成
注意:2025 UI 上講的「Tags / 議程標籤」並不是 Pretalx 的 tags table,而是把 language 和 difficulty 兩個多語欄位當成標籤呈現+篩選:
<!-- components/Session/SessionModal.vue L123–129 -->
<section class="session-tags">
<CTag variant="secondary">{{ session.language }}</CTag>
<CTag variant="secondary">{{ session.difficulty }}</CTag>
</section>
// components/Session/SessionsSchedule.vue L71–76(tags 篩選選項)
props.submissions.forEach(({ language, difficulty }) => {
optionsMap.set(`language:${language}`, language)
optionsMap.set(`difficulty:${difficulty}`, difficulty)
})
小結 2025:track 走 /tracks/ 端點並以 id merge;difficulty 走 answers(Q270)→ 正規化 → 多語化;「tags」是 language+difficulty 的前端合成,並非獨立資料來源。
二、2026 現況與缺漏(COSCUP/2026)
2026 改成 Nuxt server route(server/api/session/...),先把 Pretalx 各 table 抓下來快取,再於 handler 組裝。相關檔案:
server/api/session/index.get.ts(列表)、server/api/session/[id]/index.get.ts(單筆)
server/utils/pretalx/index.ts(抓哪些 table)
server/utils/pretalx/parser.ts(parseAnswer 等)
shared/types/pretalx.ts、shared/types/session.ts(schema / 型別)
缺漏盤點:
(a) tags 被寫死成空陣列。 server/api/session/index.get.ts L44 與 [id]/index.get.ts 對應位置都是:
而 shared/types/session.ts 的 SessionSummarySchema 也只有 tags: z.array(z.string()),沒有 difficulty、沒有 track 欄位。
(b) difficulty 已經 parse 出來,但沒有被輸出。 server/utils/pretalx/parser.ts 的 QUESTION_MAP 已包含 difficulty: 270,parseAnswer() 會回傳 answers.difficulty(原始字串)——但兩個 session handler 都只用了 answers.language,answers.difficulty 被丟掉,也沒有 2025 那套正規化/多語化。
(c) track 完全沒被 fetch。 server/utils/pretalx/index.ts 只抓 6 張表:
fetchPretalxTable('submissions'), ('submission-types'), ('speakers'),
('rooms'), ('answers'), ('slots') // ← 沒有 tracks、沒有 tags table
shared/types/pretalx.ts 的 PRETALX_TABLES 同樣只列這 6 張。但好消息是 SubmissionSchema 已經保留了原始 id:
// shared/types/pretalx.ts SubmissionSchema
track: z.number().nullable().optional(),
tags: z.array(z.number()), // ← Pretalx 真的有 tags(id 陣列)
也就是說每筆 submission 上的 track(id)與 tags(id 陣列)資料其實都拿得到,只是沒有對應的 tracks/tags table 可以把 id 換成名稱,最後也沒寫進輸出。
補充:2026 的 tags 對應的是 Pretalx tags table(/api/events/{event}/tags/,欄位 { id, tag, description, color }),語意上比 2025「language+difficulty 合成」更乾淨。建議 2026 直接走 Pretalx tags table,difficulty 另外獨立成欄位。
三、補齊提案(讓 2026 /api/session 帶上 difficulty / track / tags)
照 2025 的 pattern,分三塊,盡量複用 2026 既有結構。
1. 新增 tracks / tags 兩張 table 的抓取與 schema
shared/types/pretalx.ts:
export const PRETALX_TABLES = [
'submissions', 'submission-types', 'speakers', 'rooms', 'answers', 'slots',
'tracks', 'tags', // ← 新增
] as const
export const TrackSchema = z.object({
id: z.number(),
name: PretalxLocaleSchema,
description: PretalxLocaleSchema.nullable().optional(),
})
export const TagSchema = z.object({
id: z.number(),
tag: z.string(), // Pretalx tag 顯示字串
description: PretalxLocaleSchema.nullable().optional(),
color: z.string().optional(),
})
export const PRETALX_TABLE_SCHEMAS = {
// ...原本 6 張
'tracks': TrackSchema,
'tags': TagSchema,
} as const
注意 fetch.ts 的 getPretalxItemKey() 是「有 code 用 code,否則用 id」,tracks/tags 都用 id,可直接相容,map 會以 id 為 key。
server/utils/pretalx/index.ts 的 Promise.all 加兩筆:
const [submissions, submissionTypes, speakers, rooms, answers, slots, tracks, tags] =
await Promise.all([
fetchPretalxTable('submissions'), fetchPretalxTable('submission-types'),
fetchPretalxTable('speakers'), fetchPretalxTable('rooms'),
fetchPretalxTable('answers'), fetchPretalxTable('slots'),
fetchPretalxTable('tracks'), fetchPretalxTable('tags'), // ← 新增
])
return { submissions, speakers, rooms, answers, slots,
'submission-types': submissionTypes, tracks, tags }
2. parser 加上 track / tags 解析,並補 difficulty 正規化
server/utils/pretalx/parser.ts 新增(仿 parseType,用 id 查 map):
export function parseTrack(trackId: Submission['track'], data: PretalxResult) {
if (trackId == null) return undefined
const track = data.tracks.map[trackId]
return track ? { id: track.id, name: track.name } : undefined // name 為多語
}
export function parseTags(tagIds: Submission['tags'], data: PretalxResult) {
return tagIds
.map((id) => data.tags.map[id])
.filter(Boolean)
.map((t) => t.tag) // string[],符合現有 schema
}
difficulty 建議把 2025 的 difficultyGeneralizeMap 搬過來(可放 parser 或新 util),把 answers.difficulty 收斂成 enum 再多語化;若要先快速上線,也可以先直接輸出 answers.difficulty 原始字串。
3. 兩個 handler 輸出新欄位
server/api/session/index.get.ts(L44 一帶)與 [id]/index.get.ts:
const track = parseTrack(submission.track, data)
return {
id: submission.code,
room: slot.room.name,
start: slot.start, end: slot.end,
language: answers.language,
difficulty: answers.difficulty, // ← 新增(建議再過正規化)
track, // ← 新增({ id, name } 多語)
speakers,
zh: { ... }, en: { ... },
tags: parseTags(submission.tags, data), // ← 取代 tags: []
uri: ...,
}
shared/types/session.ts 對應補欄位:
export const SessionSummarySchema = z.object({
// ...
language: z.string().optional(),
difficulty: z.string().optional(), // ← 新增
track: z.object({ id: z.number(), name: PretalxLocaleSchema }).optional(), // ← 新增
tags: z.array(z.string()), // 內容由 [] 改成實際 tag 名稱
})
重點整理
| 項目 |
2025 來源 |
2026 現況 |
需要做的事 |
| track |
/tracks/ 端點,以 id merge |
沒 fetch;submission.track(id)有保留 |
加 tracks table + parseTrack + 輸出 |
| difficulty |
answers Q270 → 正規化 → 多語 |
parseAnswer 已 parse 但被丟棄 |
handler 輸出 answers.difficulty(建議補正規化)+ schema 加欄位 |
| tags |
前端合成 language+difficulty |
tags: [] 寫死;submission.tags(id[])有保留 |
加 tags table + parseTags 把 id→名稱,取代 [] |
最小改動成本其實不高:difficulty 幾乎只差「把已 parse 的值寫進輸出」;track / tags 則是「多抓兩張 table + 兩個 parser function + 輸出」。Pretalx event 字串在 2026 走 NUXT_PRETALX_API_URL(runtimeConfig),端點路徑與 2025 一致(/api/events/coscup-2026/tracks/、/tags/)。
議程 difficulty / track / tags metadata 調查與補齊提案
整理一下 2025 怎麼處理「議程難度(difficulty)/議程軌(track)/議程標籤(tags)」,以及 2026 目前缺什麼、要怎麼補。檔案行號以目前
main為準。一、2025 的做法(COSCUP/2025)
2025 是 VitePress +
defineLoader,在 build time 直接打 Pretalx API 把資料 merge 好。核心檔案:loaders/allSubmissions.zh-tw.data.ts/allSubmissions.en.data.ts:merge 進場loaders/pretalx/client.ts:Pretalx API client,負責 fetch + 正規化loaders/pretalx/pretalx-types.ts:question id 對應表、difficulty 正規化表、翻譯表loaders/pretalx/types.ts:資料型別loaders/pretalx/utils.ts:getAnswer()、extractLocalizedStructure()1. track(議程軌)— 來自 Pretalx tracks 端點
client.ts的getTracks()打/api/events/{event}/tracks/,回傳{ id, name, description }(name為多語):merge 發生在
allSubmissions.zh-tw.data.ts:把 tracks 做成Map<id, Track>,再用submission.track(id)查回完整 track 物件塞進每筆 session:UI 端
components/Session/SessionModal.vue把session.track.name當成主標籤顯示(L132–135),SessionsSchedule.vue也用 track 當「社群(Community)」篩選器(L62–68、L174–176)。2. difficulty(難度)— 來自 submission 的 answers(自訂問題)
難度不是獨立端點,而是投稿者在 CfP 回答的自訂問題。
pretalx-types.ts定義 question id:client.ts的getSubmissionsOf()在抓 submissions 時帶expand: ['answers', 'slots'],再用getAnswer(answers, 270)取出原始字串,接著做兩段處理:正規化(把各種寫法收斂成 4 個 enum)與多語在地化:正規化/翻譯表(
pretalx-types.ts):3. tags(議程標籤)— 2025 其實是 language + difficulty 的合成
注意:2025 UI 上講的「Tags / 議程標籤」並不是 Pretalx 的 tags table,而是把
language和difficulty兩個多語欄位當成標籤呈現+篩選:小結 2025:track 走
/tracks/端點並以 id merge;difficulty 走 answers(Q270)→ 正規化 → 多語化;「tags」是 language+difficulty 的前端合成,並非獨立資料來源。二、2026 現況與缺漏(COSCUP/2026)
2026 改成 Nuxt server route(
server/api/session/...),先把 Pretalx 各 table 抓下來快取,再於 handler 組裝。相關檔案:server/api/session/index.get.ts(列表)、server/api/session/[id]/index.get.ts(單筆)server/utils/pretalx/index.ts(抓哪些 table)server/utils/pretalx/parser.ts(parseAnswer等)shared/types/pretalx.ts、shared/types/session.ts(schema / 型別)缺漏盤點:
(a) tags 被寫死成空陣列。
server/api/session/index.get.tsL44 與[id]/index.get.ts對應位置都是:而
shared/types/session.ts的SessionSummarySchema也只有tags: z.array(z.string()),沒有 difficulty、沒有 track 欄位。(b) difficulty 已經 parse 出來,但沒有被輸出。
server/utils/pretalx/parser.ts的QUESTION_MAP已包含difficulty: 270,parseAnswer()會回傳answers.difficulty(原始字串)——但兩個 session handler 都只用了answers.language,answers.difficulty被丟掉,也沒有 2025 那套正規化/多語化。(c) track 完全沒被 fetch。
server/utils/pretalx/index.ts只抓 6 張表:shared/types/pretalx.ts的PRETALX_TABLES同樣只列這 6 張。但好消息是SubmissionSchema已經保留了原始 id:也就是說每筆 submission 上的
track(id)與tags(id 陣列)資料其實都拿得到,只是沒有對應的 tracks/tags table 可以把 id 換成名稱,最後也沒寫進輸出。三、補齊提案(讓 2026 /api/session 帶上 difficulty / track / tags)
照 2025 的 pattern,分三塊,盡量複用 2026 既有結構。
1. 新增 tracks / tags 兩張 table 的抓取與 schema
shared/types/pretalx.ts:server/utils/pretalx/index.ts的Promise.all加兩筆:2. parser 加上 track / tags 解析,並補 difficulty 正規化
server/utils/pretalx/parser.ts新增(仿parseType,用 id 查 map):difficulty 建議把 2025 的
difficultyGeneralizeMap搬過來(可放 parser 或新 util),把answers.difficulty收斂成 enum 再多語化;若要先快速上線,也可以先直接輸出answers.difficulty原始字串。3. 兩個 handler 輸出新欄位
server/api/session/index.get.ts(L44 一帶)與[id]/index.get.ts:shared/types/session.ts對應補欄位:重點整理
/tracks/端點,以 id mergeparseTrack+ 輸出parseAnswer已 parse 但被丟棄answers.difficulty(建議補正規化)+ schema 加欄位tags: []寫死;submission.tags(id[])有保留parseTags把 id→名稱,取代[]最小改動成本其實不高:difficulty 幾乎只差「把已 parse 的值寫進輸出」;track / tags 則是「多抓兩張 table + 兩個 parser function + 輸出」。Pretalx event 字串在 2026 走
NUXT_PRETALX_API_URL(runtimeConfig),端點路徑與 2025 一致(/api/events/coscup-2026/tracks/、/tags/)。