[CODE]

靜態網站後台不用資料庫:用 GitHub API 存資料

客戶要能自己改首頁的促銷橫幅,但網站是靜態的,沒有後端。解法是做一個 Web 後台,直接透過 GitHub API 把變更 commit 到部署分支。

2 min read
github-api astro typescript web

在幫台灣民宿做網站的時候,有個問題一直反覆出現:業主希望自己能更新房型資訊、定價和季節促銷。不透過我、不用等一天、不用付一個小時的工時費。他們自己做。

標準答案是接 CMS。但我一直遇到一個落差:CMS 產品假設的使用情境,跟這些業主實際的需求對不上。大多數人從來沒用過內容管理系統。「內容集合」、「條目」這些概念跟民宿業主想事情的方式不一樣。他們能理解的是:「我要把這個房型的夏季價格改一下。」

所以我換了個方向:如果後台直接寫進 repo 呢?

核心想法

GitHub 本來就在儲存網站內容,push 的時候也會觸發部署。所以我不用多加一層資料庫,而是直接把 GitHub API 當成儲存層。後台讀取 repo 裡現有的設定,讓客戶編輯,然後把變更寫回去成為一個 commit。Firebase Hosting 透過 CI 偵測到更新,大約一分鐘後就部署完成。

不用維護資料庫。每次改動自動有版本紀錄。要復原就 git revert。對於一年只更新四次季節促銷的民宿業主來說,一分鐘的部署延遲根本感覺不到——他們按儲存、去接一通客人的電話、回來重新整理,改動已經上線了。

先搞定認證

後台是一個獨立的 Astro 頁面,用 Google OAuth 保護。客戶用我事先加進允許清單的 Google 帳號登入。認證完成後,app 會換取 token 並把 refresh token 儲存起來,讓他們之後不用每次都重新登入。

這部分花了我最多時間。Google 的 OAuth refresh token 只在第一次授權時才會回傳,所以必須在初次握手的時候就把它存好,不能丟掉。

用 Google OAuth 有個實際上的考量,不只是安全性:這些業主每個人都已經有 Google 帳號了,平常用 Gmail 或 Google Maps。不需要再管一組新的帳號密碼,也不會有「我忘記 CMS 密碼了」的支援電話。

讀取目前的設定

後台載入時,會用 Octokit 從 GitHub 抓取目前的橫幅設定:

import { Octokit } from "@octokit/rest";

const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });

async function getBannerConfig() {
  const { data } = await octokit.repos.getContent({
    owner: "wayne2002tw",
    repo: "my-client-site",
    path: "src/data/banner.json",
    ref: "deploy",
  });

  if ("content" in data) {
    const content = Buffer.from(data.content, "base64").toString("utf-8");
    return { config: JSON.parse(content), sha: data.sha };
  }
}

sha 很重要——之後更新檔案時需要帶上它,GitHub 用來偵測衝突。

儲存變更

客戶按下儲存時,會發生兩件事:如果有換圖的話先上傳圖片,然後再更新 JSON 設定檔。兩個分開 commit,或是合成一個也可以,我選擇分開比較清楚。

圖片上傳的地方要處理 base64 編碼。GitHub API 要求檔案內容用 base64 傳送:

async function uploadImage(file: File, path: string) {
  const buffer = await file.arrayBuffer();
  const base64 = Buffer.from(buffer).toString("base64");

  await octokit.repos.createOrUpdateFileContents({
    owner: "wayne2002tw",
    repo: "my-client-site",
    path,
    message: "chore: update banner image",
    content: base64,
    branch: "deploy",
  });
}

更新 JSON 設定檔的方式一樣,記得帶上剛才讀到的 sha,避免覆蓋掉別人的修改。

哪些地方真的好用

版本控制這點出乎意料地實用。有一次客戶不小心上傳了一張模糊的圖片,問我能不能「復原」,我直接 git revert 就搞定了。不需要額外的備份機制。commit 歷史也順帶成了修改紀錄——可以精確看到價格是什麼時候改的、改之前是什麼。

GitHub API 的速率限制是每小時 5000 次請求(需認證),對一個一天可能只有 10 次請求的後台來說根本不是問題。

部署自動觸發,速度也快。客戶編輯橫幅、按儲存、等 60 秒、重新整理正式網站,就這樣。這個回饋循環其實比很多 CMS 方案還快——沒有草稿/發佈的流程要走,不需要手動清快取。

比較麻煩的地方

衝突處理是後寫覆蓋(last-write-wins)。如果兩個人同時編輯——單一業主的民宿網站根本不會發生,但還是要提——其中一個人會從 GitHub 拿到 409 錯誤,app 會顯示錯誤訊息。這個 use case 來說可以接受。如果是多人協作的場景,就需要重新設計這塊。

圖片的 base64 編碼會帶來一些大小上的考量。GitHub 有 100MB 的檔案限制,但更實際的問題是大圖會讓 repo clone 越來越慢。我在上傳前加了一個前端 resize 步驟,把圖片壓縮到 1MB 以下再編碼。

設定 GitHub token 的 scope 也花了一些時間才搞定。我最後用了細粒度的 personal access token,只授予那一個 repo 的 contents: write 權限,感覺比用有廣泛權限的傳統 token 安全。

更重要的一點

這套系統帶來的真正改變不在技術層面。在這之前,要更新房型定價或季節促銷,業主得聯絡我、等我有空、為一個十五分鐘就能做完的事情付費。一年要操作好幾次的事情,卻要依賴外部的工程師。

之後:他們打開後台頁面,做修改,六十秒後上線。他們掌控了自己的內容。

對我合作的這些民宿來說,這個所有權的轉移——不是 Astro 的 build、不是 Cloudflare CDN、不是 WebP 圖片——才是這個案子真正交付的東西。GitHub API 的方案讓我能做到這一點,而且不需要我們兩方都不想維護的 CMS 平台。

整體來說,對於正確的使用情境——小型靜態網站、單一業主、更新不頻繁——這個方案意外地簡潔。不用維護資料庫,這本身就是一個優點,不是限制。