這份文件寫給想了解野馬營網站的人,不需要任何程式背景。技術名詞第一次出現時都會用日常的話解釋一次。
一分鐘版本
野馬營是一個溫哥華越野跑俱樂部的網站。它同時做三件事:
- 對外:讓任何人讀跑者寫的比賽心得、看相簿、查賽事行事曆、瀏覽跑者名錄。
- 對內:讓俱樂部成員自己寫文章、傳照片影片、記錄自己跑完的比賽。
- 自動化:成員記錄比賽之後,網站自動算出他該得的徽章,並把影片轉成適合網路播放的格式。
整個網站沒有傳統的「主機」。它跑在 Cloudflare 的邊緣網路上 —— 簡單說,就是把網站同時放在全世界幾百個機房,誰來看就由最近的那個機房回應。
全景圖
訪客 會員 管理員
(不必登入) (需邀請) (俱樂部幹部)
│ │ │
▼ ▼ ▼
┌───────────┐ ┌────────────┐ ┌───────────┐
│ 公開頁面 │ │ 會員後台 │ │ 管理後台 │
│ 文章 相簿 │ │ 寫文章 │ │ 改任何資料 │
│ 賽事 名錄 │ │ 傳照片影片 │ │ 發邀請 │
└─────┬─────┘ └──────┬─────┘ └─────┬─────┘
│ │ │
└───────────┬───────┴──────────────────┘
▼
┌─────────────────────┐
│ 網站程式(Worker) │
│ 跑在 Cloudflare 上 │
└──────────┬──────────┘
│
┌────────────┼────────────┬─────────────┐
▼ ▼ ▼ ▼
┌──────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ D1 │ │ R2 │ │ Stream │ │ AI │
│資料庫 │ │檔案倉庫 │ │影片播放 │ │寫作助手 │
└──────┘ └────────┘ └────────┘ └────────┘
文字資料 原始照片 轉好的影片 改寫、摘要
誰寫了什麼 影片檔案 各種畫質四個外部服務各司其職:
名字 | 白話解釋 | 存什麼 |
|---|---|---|
D1 | 資料庫,像一疊可以快速查詢的表格 | 文章內容、比賽紀錄、誰是誰 |
R2 | 檔案倉庫,像一個超大的雲端硬碟 | 照片原檔、影片原檔 |
Stream | 影片服務 | 轉好的影片,會依網速自動切畫質 |
AI | 語言模型 | 不存東西,只幫忙改寫和摘要 |
訪客看到什麼
不必登入就能看的部分:
- 首頁 — 精選文章與相簿。
- 文章 — 跑者寫的比賽心得。如果作者把文章連到自己的一筆比賽紀錄,文章上方會出現那場比賽的徽章。
- 相簿 — 一次活動的照片集,影片也在裡面,點了可以單獨開一頁分享。
- 賽事 — 未來賽事的行事曆,可以切換顯示方式,也可以只看「越野世界巡迴賽」之類的系列,或只看「西部百英里的資格賽」。
- 跑者名錄 — 每位成員一張卡片,上面是他跑過的比賽徽章。可以用徽章篩選,例如「誰跑過 UTMB 100 英里」,而且篩選結果的網址可以直接分享給別人。
會員能做什麼
野馬營沒有公開註冊。 新成員一律由現有管理員發邀請信,邀請連結七天內有效。這是刻意的設計:俱樂部網站的成員名單本來就該由人決定。
會員登入後可以:
- 寫文章 — 所見即所得的編輯器,打字時會自動存草稿。
- 匯入文章 — 把寫好的 Markdown / MDX 檔案丟進來,直接變成可編輯的文章(就是這份文件的用法)。
- 傳照片和影片 — 每人有儲存額度,預設 10 GB。
- 記錄比賽 — 從賽事目錄挑「哪場比賽、哪個組別、哪一年」。徽章由此自動產生。
- 寫比賽報告 — 挑一場自己跑完的比賽,文章就跟那筆紀錄綁在一起。同一場比賽只能寫一篇。
比賽資料為什麼要分成四層
這是整個網站最值得理解的一塊。直覺上「比賽」是一件事,但實際上它是四件會用不同速度變化的事,硬湊成一張表會出問題。
┌──────────────────────────────────────────────┐
│ 賽事 RaceEvent ── 這場比賽「是什麼」 │
│ 例:Hardrock 100 │
│ 幾十年不變。名字、國家、屬於哪個系列 │
└───────┬──────────────────────┬───────────────┘
│ │
│ 有哪些組別可以報 │ 每年辦一次
▼ ▼
┌────────────────┐ ┌──────────────────────┐
│ 組別 Category │ │ 屆次 Edition │
│ 例:UTMB/CCC/ │ │ 例:2026 年那一屆 │
│ OCC/TDS │ │ 日期、報名開關 │
│ 偶爾增減 │ │ 每年換一次 │
└────────┬───────┘ └──────────┬───────────┘
│ │
└───────────┬───────────┘
▼
┌──────────────────────────┐
│ 紀錄 RaceRecord │
│ 「我 2015 年跑完了這個組別」│
│ 成員自己填,只增不減 │
└──────────────────────────┘三個容易被忽略的設計理由:
為什麼叫「組別」不叫「距離」。 白朗峰的報名項目是 UTMB、CCC、OCC、TDS;Sinister 7 有接力組;Barkley 有 Fun Run。這些都不是距離。叫它「距離」會讓人問出「這場比賽的徽章為什麼沒有距離」這種無解的問題。
為什麼屆次可以沒有日期。 有成員想記錄他 2015 年跑的 Hardrock,但沒人查得到那一屆的確切日期,硬編一個會讓公開行事曆顯示錯的日期。所以屆次允許只有年份、沒有日期 —— 行事曆只顯示有日期的,沒日期的就安靜地待在徽章後面撐著那筆紀錄。
為什麼賽事有一個「代號」。 徽章的顏色是把賽事代號丟進一個固定算式算出來的。如果用資料庫自動編的流水號,測試環境和正式環境會編出不同號碼,同一場比賽就會變成兩種顏色。代號是人取的、跨環境一致的,所以顏色永遠一樣。代號一旦定了就不能改 —— 成員的徽章指著它。
徽章怎麼來的
成員填一筆比賽紀錄,徽章就自動出現,不需要任何人核可。有兩種特別的徽章:
- 六大馬拉松(Six Star) — 跑完波士頓、紐約、芝加哥、倫敦、柏林、東京六場。網站是比對明確的六場清單,而不是「馬拉松系列跑滿六場」—— 跑六次波士頓不算。
- 資格賽標記 — 有些比賽是西部百英里或 Hardrock 的報名資格賽,行事曆上可以只篩這些。
照片和影片上傳後經歷了什麼
照片和影片走的路完全不同,因為影片必須轉檔。
照片
────
選檔 ──▶ 瀏覽器先縮圖 ──▶ 直接傳進 R2 ──▶ 產生模糊預覽圖
(載入時先顯示的色塊)
影片
────
選檔 ──▶ 直接傳進 R2 ──▶ 排進轉檔佇列 ──▶ 轉檔容器取件
│
┌───────────────────┘
▼
轉成多種畫質 ──▶ 存進 Stream ──▶ 相簿可播放
│
└─▶ 失敗 ──▶ 重試,超過次數就標記失敗並通知兩個值得一提的地方:
「佇列」其實不是佇列。 網站沒有另外架排隊系統,而是直接用影片那一列資料的狀態欄當佇列:queued 是待辦、running 是有人正在處理、failed 是放棄。這樣少維護一個服務。
為什麼要「租約」而不是信任轉檔程式。 Cloudflare 明說容器不保證跑多久,隨時可能因為主機重啟被中止。所以轉檔程式領件時是「租」一段時間,租約過期還沒完成就視為失敗、可以被別人接手。不然一個中途死掉的工作會永遠卡在「處理中」,而沒有人會知道。
還有一個定期打掃的機制:沒有被任何文章或相簿使用的檔案會被標記,過一段時間後清掉,以免成員的額度被孤兒檔案吃掉。
AI 幫什麼忙
編輯器裡有三個 AI 功能,都跑在 Cloudflare 自己的模型上:
- 潤飾 — 把選取的段落改寫,改完是並排顯示,原文在左、AI 版在右,成員自己決定要不要換。
- 擴寫 — 把簡短的幾句話展開成段落。
- 摘要 — 產生文章摘要。
兩個設計細節:AI 看不到文章裡的圖片和特殊區塊,那些位置會先換成記號,改完再放回去,所以 AI 不會把圖片弄丟。另外每位成員有使用次數上限,而且限制的是「問」而不是「問對」 —— 失敗的請求一樣計數,否則一直送錯誤請求就能繞過限制。
內容其實有兩個來源
這是看原始碼才會發現的事:網站的文章有兩種來路。
┌──────────────────────┐ ┌──────────────────────┐
│ 早期文章 │ │ 現在的文章 │
│ 寫成檔案放進程式碼裡 │ │ 會員在網站上寫 │
│ 要改就要重新部署 │ │ 存在 D1 資料庫 │
└──────────┬───────────┘ └──────────┬───────────┘
│ │
│ 一次性搬遷 │
└──────────────▶ D1 ◀────────────┘
│
▼
網站讀這裡早期的文章是直接寫成檔案跟程式碼放在一起的,改一個字就得重新部署整個網站。現在文章存在資料庫,會員自己就能改。舊文章已經搬進資料庫了,但那套舊機制還留著,因為它同時也負責處理網站的一些固定設定。
網站住在哪裡
有人把修改合併進主線
│
▼
┌────────────────┐
│ 部署到測試環境 │ 先上測試站
└───────┬────────┘
▼
┌────────────────┐
│ 對測試站跑測試 │ 用真的瀏覽器把網站點一遍
└───────┬────────┘
│ 通過才繼續
▼
┌────────────────┐
│ 部署到正式環境 │ 需要人按下確認
└────────────────┘測試站和正式站跑的是完全相同的程式,差別只在它們連的是不同的資料庫。這一點在設定上很容易搞混,也確實出過事 —— 所以專案文件裡有一整節在講這兩個環境怎麼分辨。
「對測試站跑測試」這一關是刻意擋在正式部署前面的:它用真的瀏覽器操作真的部署,能抓到只在正式環境才會出現的問題,那是在開發者自己電腦上永遠測不出來的。
幾個保護使用者的設計
- 不把個人資料洩進公開頁面。 文章、相簿、照片都連著「誰上傳的」,而那筆資料裡有電子郵件和登入狀態。所有公開查詢都明確排除這個欄位,而且有測試在盯著公開頁面的原始碼不能出現這些東西。
- 測試帳號的密碼是公開的,所以有一道鎖。 測試用的預設密碼寫在公開的程式碼裡。網站因此加了一條規則:帶著這個公開密碼去登入任何非本機的網址,會直接報錯停下來。
- 賽事資料由人審過才進來。 賽事目錄是人工核對過的表格檔,每一列都記著「這是從哪個網址讀來的」和「誰在哪天確認過」。空白就代表沒人確認過。網站不做自動抓取 —— 因為每個賽事網站長得都不一樣,抓錯了會寫進錯的日期,比沒有還糟。
詞彙對照
你會看到的詞 | 白話 |
|---|---|
Worker | 跑在 Cloudflare 上的網站程式 |
D1 | 資料庫,存文字和關聯 |
R2 | 檔案倉庫,存照片影片原檔 |
Stream | 影片服務,負責轉檔和播放 |
遷移(migration) | 改資料庫結構的步驟,只執行一次 |
部署(deploy) | 把新版本推上線 |
邊緣網路 | 把網站放在全球各地機房,就近回應 |
前言(frontmatter) | 文章檔案最上面那段設定,寫標題和日期 |
這份文件描述的是 2026 年 9 月的野馬營網站。比賽資料每年都會變,功能也還在長 —— 但上面那四層資料模型和兩條上傳路徑,是這個網站的骨架。