用 Markdown 建立私人筆記網站:GCP、Quartz、GitHub 與網域設定

這篇記錄一個已實作的做法:平常用 Obsidian 寫 Markdown(MD),選出要分享的文章與附件,保存到私人 GitHub,再發布成有 Google 帳號保護的網站。同一網址可以讓自己、家人與朋友看到不同內容。

以下帳號、網域、資料夾與專案名稱皆為示例。這是架構與製作流程說明;不是只安裝 Quartz 就自動具備所有功能的套件。文章權限、登入身分驗證、附件保護及伺服器搜尋,都需要另外實作。

一、先看整體架構

flowchart TD
    A[Obsidian 原始 MD 與附件] --> B[挑選發布清單及中央權限表]
    B --> C[私人 GitHub 儲存庫]
    C --> D[GitHub Actions 建置與測試]
    D --> E[Artifact Registry 容器映像]
    E --> F[Cloud Run 網站服務]
    G[自訂網域與 DNS] --> I[Google IAP 登入與入口授權]
    H[讀者瀏覽器] --> G
    I --> F
    F --> J[後端驗證身分並選擇可讀內容]

上圖的 IAP 是 Cloud Run 的入口防護;請求通過 IAP 才會到達網站容器。

工具或服務在這個網站負責什麼
Obsidian/Markdown寫作及保存原稿,網站不用時仍保有可讀文字
Quartz把 MD 轉成閱讀介面,包含目錄、標籤、反向連結與深色模式
私人 GitHub保存網站程式、選定的發布副本及版本歷史
GitHub Actions自動建置、測試,再部署通過檢查的版本
Artifact Registry保存可部署的容器映像
Cloud Run執行網站後端、回傳文章及處理搜尋
Google IAP驗證讀者 Google 身分,限制誰能進入服務
網站中央權限表決定登入後能讀哪些文章及附件
網域與 DNS提供固定入口,例如 notes.example.com

Quartz 是 Markdown 靜態網站產生器;本案例在它外面加上受保護的後端,所以文章預先產生,讀者開頁時不必重新編譯 MD。Quartz 官方說明

二、MD 是原稿,網站是選定內容的發布版

不把整個筆記庫直接當作網站根目錄。先維持自己習慣的筆記分類,再用一份清單挑選要上網的檔案。

例如原稿可以是:

Obsidian 筆記庫/
├─ Personal/Car/維修紀錄.md
├─ Family/Trips/家庭旅行.md
├─ Shared/Podcast/某集節目筆記.md
└─ Attachments/某份原始單據.pdf

網站儲存庫只放核准副本:

private-notes-site/
├─ content/                  選定的文章與附件
├─ publish-manifest.json     逐檔發布對照
├─ config/publication.md     中央權限表的核准快照
├─ portal/                   登入驗證、內容分流及搜尋程式
└─ .github/workflows/        建置、測試與部署流程

發布清單示意:

{
  "files": [
    {
      "source": "Shared/Podcast/某集節目筆記.md",
      "target": "shared/podcast/episode-001.md"
    }
  ]
}

這份 JSON 需要自己的同步程式讀取,不是 Quartz 通用設定。同步時只複製清單中的檔案,並調整內部連結;來源 MD 保留在 Obsidian。新增筆記不會因為放進資料夾,就立刻公開。

私人 GitHub 也不等於備份整個 Obsidian:它備份的是提交進去的網站程式與核准副本,完整筆記庫仍需自己的備份方式。

三、用中央表管理朋友與文章,不必逐篇加屬性

本案例把「讀者名單」和「資料夾規則」集中管理。以下是自訂格式示意,所有信箱都是占位範例。

讀者群組

發布與閱讀規則

