Skip to content

sessions 應回傳「社群」欄位 #171

@pan93412

Description

@pan93412

議程 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.tsgetAnswer()extractLocalizedStructure()

1. track(議程軌)— 來自 Pretalx tracks 端點

client.tsgetTracks()/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.vuesession.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.tsgetSubmissionsOf() 在抓 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,而是把 languagedifficulty 兩個多語欄位當成標籤呈現+篩選:

<!-- 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.tsparseAnswer 等)
  • shared/types/pretalx.tsshared/types/session.ts(schema / 型別)

缺漏盤點:

(a) tags 被寫死成空陣列。 server/api/session/index.get.ts L44 與 [id]/index.get.ts 對應位置都是:

tags: [],   // ← 永遠空的

shared/types/session.tsSessionSummarySchema 也只有 tags: z.array(z.string())沒有 difficulty、沒有 track 欄位

(b) difficulty 已經 parse 出來,但沒有被輸出。 server/utils/pretalx/parser.tsQUESTION_MAP 已包含 difficulty: 270parseAnswer() 會回傳 answers.difficulty(原始字串)——但兩個 session handler 都只用了 answers.languageanswers.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.tsPRETALX_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.tsgetPretalxItemKey() 是「有 code 用 code,否則用 id」,tracks/tags 都用 id,可直接相容,map 會以 id 為 key。

server/utils/pretalx/index.tsPromise.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/)。

Metadata

Metadata

Assignees

Labels

No labels
No labels
No fields configured for Feature.

Projects

Status
In Progress

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions