用 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:它備份的是提交進去的網站程式與核准副本,完整筆記庫仍需自己的備份方式。
三、用中央表管理朋友與文章,不必逐篇加屬性
本案例把「讀者名單」和「資料夾規則」集中管理。以下是自訂格式示意,所有信箱都是占位範例。
讀者群組
| 群組 | Google 帳號 |
|---|---|
| owner | admin@example.com |
| family | family-reader@example.com |
| friends | friend-reader@example.com |
發布與閱讀規則
| 原稿範圍 | 閱讀群組 | 分類 | 發布 |
|---|---|---|---|
| Personal/Car/** | owner | 車輛 | 是 |
| Family/Trips/** | family | 旅遊 | 是 |
| Shared/Podcast/** | owner,friends | Podcast | 是 |
這套實作的 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 部署聯邦身分流程
我們的工作流程是:
- 檢出私人 repo 的核准版本,安裝鎖定版本的依賴。
- 檢查發布清單、來源存在與中央權限規則。
- 依可讀群組先排除不該出現的內容,再建置 Quartz。
- 測試登入驗證、文章/附件權限、搜尋結果與手機載入大小。
- 將通過測試的容器推到 Artifact Registry,再更新 Cloud Run。
- 核對實際 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 設定順序
- 決定主網址:例如
notes.example.com,或根網域example.com。 - 在 Cloud Run 建立對應的 domain mapping,完成網域所有權驗證。
- 在 Route 53 的正確 Hosted Zone,填入 Cloud Run 回傳的紀錄;不要自行猜 IP 或沿用別人的值。
- 子網域可能使用 CNAME;根網域依平台提供 A/AAAA。Route 53 的根網域不能直接放一般 CNAME。Route 53 紀錄類型
- 移除或替換同一主機名稱中衝突的舊網站紀錄;保留郵件 MX、SPF、DKIM、DMARC 等無關紀錄。若平台要求 IPv6,就保留正確 AAAA,不要套用「一律刪掉 AAAA」的做法。
- 等 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 入口政策與網站群組;移除朋友也要處理這些層次,而不是只刪掉一個首頁連結。
十一、第一次照做的實用順序
- 先拿幾篇沒有個資的測試 MD,在本機做出 Quartz 頁面。
- 建立私人 GitHub,選定發布來源與中央權限格式。
- 在 Cloud Run 跑起自己的受保護後端,先只允許自己。
- 完成 Google OAuth/IAP,先測預設網址與附件直連。
- 加入一位朋友,測「自己可讀、朋友可讀、朋友不可讀」三種情況。
- 串 GitHub Actions 自動建置與部署。
- 選定適合用途的網域方案,再處理 DNS 與 HTTPS。
- 測手機、搜尋與權限後,再逐批增加正式內容。
這個做法保留 MD 的可攜性、Git 的版本歷史與固定網域入口。真正需要額外設計的,是「選哪些內容發布」以及「登入後只回傳哪些資料」。
查核日期:2026-10-25。官方產品介面、可用區域與限制可能變更,實作時請以各節官方連結為準。文中以占位資料說明流程,不含實際讀者名單、憑證、計費帳戶、專案編號或私人原始筆記。