原稿範圍閱讀群組分類發布
Personal/Car/**owner車輛是
Family/Trips/**family旅遊是
Shared/Podcast/**owner,friendsPodcast是

這套實作的 owner 可以讀全部「已選入發布」的內容;其他人只讀自己群組獲准的部分。單篇例外優先於資料夾規則。沒有發布清單、沒有有效規則或標為不發布,就不進入網站。

同一篇給多個群組看,只需要一份原稿與發布來源。為了維持 Quartz 的目錄、標籤及關聯功能,建置時仍會依不同權限組合產生對應頁面,因此不是所有 HTML 都只存一份。大型附件可以依雜湊共用保存,每次下載再驗權限。

這些功能是額外寫的發布/授權程式;一般 Quartz 標籤或 MD 前置屬性,單獨使用不會變成安全的權限控制。

四、建立 GCP 執行環境

製作時先選定一個 GCP 專案,連結計費帳戶,再準備 Cloud Run、Artifact Registry、IAP,以及部署身分需要的 IAM/OIDC 設定。不同站台或讀者群組不一定要各開一個 GCP 專案。

先讓網站在 Cloud Run 預設網址完成測試,之後再接正式網域,比同時排除程式、登入及 DNS 三種問題容易。

本案例是單一 Cloud Run 服務,採請求式計費、最小執行個體為 0,並設定較小的最大執行個體數。沒有流量時可縮到零,但首次開啟可能遇到冷啟動;若改成保留常駐實例,則有相應費用。Cloud Run 最小實例說明

容器需監聽平台提供的 PORT,以非管理員使用者執行,並把正式網站所需檔案放進映像。這個案例沒有在容器裡掛載整個筆記庫,也不讓讀者透過網站直接編輯原稿。

五、Google 登入:分清楚三份名單

同一個人能登入並看到文章,可能需要通過三個不同層次:

層次管理內容單獨完成是否足夠
Google Auth Platform/OAuth登入應用程式的名稱、受眾、OAuth client 與測試狀態不足以授權網站文章
IAP 入口存取政策哪些帳號可以進入 Cloud Run 服務不代表可以看全部文章
網站中央權限表該帳號能讀哪些分類、文章及附件不會自動替人加入 IAP 或 OAuth 名單

1. 設定登入應用程式

若讀者包含一般 Gmail 或組織外的朋友,需依 IAP 的外部使用者流程建立適合的自訂 OAuth client;不要假設預設 Google-managed client 一定能接納組織外帳號。可先在 Google Cloud Console 完成首次設定。IAP 自訂 OAuth client

在 Google Auth Platform 設定應用程式名稱、支援聯絡方式與適當的 Audience。若採 Testing,檢查需要的測試使用者名單;正式分享前再評估發布狀態與驗證要求。OAuth Audience 官方說明

OAuth client 類型使用 Web application,依官方頁面設定 IAP 回呼 URI。Client secret 留在受保護的設定中,不放入 MD、前端 JS 或 Git 提交。

IAP 回呼位址格式如下,把 YOUR_CLIENT_ID 換成自己的 OAuth client ID,並把該 client 套用到 IAP 設定;不是把自訂網域隨意加上 /callback 就會生效。

https://iap.googleapis.com/v1/oauth/clientIds/YOUR_CLIENT_ID:handleRedirect

本案例只用 Google 身分識別登入,不要求讀者授權讀取 Gmail 郵件或整個 Google Drive。Google 密碼在 Google 的登入畫面輸入,網站本身不收密碼。

2. 在 Cloud Run 直接啟用 IAP

到服務的 Security 設定選擇需要驗證,並啟用 Identity-Aware Proxy。IAP 服務代理需要呼叫該 Cloud Run 的權限;使用 Console 啟用時由平台設定,使用 CLI 則依官方步驟檢查。讀者授予的是 IAP 存取角色,不必給朋友專案管理員權限。IAP 可直接保護 Cloud Run,包括預設 run.app 入口,不是只有自訂網址才受保護。Cloud Run 直接使用 IAP

3. 後端再次驗證身分並限制內容

後端驗證 IAP 簽署的 x-goog-iap-jwt-assertion:檢查簽章、發行者、服務專屬 audience 與有效期,再將已驗證的帳號對應群組。不能只相信一個未驗簽的 email header,也不能接受 ?role=owner 之類的網址參數切換身分。IAP 簽署標頭驗證

讀者實際看到的流程

sequenceDiagram
    participant R as 讀者
    participant I as Google IAP
    participant G as Google 登入
    participant W as 網站後端
    R->>I: 開啟 notes.example.com
    I->>G: 需要時選擇帳號或登入
    G-->>I: 確認 Google 身分
    I->>I: 檢查入口名單
    I->>W: 傳遞帶簽章的身分
    W->>W: 驗證簽章並對應閱讀群組
    W-->>R: 只回傳該群組可讀的頁面

朋友若已登入 Google,通常可沿用既有工作階段,但第一次授權、選錯帳號、登入過期或 Google 要求重新確認時,仍可能出現額外步驟,不能保證永遠不用輸入密碼。

六、私人 GitHub 如何發布到 GCP

流程是「選檔 → 同步副本與權限 → 提交 Git → 自動測試 → 部署」。GitHub 儲存庫設為私人,網站程式與 MD 副本就不必開放原始碼給所有網友。

GitHub Actions 經由 Workload Identity Federation 取得部署所需的短期身分,避免把長效服務帳戶金鑰放進儲存庫。部署身分需限制到指定儲存庫及可信任分支/環境,並只授予建置、推送映像及更新指定服務需要的權限;不要直接給 Owner。Google 的 GitHub 部署聯邦身分流程

我們的工作流程是:

  1. 檢出私人 repo 的核准版本,安裝鎖定版本的依賴。
  2. 檢查發布清單、來源存在與中央權限規則。
  3. 依可讀群組先排除不該出現的內容,再建置 Quartz。
  4. 測試登入驗證、文章/附件權限、搜尋結果與手機載入大小。
  5. 將通過測試的容器推到 Artifact Registry,再更新 Cloud Run。
  6. 核對實際 revision、登入保護及正式頁面,而不只看 CI 顯示成功。

本地同步 Obsidian/Drive、把檔案提交 GitHub,以及發布到網站是三件事。只做其中一步,不代表其他兩步已完成。

七、Domain、DNS 與 HTTPS 怎麼接

Domain(網域)是固定名稱,DNS 決定它指向哪裡,HTTPS 憑證讓瀏覽器能驗證加密連線。這三件事與文章權限各自獨立:擁有網址不代表已做登入保護。

本案例的 DNS 放在 AWS Route 53,網站服務在 GCP;不需要因為搬到 GCP,就把 DNS 也搬走。

我們實際採用的接法

使用 Cloud Run 原生 Domain Mapping,把指定網域映射到服務,再在 DNS 業者依平台回傳的紀錄設定。平台核發 Google-managed HTTPS 憑證。

這個選擇有範圍限制:查核時原生 Domain Mapping 仍是 Preview、只開放部分區域,Google 不建議用於正式生產服務。 本案例採它作為少量讀者的私人網站接法,不把它當成所有商用網站的標準答案;正式商用或要求較高可用性時,應評估官方列出的其他方案,例如外部 Application Load Balancer。網域映射方式與限制

DNS 設定順序

  1. 決定主網址:例如 notes.example.com,或根網域 example.com。
  2. 在 Cloud Run 建立對應的 domain mapping,完成網域所有權驗證。
  3. 在 Route 53 的正確 Hosted Zone,填入 Cloud Run 回傳的紀錄;不要自行猜 IP 或沿用別人的值。
  4. 子網域可能使用 CNAME;根網域依平台提供 A/AAAA。Route 53 的根網域不能直接放一般 CNAME。Route 53 紀錄類型
  5. 移除或替換同一主機名稱中衝突的舊網站紀錄;保留郵件 MX、SPF、DKIM、DMARC 等無關紀錄。若平台要求 IPv6,就保留正確 AAAA,不要套用「一律刪掉 AAAA」的做法。
  6. 等 DNS 傳播與憑證就緒,再以瀏覽器實測;不能只看到紀錄建立成功就宣布完成。

若要同時使用 www.example.com,需要另做適當的主機對應與憑證,或在 HTTP 層設定轉址。DNS 的 CNAME 只是名稱指向,不會自動把瀏覽器網址變成另一個主網址。

我們先把預設網址測通,才接正式網域。更換代管平台時,可保留網域名稱、改動 DNS 與平台映射;讀者仍從同一入口進來。

八、不能只保護文章畫面

本案例會在建置前先按權限篩選,避免把全站資料交給瀏覽器後才隱藏選單。HTML、搜尋 API、目錄索引、標籤、反向連結及 PDF 都受保護;未授權的人也不能藉由 JSON 或附件直連取得內容。

私人頁面使用 private, no-store,不放進公開 CDN 共用快取。PDF 可共用雜湊檔案,但請求仍逐次驗權限。已授權讀者仍可以複製、下載或截圖,登入保護不等於防止再轉傳。

MP3 若只保留 Google Drive 連結,網站不必儲存音訊;但「能進網站」不會自動取得 Drive 檔案權限。分享文章及分享音訊要分別確認。

九、手機閱讀是製作過程中補上的一課

本案例曾在加入大量逐字稿後收到 iPhone 載入失敗的回報。我們核實原本會下載約 27.6 MB 全文索引,還在瀏覽器建立全文搜尋;這是負荷問題,但沒有裝置診斷資料,不能把它說成已證實的 Safari 當機原因。

後續改成:全文搜尋由伺服器依帳號執行;一般開頁只載入輕量目錄索引。實作案例的索引壓縮傳輸由約 7.8 MB 降到 154 KB,手機不啟動隱藏圖譜,只建立需要展開的目錄節點。

另一個經驗是:有些 Quartz 元件優先使用預先編譯檔。修改 TypeScript 原始碼後,還要確認實際輸出的瀏覽器程式已更新。現在測試同時檢查權限、索引大小與成品內容,並另外用手機尺寸操作頁面;手機尺寸模擬仍不等於 iPhone 實機驗證。

十、費用與日常維護

低流量可以採小規格及縮到零的設定,但不要把整套服務當作保證免費。除了 Cloud Run,映像儲存、建置方式、網路傳輸、網域註冊及 DNS 都要分別計入;優惠額度也要確認適用項目與期限。Cloud Run 計費範圍

設定預算通知、合理的擴展上限及映像保留策略。一般 alerts-only 預算只發通知,不是自動停止花費的硬上限。預算通知說明

平常新增文章時,選入發布清單、更新群組規則、同步副本並提交版本。新增朋友時,同時檢查 OAuth 測試狀態/名單、IAP 入口政策與網站群組;移除朋友也要處理這些層次,而不是只刪掉一個首頁連結。

十一、第一次照做的實用順序

  1. 先拿幾篇沒有個資的測試 MD,在本機做出 Quartz 頁面。
  2. 建立私人 GitHub,選定發布來源與中央權限格式。
  3. 在 Cloud Run 跑起自己的受保護後端,先只允許自己。
  4. 完成 Google OAuth/IAP,先測預設網址與附件直連。
  5. 加入一位朋友,測「自己可讀、朋友可讀、朋友不可讀」三種情況。
  6. 串 GitHub Actions 自動建置與部署。
  7. 選定適合用途的網域方案,再處理 DNS 與 HTTPS。
  8. 測手機、搜尋與權限後,再逐批增加正式內容。

這個做法保留 MD 的可攜性、Git 的版本歷史與固定網域入口。真正需要額外設計的,是「選哪些內容發布」以及「登入後只回傳哪些資料」。


查核日期:2026-10-25。官方產品介面、可用區域與限制可能變更,實作時請以各節官方連結為準。文中以占位資料說明流程,不含實際讀者名單、憑證、計費帳戶、專案編號或私人原始筆記。