{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "PhiPhi的奇妙冒險 (繁體中文)",
  "home_page_url": "https://normco.re/",
  "feed_url": "https://normco.re/zh-hant/feed.json",
  "description": "Phiphi 的個人部落格，寫於中國成都。",
  "items": [
    {
      "id": "https://normco.re/zh-hant/posts/alibaba-cloud-oss-cdn-deployment/",
      "url": "https://normco.re/zh-hant/posts/alibaba-cloud-oss-cdn-deployment/",
      "title": "這個部落格如何部署到阿里雲 OSS 與 CDN",
      "content_html": "<p>這個站點使用 Lume 建置，並部署到阿里雲 OSS，再由阿里雲 CDN 對外分發。整條部署\n流水線刻意保持精簡：一個 GitHub Workflow、讓 Deno 出現在 runner 上，再加上一個\n自訂 Action；它會還原本地建置快取、執行建置、同步 OSS 並完成清理：\n<a href=\"https://github.com/frenchvandal/aliyun-oss-cdn-sync-action\"><code>frenchvandal/aliyun-oss-cdn-sync-action</code></a>。</p>\n<p>我現在希望同時滿足五件事：短時憑證、在全新 runner 上也能熱啟動的建置、可預測\n上傳、與檔案型別相匹配的快取標頭，以及自動的快取一致性。這套配置同時做到五者，\n而且不需要在每個倉庫裡額外維護部署腳本。</p>\n<h2>流水線總覽</h2>\n<p>從長期生產配置來看，這個倉庫中的 GitHub Workflow 現在做三件事：</p>\n<ol>\n<li>檢出倉庫程式碼。</li>\n<li>依 <code>.tool-versions</code> 安裝固定版本的 Deno，並啟用 runner 端工具快取。</li>\n<li>呼叫 OSS/CDN 同步 Action；它會還原 <code>_cache</code>、執行 <code>deno task build</code>、上傳\n<code>_site</code>、刷新 CDN，並完成清理。</li>\n</ol>\n<p>目前，這一步建置也會為共享樣式與按路由拆分的關鍵 CSS 產生指紋，移除 source\nmap，並在同步 <code>_site</code> 之前刪除最終 HTML 未引用的可選 Pagefind 檔案。</p>\n<pre><code class=\"language-yaml\">name: Deploy static content to OSS\n\non:\n  push:\n    branches: [&quot;master&quot;]\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  id-token: write\n\nconcurrency:\n  group: &quot;oss-deploy&quot;\n  cancel-in-progress: true\n\njobs:\n  deploy:\n    environment:\n      name: development\n    runs-on: macos-26\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Setup Deno environment\n        uses: denoland/setup-deno@v2\n        with:\n          deno-version-file: .tool-versions\n          cache: true\n\n      - name: Deploy to Alibaba Cloud OSS\n        uses: frenchvandal/aliyun-oss-cdn-sync-action@master\n        with:\n          role-oidc-arn: ${{ secrets.ALIBABA_CLOUD_ROLE_ARN }}\n          oidc-provider-arn: ${{ secrets.ALIBABA_CLOUD_OIDC_PROVIDER_ARN }}\n          role-session-name: ${{ github.run_id }}\n          role-session-expiration: 3600\n          cache-enabled: true\n          cache-key: &gt;-\n            lume-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('deno.lock') }}-${{ hashFiles('_cache/**/*') || 'empty' }}\n          cache-restore-keys: |\n            lume-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('deno.lock') }}-\n            lume-cache-${{ runner.os }}-${{ runner.arch }}-\n          input-dir: _site\n          bucket: ${{ secrets.OSS_BUCKET }}\n          region: ${{ secrets.OSS_REGION }}\n          audience: ${{ github.repository_id }}\n          cdn-enabled: true\n          cdn-base-url: ${{ secrets.OSS_CDN_BASE_URL }}\n          cdn-actions: refresh\n          build-command: deno task build\n</code></pre>\n<p>在這個倉庫裡，我現在只保留 <code>cdn-actions: refresh</code>。Action 仍支援\n<code>refresh,preload</code>，只是這個站點目前不再為每次部署啟用 preload。</p>\n<h2>為什麼選 OIDC，而不是長期 Access Key</h2>\n<p>這個 Action 在執行時透過 GitHub OIDC 去扮演阿里雲 RAM 角色。倉庫中不保存長期\nAccess Key。Workflow 只需要 <code>id-token: write</code>，再加上 RAM 角色與 OIDC Provider\n的 ARN。</p>\n<p>現在的認證路徑也只走 OIDC。如果角色扮演失敗，Action 不會再從輸入或環境變數回退到\n靜態 Access Key。</p>\n<p>目前的配置還傳入了 <code>audience: ${{ github.repository_id }}</code>。GitHub 可以為自訂\naudience 簽發 ID token，而阿里雲 RAM 可以把這個 <code>aud</code> 值與 OIDC 身分提供者裡配置\n的 Client ID，以及角色信任策略中的條件進行校驗。這比單純依賴預設 audience\n更收斂。</p>\n<h2>Action 內部如何執行</h2>\n<p>這個 Action 被拆成三個階段：</p>\n<ul>\n<li><strong>pre</strong>：透過 OIDC 扮演 RAM 角色，並把臨時憑證寫入 action state。</li>\n<li><strong>main</strong>：按需還原本地 <code>_cache</code> 目錄，執行 <code>build-command</code>，上傳本地檔案到\nOSS，寫入快取標頭，並在啟用時執行 CDN 動作。</li>\n<li><strong>post</strong>：比較目標前綴下的遠端物件與本地檔案，刪除遠端孤兒物件，在需要時刷新已\n刪除 URL 的 CDN 快取，寫入 CDN 任務摘要，並在設定了 <code>cache-key</code> 時保存\n<code>_cache</code>。</li>\n</ul>\n<p>清理階段透過 <code>post-if: always()</code> 執行，所以即使前面步驟失敗也會執行。清理與 CDN\n呼叫，以及快取還原或保存，都被設計成「非致命」：會記錄警告，但不會因 CDN API\n或快取服務的短暫波動阻塞部署。</p>\n<h2>上傳、快取、配額與漂移控制</h2>\n<p>以下實作細節對可靠性非常關鍵：</p>\n<ul>\n<li><code>build-command</code> 在 Action 內部執行，而且發生在 OSS 上傳或 CDN 呼叫之前。</li>\n<li>如果 <code>cache-enabled</code> 為 <code>true</code> 且設定了 <code>cache-key</code>，就會先還原 <code>_cache</code>，\nbuild 結束後也可以在 <code>post</code> 階段保存新的快照。</li>\n<li><code>cache-enabled</code> 只控制還原路徑，所以我可以暫時關閉 warm cache，同時保留\npost-step 的快取保存。</li>\n<li>上傳透過 <code>max-concurrency</code> 平行化。</li>\n<li>全域 API 限速由 <code>api-rps-limit</code> 控制。</li>\n<li>每個檔案上傳失敗後最多重試三次。</li>\n<li>部分上傳失敗會透過 <code>failed-count</code> 和 GitHub Actions job summary 暴露出來，\n而不是埋在日誌裡。</li>\n<li>上傳階段現在會自動寫入 <code>Cache-Control</code>：HTML 與 <code>sw.js</code> 會被更積極地重新驗證，\n帶雜湊的資源會以 immutable 方式處理，常見靜態資源也會拿到更短的重新驗證窗口，\n而不是一刀切的統一策略。</li>\n<li><code>cdn-actions</code> 只接受 <code>refresh</code> 或 <code>refresh,preload</code>；如果 CDN\n已啟用而這個值為空或無效，Action 會回退到 <code>refresh</code>。</li>\n<li>Action 會在提交 refresh 或 preload 批次前檢查剩餘 CDN 配額。</li>\n<li>刪除 OSS 物件時可觸發對應 URL 的 CDN 刷新，減少過期快取窗口，也能抑制物件隨\n時間漂移。</li>\n<li>Action 還會為部署、清理和 CDN 任務狀態寫入 GitHub Actions job summary。</li>\n</ul>\n<p>最後一點很容易被低估：長期運行的靜態站點僅靠上傳遠遠不夠。你還需要刪除機制，以及\n對不應再存在物件的快取失效處理。</p>\n<h2>最小化 RAM 權限</h2>\n<p>權限上，這個角色需要目標 bucket 範圍內的 OSS 權限，用於列舉、上傳與刪除物件。\nCDN 一側，這個 workflow 需要 refresh 權限，以及用於查詢配額與任務狀態的唯讀 API\n權限。如果啟用 preload，再補上對應權限即可。此外，Trust Policy 必須允許 GitHub\nOIDC Provider 扮演部署角色。</p>\n<p>我把它獨立作為「部署角色」維護，不與更寬泛的營運權限混用。</p>\n<h2>在其他倉庫重用這個 Action</h2>\n<p>這個 Action 已發布，可直接重用：\n<a href=\"https://github.com/frenchvandal/aliyun-oss-cdn-sync-action\">github.com/frenchvandal/aliyun-oss-cdn-sync-action</a>。</p>\n<p>這個倉庫目前追蹤 <code>@master</code>，因為站點和 Action 是一起維護、一起驗證的。放到獨立\n倉庫時，我通常會固定到 <code>@v1</code>，或者更進一步固定到完整的 commit SHA。我現在也把\nbuild 和 <code>_cache</code> 生命週期交給 Action 自己管理，讓消費端 workflow 更短，也更偏\n向宣告式配置。</p>\n<p>對我來說，最關鍵的結果還是一樣：部署維持「無聊」。還原快取、build、sync、\nrefresh、cleanup，完成。</p>\n",
      "date_published": "2026-03-10T00:00:00Z"
    },
    {
      "id": "https://normco.re/zh-hant/posts/lorem-ipsum/",
      "url": "https://normco.re/zh-hant/posts/lorem-ipsum/",
      "title": "Lorem Ipsum 與占位文字的藝術",
      "content_html": "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor\nincididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis\nnostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>\n<p>很多人都聽過這段文字，卻很少知道它的來源。它其實來自西塞羅 <em>de Finibus Bonorum\net Malorum</em> 的一段打亂文本，這部哲學著作寫於西元前 45 年。自 16\n世紀起，它就被當作排版占位文字使用：一位不知名的印刷工把活字順序打亂，\n做出了一本字體樣張。</p>\n<p><img src=\"https://normco.re/posts/lorem-ipsum/images/amol-srivastava-uOYc6OlgpUI-unsplash.jpg\" alt=\"\"></p>\n<h2>為什麼占位文字仍然重要</h2>\n<p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu\nfugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in\nculpa qui officia deserunt mollit anim id est laborum.</p>\n<p>當你用真實文案設計版面時，你其實是在為那一批具體句子做設計。\n占位文字迫使你先為結構而不是語義做決定。這是一種很有價值的約束：\n它能暴露設計是否能承受不確定性，例如三行標題、沒有自然換氣點的段落，\n或是一個超出容器長度的長單字。</p>\n<h2>關於「擬真度」的問題</h2>\n<p>Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit\nlaboriosam? 在設計中，真實內容的還原度與抽象帶來的自由之間，總是存在張力。</p>\n<p>高保真原型需要先有真實內容。低保真線框圖則在不被語義分散注意力的前提下傳達結構。\n兩種方法都合理，關鍵是判斷當下該用哪一種工具。</p>\n<pre><code class=\"language-ts\">// A small utility to generate repeating text blocks.\nfunction lorem(words: number): string {\n  const base = &quot;lorem ipsum dolor sit amet consectetur adipiscing elit&quot;;\n  const tokens = base.split(&quot; &quot;);\n  const result: string[] = [];\n  for (let i = 0; i &lt; words; i++) {\n    result.push(tokens[i % tokens.length] ?? &quot;&quot;);\n  }\n  return result.join(&quot; &quot;);\n}\n</code></pre>\n<p>Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed\nquia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.</p>\n<p><img src=\"https://normco.re/posts/lorem-ipsum/images/scott-rodgerson-z0MDyylvY1k-unsplash.jpg\" alt=\"\"></p>\n<h2>結語</h2>\n<p>Lorem ipsum 不只是填充物。它像一面鏡子，照出設計在剝離語義後留下的純結構：\n有形式、無內容。也正因如此，它才能跨越這麼多時代持續存在。</p>\n",
      "date_published": "2026-02-18T00:00:00Z"
    },
    {
      "id": "https://normco.re/zh-hant/posts/vestibulum-ante/",
      "url": "https://normco.re/zh-hant/posts/vestibulum-ante/",
      "title": "Vestibulum Ante：關於門檻與新的開始",
      "content_html": "<p>Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia\ncurae. 拉丁詞 <em>vestibulum</em> 指前庭、入口、門前空間，\n它的語義重量遠比現代「門廳」一詞聽起來更深。</p>\n<p>門檻既不是結束，也不是開始。它是兩者之間的臨界地帶：開口前屏住的一口氣，\n鑰匙轉動後那一瞬停頓。它是「舊的已結束，而新的尚未開始」的位置。</p>\n<p><img src=\"https://normco.re/posts/vestibulum-ante/images/bruno-martins-4cwf-iW6I1Q-unsplash.jpg\" alt=\"\"></p>\n<h2>中間地帶的價值</h2>\n<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac\nturpis egestas. 我們生活在一個低估「過渡期」的文化裡。\n我們追求結果、產出與成品。那些混亂的中段、反覆草稿、猶疑時刻與緩慢累積的理解，\n往往既不被記錄，也不被慶祝。</p>\n<p>但人生大部分時間都發生在中間地帶。通勤路上、候診室裡，\n傳送一則訊息到收到回覆之間，\n以及「知道自己想要什麼」到「知道如何得到它」之間的那些年。</p>\n<h2>站在門口</h2>\n<p>Fusce suscipit varius mi. Cum sociis natoque penatibus et magnis dis parturient\nmontes, nascetur ridiculus mus. Phasellus viverra nulla ut metus varius laoreet.</p>\n<p>我在初秋的一個星期二搬到成都。這座城市的「溫暖」讓我意外，\n不是夏天的熱，而是一種「它已經決定要喜歡你」的溫度。\n街道聞起來是辣椒油與桂花的味道，一切都同時有一點陌生，也有一點親切。</p>\n<p>我想，這就是門檻的感覺。它既不敵對，也不主動歡迎。\n它只是敞開著，等你決定要把它變成什麼。</p>\n<h2>尾聲</h2>\n<p>Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi,\ncondimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit\neget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim\nac dui.</p>\n<p>每篇文本都有自己的「前廳」：標題頁、引言、第一段。\n這個空間負責讓讀者為接下來的內容做好準備。它值得被認真寫好。</p>\n",
      "date_published": "2025-09-04T00:00:00Z"
    },
    {
      "id": "https://normco.re/zh-hant/posts/proin-facilisis/",
      "url": "https://normco.re/zh-hant/posts/proin-facilisis/",
      "title": "Proin Facilisis：讓事情更輕鬆",
      "content_html": "<p><em>Proin facilisis</em> 在拉丁語裡可理解為「促進順暢」。它曾出現在古老植物學文本中，\n形容一種能幫助消化、疏通阻滯的植物。作為軟體設計哲學，這個詞意外地貼切。</p>\n<p>好的軟體會降低摩擦。它會提前預判使用者的下一步，在恰當時機給出恰當的可供性，\n然後退到背景裡。</p>\n<p><img src=\"https://normco.re/posts/proin-facilisis/images/brett-jordan-92-mTYj5oGs-unsplash.jpg\" alt=\"\"></p>\n<h2>先做「摩擦清單」</h2>\n<p>Proin in tellus sit amet nibh dignissim sagittis. 減少摩擦的第一步是把它畫出來：\n使用者在哪些環節會放慢？注意力在哪些節點會飆升？錯誤會集中出現在哪裡？</p>\n<p>在典型 Web\n應用中，高摩擦時刻通常很穩定：註冊引導、表單提交、錯誤復原、載入狀態。\n這些就是產品的「門廳」，是使用者為了抵達價值必須跨過的門檻。</p>\n<pre><code class=\"language-ts\">// Friction shows up in code too.\n// Compare these two approaches to handling a missing value:\n\n// High friction — the caller must always check:\nfunction getUser(id: string): User | undefined {/* … */}\n\n// Lower friction — the error is explicit and handled at the boundary:\nfunction getUser(id: string): User {\n  const user = db.find(id);\n  if (user === undefined) {\n    throw new Error(`User not found: ${id}`);\n  }\n  return user;\n}\n</code></pre>\n<h2>「簡單」本身的悖論</h2>\n<p>Vivamus pretium aliquet erat. 「讓事情變簡單」最困難的地方在於：它真的很難。\n你需要深入理解使用者、情境與失敗模式。也就是說，建構者要多做工作，使用者才能少費力。</p>\n<p>這也是為什麼「簡單」是一種慷慨。每移除一個不必要步驟，都是把時間還給使用者，\n讓他們去做真正重要的事。</p>\n<h2>如何把 facilisis 落地</h2>\n<p>Donec aliquet metus ut erat semper, et tincidunt nulla luctus.\n我經常回到這些原則：</p>\n<ul>\n<li>預設值應覆蓋多數人與多數情境。</li>\n<li>錯誤訊息應說明問題，也說明修復路徑。</li>\n<li>主流程不應要求額外思考。</li>\n<li>設定應當可選，而不是前置門檻。</li>\n<li>文件是產品的一部分，而不是附錄。</li>\n</ul>\n<p>Nulla facilisi. Phasellus blandit leo ut odio. Nam sed nulla non diam tincidunt\ntempus. 這個原則的名稱本身也提醒我們：<em>nulla\nfacilisi</em>，沒有真正「天然簡單」的事。好的簡單，從來都不是偶然。</p>\n",
      "date_published": "2025-04-12T00:00:00Z"
    },
    {
      "id": "https://normco.re/zh-hant/posts/instructions/",
      "url": "https://normco.re/zh-hant/posts/instructions/",
      "title": "如何安裝這個主題？",
      "content_html": "<p><strong>Simple blog</strong> 是一個為 Lume 準備的簡潔極簡部落格主題，支援標籤與作者。\n你可以<strong>在幾秒內</strong>建立自己的部落格，並為讀者提供 Atom 與 JSON 訂閱來源。</p>\n<p>設定這個主題<strong>最快最簡單</strong>的方法是使用\n<a href=\"https://deno.land/x/lume_init\">Lume init 指令</a>，你也可以直接從\n<a href=\"https://lume.land/theme/simple-blog/\">Simple Blog 主題頁面</a>複製。執行：</p>\n<pre><code class=\"language-bash\">deno run -A https://lume.land/init.ts --theme=simple-blog\n</code></pre>\n<p>即可建立一個已設定好 Simple Blog 的新專案。接著編輯部落格根目錄下的 <code>_data.yml</code>\n檔案，自訂站點標題、描述與中繼資料。</p>\n<p>文章需要儲存在 <code>posts</code> 目錄中，例如：<code>posts/my-first-post.md</code>。</p>\n<h2>以遠端主題安裝</h2>\n<p>若要將這個主題接入既有的 Lume 專案，可在 <code>_config.ts</code>\n中以遠端模組方式匯入。後續更新時，只要調整匯入 URL 裡的版本號：</p>\n<pre><code class=\"language-ts\">import lume from &quot;lume/mod.ts&quot;;\nimport blog from &quot;https://deno.land/x/lume_theme_simple_blog@v0.15.6/mod.ts&quot;;\n\nconst site = lume();\n\nsite.use(blog());\n\nexport default site;\n</code></pre>\n<p>然後把\n<a href=\"https://github.com/lumeland/theme-simple-blog/blob/main/src/_data.yml\"><code>_data.yml</code></a>\n複製到部落格根目錄，並填入你的資訊。</p>\n<h2>客製化</h2>\n<p>你也可以使用 <a href=\"https://lume.land/cms\">lumeCMS</a> 來客製部落格並更輕鬆地發佈內容。</p>\n",
      "date_published": "2022-06-12T00:00:00Z"
    }
  ],
  "language": "zh-Hant"
}
