这份文件写给想了解野马营网站的人,不需要任何程式背景。技术名词第一次出现时都会用日常的话解释一次。

一分钟版本

野马营是一个温哥华越野跑俱乐部的网站。它同时做三件事:

  • 对外:让任何人读跑者写的比赛心得、看相册、查赛事行事历、浏览跑者名录。
  • 对内:让俱乐部成员自己写文章、传照片影片、记录自己跑完的比赛。
  • 自动化:成员记录比赛之后,网站自动算出他该得的徽章,并把影片转成适合网路播放的格式。

整个网站没有传统的「主机」。它跑在 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 月的野马营网站。比赛资料每年都会变,功能也还在长 —— 但上面那四层资料模型和两条上传路径,是这个网站的骨架。