这份文件写给想了解野马营网站的人,不需要任何程式背景。技术名词第一次出现时都会用日常的话解释一次。
一分钟版本
野马营是一个温哥华越野跑俱乐部的网站。它同时做三件事:
- 对外:让任何人读跑者写的比赛心得、看相册、查赛事行事历、浏览跑者名录。
- 对内:让俱乐部成员自己写文章、传照片影片、记录自己跑完的比赛。
- 自动化:成员记录比赛之后,网站自动算出他该得的徽章,并把影片转成适合网路播放的格式。
整个网站没有传统的「主机」。它跑在 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 月的野马营网站。比赛资料每年都会变,功能也还在长 —— 但上面那四层资料模型和两条上传路径,是这个网站的骨架。