2026-09-23

Devlog - 放在 README 上的隨機每日金句

因為逛了一下別人的 GitHub,發現大家的 profile README 都在比 hi-hua(稱讚意味)的,所以想說也來做點東西塞進去 XDD

於是做了這個!隨機每日金句(有些很靠杯 XD):

Testing Quote of the Day

用下面這段 HTML 就可以插入隨機每日金句:

<img src="https://testing-quotes.goescat1024.workers.dev/" width="500" />

這個 API 放在 Cloudflare Workers。Cloudflare Workers 是 Cloudflare 提供的 Serverless 執行環境,可以把 JavaScript / TypeScript 程式直接部署到 Cloudflare 的網路上執行。

它的概念很簡單:

收到 HTTP Request
        ↓
執行你的程式
        ↓
回傳 HTTP Response

不需要自己管理傳統 Server,也不用處理 VM、作業系統或 Nginx。Cloudflare 負責底層的執行環境與網路基礎設施,你只需要寫要執行的程式。


建立 Worker

進入 Cloudflare Dashboard:Workers & Pages → Create → Worker

建立後可以直接在 Cloudflare 的線上 Editor 編輯程式。Deploy 後,Cloudflare 會提供一個像這樣的網址:

https://<worker-name>.<subdomain>.workers.dev

最基本的 Worker

Worker 最核心的概念就是:

export default {
  async fetch(request) {
    return new Response("Hello World!");
  }
};

每次有人 request Worker,都會經過:

HTTP Request
     ↓
fetch(request)
     ↓
HTTP Response

所以 Worker 本質上就是一個可以直接接收 HTTP Request、執行程式邏輯,再回傳 Response 的 Serverless endpoint。

這次的 Testing Quote of the Day,就是在這個 fetch() 裡隨機選一句 Quote,再動態產生 SVG 回傳。

用陣列管理 Quote

所有名言直接定義在 QUOTES 陣列中:

const QUOTES = [
  {
    quote: "Testing shows the presence, not the absence of bugs.",
    translation: "測試可以證明 Bug 存在,卻不能證明 Bug 不存在。",
    source: "Edsger W. Dijkstra"
  }
];

每次收到 Request 時,利用 Math.random() 隨機挑選一筆:

const item =
  QUOTES[Math.floor(Math.random() * QUOTES.length)];

因此每次請求都有機會得到不同的 Quote(沒快取的話,放在 Github profile 會有一層他們的 CDN,不會每次都 request 一次)。

直接產生 SVG

沒有準備圖片素材,而是讓 Worker 即時產生 SVG

SVG 本身就是文字格式,所以可以直接透過 template string 組出完整的 <svg>,再用 <text><tspan> 等元素控制文字與排版。

這樣做的好處是圖片中的文字仍然是 SVG 內容,不需要預先產生幾十張圖片。

處理文字排版

不同 Quote 的長度不一樣,因此簡單做了一層排版:

  • fontSize():依英文長度調整字體大小
  • wrapText():超過指定長度時自動換行
  • translationFontSize():中文翻譯另外調整字體大小

例如較長的 Quote 就會自動縮小:

if (text.length <= 40) return 27;
if (text.length <= 60) return 24;
if (text.length <= 80) return 21;
return 18;

雖然不是完整的排版引擎,但對這種固定尺寸的小型 SVG 已經夠用。

不要忘記 Escape

因為 Quote 最後會被插入 SVG/XML,所以不能直接把原始文字塞進去。

這裡用 escapeXML() 處理:

.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")

避免 Quote 裡面的特殊字元破壞 SVG markup。

Worker 最後只需要回傳 SVG

產生完成後直接:

return new Response(svg, {
  headers: {
    "Content-Type": "image/svg+xml; charset=utf-8",
    "Cache-Control": "no-store"
  }
});

因此這個 Worker 本質上就是一個很小的 dynamic SVG endpoint

Request → 隨機選 Quote → 產生 SVG → Response

沒有資料庫、沒有前端,也沒有圖片儲存最後得到的是一個可以直接嵌入 README、網站或其他地方的動態圖片 URL~