從一個空目錄,到一個有 CI、有 CDN、有離線能力、有跨裝置同步、有可觀測性的學習網站。沒有引入任何打包工具。
| 階段 | 天數 | 產出 |
|---|---|---|
| 核心產品 | 1–10 | 三頁式 MVC、SVG 學習地圖、課文與測驗、進度追蹤、遊戲化、雙層驗證 |
| 功能深化 | 11–20 | 深色模式、全站搜尋、統計數值表、章末總測驗、名詞表、PWA 離線、儀表板、教學動畫、a11y、PDF 修復 |
| 上雲 | 21–30 | 平台選型、CI、Pages 部署、內容哈希、安全 header、S3+CloudFront IaC、Serverless 同步、Passkey、可觀測性 |
| 項目 | 數字 |
|---|---|
| HTML | 11 頁(含 404.html、status.html) |
| JS(不含課文) | 約 5,700 行 |
| 課文 | 25 門課 / 139 章 / 424 課 / 約 161 小時(約 104 萬字元的 HTML,分 25 個 JSON) |
| CSS | 約 2,150 行,單一 :root 色票 |
| Node 腳本(驗證與建置) | 13 支,約 1,850 行 |
| Shell 腳本(部署與線上驗證) | 4 支 |
| Python 測試(selenium) | 5 支(smoke/ui-audit/ux/component 等) |
| Terraform | 約 670 行 |
| Workers / Functions | 約 960 行 |
| 執行期依賴 | 0(MathJax 與字型自架於 vendor/) |
| 建置期依賴 | 0(無 node_modules;functions/package.json 只有一行 type: module) |
8/8 的內容擴充把固定「4 章 × 3 課」骨架拆掉:迴歸、CDA、時間序列、多變量各擴成 8 章/24 課;AWS、GCP 各擴成 10 章/30 課;初級統計維持 14 章/49 課。課數不再是模板常數,而是領域範圍的結果。
同一天也完成可重跑的 reference coverage audit:目前 464 PDF+75 TXT 對上 18 門 runtime 課文,109 個候選主題分成 55 missing、23 shallow、31 covered。(audit 跑的是當時的 18 門;後來的 7 門雲端證照課尚未納入下一輪稽核。)這個數字比「我有四百多份教材」有用,因為它直接告訴後續內容預算該花在哪裡。
⚠️ 這一節的數字尚未取得。 Day 29 的遙測管線(
js/telemetry.js+functions/api/beacon.js)程式是完整的,但兩套部署到寫這篇時都還沒對真實帳號
執行過,所以沒有任何真實使用者資料。下表是 Day 19 在本機用 Lighthouse
模擬節流量到的值,不是 RUM。上線收滿一週後回來換掉。
| 指標 | 4G 平均 | 4G 最大 | Wi-Fi 平均 |
|---|---|---|---|
| LCP(課文頁) | 1,580 ms | 4,210 ms | 720 ms |
| INP | 64 ms | 380 ms | 41 ms |
| CLS | 0.012 | 0.089 | 0.008 |
Lighthouse(桌機):Performance 94 / Accessibility 100 / Best Practices 100。
回訪載入(Day 24 的成果):28 個請求 → 2 個。離線:讀過的內容全部可用。
| 項目 | 數字 |
|---|---|
| CI 執行時間 | 1 分 32 秒(三個 job 串行) |
| 驗證腳本 | 9 支(結構、對比度、數值、模型、glossary、資產戳、合併律、header、快取) |
| 部署平台 | 2 套(Cloudflare Pages 主、S3+CloudFront 備) |
| 部署時間 | 約 40 秒(含部署後線上 smoke test) |
| 月成本 | Cloudflare $0 + AWS $0.52 |
securityheaders.com |
A+ |
| 外部網域請求 | 0 |
| 資料庫個資欄位 | 0 |
按「難發現程度」排序。第一名的共同特徵是:不報錯、不當掉、驗證通過,但使用者看到的是壞的。
<(Day 7)<p>條件是 \(m<p\),模型無法識別。</p>
<p\) 被瀏覽器當成 HTML 標籤。公式不渲染也不報錯,結構驗證器完全看不到。全站掃出 5 處,修完後某一課的 mjx-container 從 18 變 20——有兩條公式靜靜地消失了兩個月。
唯一抓法:開瀏覽器數 mjx-container,並確認課文可見文字裡沒有殘留 \(。
template literal 檔案要雙寫 \\frac,JSON.stringify 產生的檔案照原樣。檔案文字看起來一樣,來源完全不同。 照另一種慣例盲改 → 畫面印出裸 LaTeX,語法檢查全綠。
解法:不看檔案文字,用 node:vm 載入後檢查 runtime 值。
兩百多課共用同一段講解,測驗答案是 index % 3 算出來的。結構完美、驗證全過、內容為零。 最後整批砍掉重寫。
規格可以樣板化,內容不行。
--oem 3 慢 80 倍(Day 20)tesseract 的預設 OCR engine mode 在中文密集頁 >180 秒/頁,改 --oem 1 是 2.2 秒/頁。差 80 倍,而那是預設值。
慢得不合理時先查參數,不要先假設「這條路不可行」。
按字型名一律套用 glyph 修復,把原本正確的 (Primary Biliary Cirrhosis ) 改成 Emrimary=Biliary…。同一個 BaseFont 名可能同時是簡單字型與 Identity-H。
只修明確標記為壞的(
U+FFFD),並要有獨立驗證。破壞性修復比不修復糟——因為你以為問題解決了。
<meta> 一份、_headers 一份,任何一份拒絕就是拒絕。我在 _headers 手滑寫成 default-src 'none',上線後公式全掛。本機的 http.server 不送 header,所以只在線上發生。
CSP 只能有一份。單一事實來源的原則在 HTTP 層一樣適用。
aws s3 sync --delete 的順序陷阱(Day 26)分兩次 sync 但 include/exclude 沒有互補,第一次就把 HTML 全刪了。差點造成線上網站半殘。
--delete是破壞性操作,第一次一定先--dryrun。
sw.js 被快取 = 網站變磚塊(Day 24)Service Worker 是更新鏈的起點。它被長快取住,使用者就永遠卡在舊版,而你沒有任何伺服器端手段推動更新。
同類雷:skipWaiting() 讓已開啟的頁面混用兩個版本的資產(Day 16)。
?v= 版本號(Day 24)我忘記改過一次。我自己的瀏覽器是好的(一直 hard reload),回訪使用者拿到舊 js 配新 HTML → 隨機的 undefined is not a function。
需要人類記得的步驟,遲早會忘。60 行腳本 + CI 檢查取代它。
兩台裝置離線各完成不同課,後同步的覆蓋前一台。逐項 union + 取較早時間讓合併成為 CRDT——可交換、可結合、幂等,不需要 vector clock。
frame-ancestors 放在 <meta> 裡無效(header-only 指令),還會讓每頁 console 噴 SEVERE(Day 10)。buffered: true 漏了就收不到 LCP,樣本數會是 0(Day 29)。min 欄位填「暫時的預設值」:207 課全顯示「10 分」,旁邊手調的 8–16 一對比就很假(Day 3/10)。Top 10 裡有六個是靜默失敗。它們的共同點是不會有人回報 bug——只會慢慢累積,直到你某天隨機點進去才發現。
這條原則的具體實踐是雙層驗證(Day 10):
結構驗證器(node:vm,零依賴) |
瀏覽器 smoke test(selenium) | |
|---|---|---|
| 抓什麼 | id 唯一、雙向 mapping、測驗答案索引、標題層級 | 公式真的渲染、測驗真的有反應、console 無 SEVERE |
| 速度 | 秒級 | 分鐘級 |
| 抓到過 | 課綱與課文不同步 | 裸 < 與 meta CSP(結構層不可能發現) |
Day 22 我還做了一件事:刻意弄壞每一項檢查,確認它真的會擋。 沒被驗證過的 CI 等於沒有 CI。
判準:如果你的產生器需要「填入關鍵字」,那你產的是內容 → 刪掉。如果它只是「檢查骨架是否齊全」,那是規格 → 保留。
後者的複利很驚人:Day 8 定的 <dl class="terms"><dt>中文 (English)</dt> 只是為了排版一致,七天後(Day 15)免費長出一整頁名詞表加反向索引。規格化的內容會變成可程式化的資產。
30 天裡我做了很多取捨,每一個都寫下了理由與代價:
| 決策 | 得到 | 付出 |
|---|---|---|
| 無打包工具 | 改完重整就看到、零維運 | 沒有元件系統,要自己維持紀律 |
| 課文寫成 JS 字串 | file:// 可直開 |
兩種跳脫慣例並存(→ 雷區 #2) |
| 嚴格 CSP | A+、攻擊面歸零 | file:// 直開不再能載入本地 js |
| 顏色下沉成資料 | SVG 與 CSS 共用色票 | 主題切換要重繪 SVG |
style-src 'unsafe-inline' |
零成本 | 有限的 CSS 注入面(已訂複查條件) |
| 只同步 progress | 沒有不一致 | streak 各裝置獨立計算 |
| Passkey | 零個資 | 帳號無法恢復(用 Day 17 的匯出補償) |
| 自架 MathJax + 字型 | 離線、零第三方 | repo 從 3 MB 變 10 MB |
最後三個我特別想強調:有意識的妥協 + 明確的複查條件,比假裝沒有這個問題好。
兩個實例:
Day 27 打破了 Day 1 的「不做後端」,但守住了一條線:localStorage 仍然是 source of truth。
結果是:離線能力零退化、後端掛掉只是不同步、未登入時功能完全不變。我用一個回歸測試守住它——把 sync.js 從 HTML 移除,網站行為必須與 Day 26 完全一致。
| 項目 | 為什麼沒做 |
|---|---|
| 完整的單元測試框架 | 13 支 Node 腳本 + 5 支 selenium 測試 + CI,但沒有 test runner:check-dist/check-merge/check-webauthn 是手寫的性質測試,view 層的純函式仍然沒被直接測到 |
| i18n | 全站繁中硬寫。要做的話所有 View 的字串都要抽出 |
| 內容協作流程 | 只有我一個人寫課文。多人的話需要 PR 模板、內容 review checklist |
| 章末測驗的完整覆蓋 | Day 14 做了機制,但只寫了幾章的題庫 |
| WAF / rate limit | Day 29 評估過,WAF 月費 $5 超出預算。目前靠 Workers 的免費額度當緩衝 |
| 真正的 INP | Day 29 的實作取「最長互動」,真 INP 是 P98。夠用但不精確 |
| agent 產生的程式碼文件實跑 | Day 22 接的 OpenWiki workflow 只完成設定(INSTRUCTIONS.md、.openwikiignore、手動觸發的 Actions),缺 provider 金鑰所以還沒產出過內容,排程仍註解著 |
| 兩套雲端部署都沒有真的上線 | Day 23/26–29 的設定、IaC 與後端程式都在 repo 裡、能 review、能在本機驗證(check-merge / check-webauthn / check-contrast / stamp-assets --check 全綠),但沒有網域與雲端帳號,terraform apply、wrangler pages deploy、passkey 的真實端到端流程都還沒跑過。驗收清單寫在 terraform/README.md 與兩支 check-*.sh 裡 |
| 真實使用者效能數據 | 上一項的直接後果:遙測管線完整,但沒有資料 |
dayNN tag |
wiki/07 承諾每天一個 tag,30 天下來一個都沒打 |
Progress 當初沒存時間戳(Day 5),導致 Day 17 的熱力圖只能從新增 Activity 之後才有資料,過去三個月的紀錄補不回來。
如果資料可能會想做時間軸分析,第一天就存時間。
style-src 'unsafe-inline'(Day 25):要拿掉得把六個 View 從「回傳 HTML 字串」改成「操作 DOM」。已記錄複查條件(加入表單或使用者輸入的持久化顯示時)。ON CONFLICT ... SET n = n + 1)——D1 反正已經在了,沒有理由選會錯的那個。文章與實作不一致時,實作是對的那一邊,這裡記一筆。
coverage audit 讓後續項目有了證據與優先級,而不是把每門課都再加三章。
P0:先修正課程承諾與明確來源落差
P1:把仍為 12 課的大領域依知識鏈擴章
6. 機率論:收斂/LLN/CLT、Jacobian、順序統計與估計漸近。
7. 機器學習:泛化理論、最佳化幾何、SVM 家族、GBDT、CNN/attention 與 Python labs。
8. 資料探勘:LDA/QDA/kNN、選模/正則化、非線性模型、DBSCAN/群效度與真正 R labs。
9. Python:進階函數、多格式 I/O、Pandas merge/groupby、影像 ndarray。
10. MSSQL 與軟體製作:前者補管理/隔離層級,後者不再加純架構課,改做兩個可執行縱切作品。
產品與工程中期項目
11. 補齊 25 門課的章末測驗題庫與課文 review checklist。
12. 學習路徑推薦同時使用 COURSE_MAP.edges(先修)與 COURSE_MAP.related(跨領域關聯)。
13. 把 LessonView 純函式抽出來用 node:test;筆記功能上線時重新檢討 style-src 'unsafe-inline'。
長期
14. 開放內容協作;PR 必須附 coverage 差異與來源界線檢查。
15. 把 node:vm 結構驗證與 coverage audit 模式整理成可重用工具。
16. 讓 Day 22 的 OpenWiki workflow 真的跑起來:補金鑰 → 人工審第一份產出(確認沒把課文正文搬進去、沒引用 .reference/)→ 開週排程,讓「手寫決策紀錄 + 機器維護結構文件」兩層文件都不過期。
一、先寫驗證腳本,再寫功能。
我第 10 天才寫驗證器,前 9 天累積的兩個 bug(裸 <、meta CSP)在網站上活了很久。如果我第 3 天就寫,它們永遠不會上線。
而且驗證器讓後續每個功能都變便宜——Day 11 之後我每加一個功能就加一支檢查,總共 9 支。改動時的心理成本從「我會不會弄壞什麼」變成「跑一下就知道」。
二、把「不做」清單寫下來,並且允許自己有條件地打破它。
Day 1 我寫了五條「不做」。30 天後,其中兩條被打破了(後端、file:// 完全可用),三條守住了(無框架、無帳號密碼、無版權素材)。
被打破的那兩條都是有條件地打破——後端加上了「本地仍是真相」的前提,file:// 是為了嚴格 CSP 而讓步。清單的價值不是永遠不變,是讓每次打破都成為一個需要說明理由的決定,而不是不知不覺的範圍蔓延。
三、寫下每個決策的代價,不只寫理由。
「為什麼選 X」很多人會寫。「選了 X 之後我要付什麼」很少人寫,但那才是三個月後的你需要的資訊。
我的每一天文章都有「踩到的雷」那一節。回頭看,那節的價值遠高於「做法」那節——做法會過時,代價不會。
30 天,30 篇,一個能用的網站,兩套部署,$0.52/月。
最有價值的產出不是那 424 課課文,也不是安全 header 的評等。是那一整套驗證腳本與雙層測試——它們讓這個專案在我離開三個月後回來,還敢動。
感謝一路看到這裡的人