{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "PhiPhi’s Bizarre Aventure",
  "home_page_url": "https://normco.re/",
  "feed_url": "https://normco.re/feed.json",
  "description": "Personal blog by Phiphi, based in Chengdu, China.",
  "items": [
    {
      "id": "https://normco.re/posts/alibaba-cloud-oss-cdn-deployment/",
      "url": "https://normco.re/posts/alibaba-cloud-oss-cdn-deployment/",
      "title": "How This Blog Deploys to Alibaba Cloud OSS and CDN",
      "content_html": "<p>This site is built with Lume and deployed to Alibaba Cloud OSS, with Alibaba\nCloud CDN in front of it. The deployment pipeline stays intentionally small: one\nGitHub workflow, Deno available on the runner, and one custom action that\nrestores a local build cache, runs the build, syncs OSS, and cleans up:\n<a href=\"https://github.com/frenchvandal/aliyun-oss-cdn-sync-action\"><code>frenchvandal/aliyun-oss-cdn-sync-action</code></a>.</p>\n<p>I now want five properties at the same time: short-lived credentials, warmed\nbuilds on fresh runners, predictable uploads, cache headers that match the files\nbeing shipped, and automatic cache coherence. This setup gives me all five\nwithout adding repository-specific deploy scripts.</p>\n<h2>Pipeline at a glance</h2>\n<p>In steady-state production, this repository’s workflow now does three things:</p>\n<ol>\n<li>Checks out the repository.</li>\n<li>Installs Deno with the pinned version from <code>.tool-versions</code> and enables the\nrunner-side tool cache.</li>\n<li>Runs the OSS/CDN sync action, which restores <code>_cache</code>, executes\n<code>deno task build</code>, uploads <code>_site</code>, refreshes CDN, and cleans up.</li>\n</ol>\n<p>At the moment, that build also fingerprints the shared and route-scoped critical\nCSS assets, strips source maps, and prunes optional Pagefind files that are not\nreferenced by the final HTML before <code>_site</code> is synced.</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>In this repository, I currently keep <code>cdn-actions: refresh</code>. The action still\nsupports <code>refresh,preload</code>, but this site’s workflow no longer enables preload\non every deployment.</p>\n<h2>Why OIDC instead of access keys</h2>\n<p>The action uses GitHub OIDC to assume an Alibaba Cloud RAM role at runtime. No\nlong-lived access key is stored in the repository. The workflow only needs\n<code>id-token: write</code> plus the RAM role ARN and the OIDC provider ARN.</p>\n<p>Authentication is now OIDC-only. The action does not fall back to static access\nkeys from inputs or environment variables if role assumption fails.</p>\n<p>The current setup also passes <code>audience: ${{ github.repository_id }}</code>. GitHub\ncan mint an ID token for a custom audience, and Alibaba Cloud RAM can validate\nthat <code>aud</code> value against the Client IDs configured on the OIDC identity provider\nand in the role trust policy. That gives me a tighter trust boundary than\nrelying on default audience handling alone.</p>\n<h2>How the action runs internally</h2>\n<p>The action is split into three phases:</p>\n<ul>\n<li><strong>pre</strong>: assumes the RAM role through OIDC and stores temporary credentials in\naction state.</li>\n<li><strong>main</strong>: optionally restores the local <code>_cache</code> directory, runs\n<code>build-command</code>, uploads local files to OSS, applies cache headers, and runs\nCDN actions if enabled.</li>\n<li><strong>post</strong>: compares remote objects under the destination prefix with local\nfiles produced by the build, deletes remote orphans, refreshes deleted URLs\nwhen needed, writes CDN task summaries, and saves <code>_cache</code> when <code>cache-key</code> is\nconfigured.</li>\n</ul>\n<p>The cleanup phase runs with <code>post-if: always()</code>, so it still executes even when\nearlier steps fail. Cleanup, CDN calls, and cache restore or save are\nintentionally best-effort: warnings are logged, but transient CDN or cache\nservice issues do not block the deployment.</p>\n<h2>Upload, cache, quota, and drift control</h2>\n<p>A few implementation details matter for reliability:</p>\n<ul>\n<li><code>build-command</code> runs inside the action before any OSS upload or CDN call.</li>\n<li>If <code>cache-enabled</code> is <code>true</code> and <code>cache-key</code> is configured, <code>_cache</code> restore\nhappens before the build and a fresh snapshot can be saved during <code>post</code>.</li>\n<li><code>cache-enabled</code> controls only restore, so I can skip warm-cache restore\ntemporarily without giving up the post-step cache save.</li>\n<li>Uploads are parallelized with <code>max-concurrency</code>.</li>\n<li>A global API throttle is applied through <code>api-rps-limit</code>.</li>\n<li>Each file upload is retried up to 3 times before being marked as failed.</li>\n<li>Partial upload failures are surfaced through <code>failed-count</code> and the GitHub job\nsummary instead of being buried in the logs.</li>\n<li>The upload step now writes <code>Cache-Control</code> headers automatically: HTML and\n<code>sw.js</code> are revalidated aggressively, hashed assets are uploaded as immutable,\nand common static assets get shorter revalidation windows instead of a\none-size-fits-all policy.</li>\n<li><code>cdn-actions</code> only accepts <code>refresh</code> or <code>refresh,preload</code>; if CDN is enabled\nand the value is empty or invalid, the action falls back to <code>refresh</code>.</li>\n<li>The action checks remaining CDN quota before submitting refresh or preload\nbatches.</li>\n<li>Deleted OSS objects can trigger CDN refresh for removed URLs, which reduces\nstale-cache windows and limits object drift over time.</li>\n<li>The action writes a GitHub Actions job summary for deployment, cleanup, and\nCDN task status.</li>\n</ul>\n<p>That last point is easy to miss: upload alone is not enough for a static site\nthat changes over time. You also need deletion and cache invalidation for\nobjects that should no longer exist.</p>\n<h2>Minimum RAM permissions</h2>\n<p>At the policy level, the role needs OSS permissions on the target bucket scope\nfor listing, uploading, and deleting objects. On the CDN side, this workflow\nneeds refresh permissions plus the read APIs used for quota checks and task\nlookups. If preload is enabled, add the corresponding preload permission too.\nThe trust policy must allow the GitHub OIDC provider to assume the deploy role.</p>\n<p>I keep this as a dedicated deploy role rather than mixing it with broader\noperational permissions.</p>\n<h2>Reusing the action in other repositories</h2>\n<p>The action is published and reusable:\n<a href=\"https://github.com/frenchvandal/aliyun-oss-cdn-sync-action\">github.com/frenchvandal/aliyun-oss-cdn-sync-action</a>.</p>\n<p>This repository currently tracks <code>@master</code> because I maintain the site and the\naction together. In a separate repository, I would usually pin <code>@v1</code> or, better\nyet, a full commit SHA. I also now let the action own the build and <code>_cache</code>\nlifecycle, which keeps consumer workflows shorter and more declarative.</p>\n<p>For me, the key result is that deploys stay boring: restore cache, build, sync,\nrefresh, cleanup, done.</p>\n",
      "date_published": "2026-03-10T00:00:00Z"
    },
    {
      "id": "https://normco.re/posts/lorem-ipsum/",
      "url": "https://normco.re/posts/lorem-ipsum/",
      "title": "Lorem Ipsum and the Art of Placeholder Text",
      "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>Most people know the phrase but few know its origin. It is a scrambled excerpt\nfrom Cicero’s <em>de Finibus Bonorum et Malorum</em>, a philosophical treatise written\nin 45 BC. The text has been used as filler copy since the 1500s, when an unknown\nprinter took a galley of type and scrambled it to make a type specimen book.</p>\n<p><img src=\"https://normco.re/posts/lorem-ipsum/images/amol-srivastava-uOYc6OlgpUI-unsplash.jpg\" alt=\"\"></p>\n<h2>Why placeholder text matters</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>When you design a layout with real words, you design for those specific words.\nPlaceholder text forces you to design for the structure, not the content. This\nis a valuable constraint—it reveals whether your design can accommodate the\nunexpected: a headline that runs to three lines, a paragraph with no natural\nbreak, a word too long for its container.</p>\n<h2>A question of fidelity</h2>\n<p>Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit\nlaboriosam? There is a tension in design between fidelity to real content and\nthe freedom that abstraction permits.</p>\n<p>High-fidelity prototypes with real copy require real copy to exist first.\nLow-fidelity wireframes communicate structure without the distraction of\nmeaning. Both approaches have their place. The key is knowing which tool serves\nthe moment.</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>Conclusion</h2>\n<p>Lorem ipsum is more than filler. It is a mirror held up to the design—one that\nreflects structure stripped of meaning, form without content. That is precisely\nwhy it endures.</p>\n",
      "date_published": "2026-02-18T00:00:00Z"
    },
    {
      "id": "https://normco.re/posts/vestibulum-ante/",
      "url": "https://normco.re/posts/vestibulum-ante/",
      "title": "Vestibulum Ante: On Thresholds and New Beginnings",
      "content_html": "<p>Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia\ncurae. The Latin word <em>vestibulum</em>—a forecourt, an entrance hall, the space\nbefore the door—carries more weight than its English descendant “vestibule”\nsuggests.</p>\n<p>A threshold is not an end, nor a beginning. It is the liminal space between the\ntwo: the breath held before the first word, the pause after a key turns in a\nlock. It is where one thing stops and another has not yet started.</p>\n<p><img src=\"https://normco.re/posts/vestibulum-ante/images/bruno-martins-4cwf-iW6I1Q-unsplash.jpg\" alt=\"\"></p>\n<h2>The value of the in-between</h2>\n<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac\nturpis egestas. We live in a culture that devalues transition. We want results,\noutcomes, the finished thing. The messy middle—the drafts, the doubt, the slow\naccumulation of understanding—goes undocumented and uncelebrated.</p>\n<p>But the in-between is where most of life happens. Commutes. Waiting rooms. The\ngap between sending a message and reading the reply. The years between knowing\nwhat you want and knowing how to get there.</p>\n<h2>Standing at the door</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>I moved to Chengdu on a Tuesday in early autumn. The city was warm in a way that\nsurprised me—not the heat of summer, but the warmth of a place that had already\ndecided it liked you. The streets smelled of chilli oil and osmanthus, and\neverything felt slightly, pleasantly unfamiliar.</p>\n<p>That is what a threshold feels like, I think. Not hostile. Not welcoming. Simply\nopen—waiting to see what you will make of it.</p>\n<h2>Coda</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>Every document has a vestibulum—the title page, the introduction, the first\nparagraph. It is the space that prepares the reader for what follows. Write it\nwith care.</p>\n",
      "date_published": "2025-09-04T00:00:00Z"
    },
    {
      "id": "https://normco.re/posts/proin-facilisis/",
      "url": "https://normco.re/posts/proin-facilisis/",
      "title": "Proin Facilisis: Making Things Easier",
      "content_html": "<p><em>Proin facilisis</em>—Latin for “promoting ease.” It appears in old botanical texts\nto describe a plant that aids digestion, smooths a passage, clears an\nobstruction. As a philosophy for building software, it translates remarkably\nwell.</p>\n<p>Good software reduces friction. It anticipates the user’s next step. It provides\nthe right affordance at the right moment. It gets out of the way.</p>\n<p><img src=\"https://normco.re/posts/proin-facilisis/images/brett-jordan-92-mTYj5oGs-unsplash.jpg\" alt=\"\"></p>\n<h2>The friction inventory</h2>\n<p>Proin in tellus sit amet nibh dignissim sagittis. The first step in reducing\nfriction is to map it. Where does the user slow down? Where does attention\nspike? Where do errors cluster?</p>\n<p>In a typical web application, the highest-friction moments are predictable:\nonboarding, form submission, error recovery, and loading states. These are the\nvestibules of the product—the thresholds users must cross to reach the value\ninside.</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>The paradox of ease</h2>\n<p>Vivamus pretium aliquet erat. There is a paradox at the heart of “making things\neasy”: it is very hard to do. Eliminating friction requires deep understanding\nof the user, the context, and the failure modes. It demands more work from the\nbuilder so that less work falls on the user.</p>\n<p>This is why ease is a form of generosity. Every unnecessary step you remove from\nyour user’s path is time returned to them—time they can spend on the thing that\nactually matters.</p>\n<h2>Facilisis in practice</h2>\n<p>Donec aliquet metus ut erat semper, et tincidunt nulla luctus. Some principles I\nreturn to:</p>\n<ul>\n<li>Defaults should be correct for most users, most of the time.</li>\n<li>Error messages should explain what went wrong and how to fix it.</li>\n<li>The happy path should require no thought.</li>\n<li>Configuration should be possible, never required.</li>\n<li>Documentation is part of the product.</li>\n</ul>\n<p>Nulla facilisi. Phasellus blandit leo ut odio. Nam sed nulla non diam tincidunt\ntempus. The name of this principle—<em>nulla facilisi</em>, “no easy thing”—is a\nreminder that ease, properly understood, is never accidental.</p>\n",
      "date_published": "2025-04-12T00:00:00Z"
    },
    {
      "id": "https://normco.re/posts/instructions/",
      "url": "https://normco.re/posts/instructions/",
      "title": "How to install this theme?",
      "content_html": "<p><strong>Simple blog</strong> is a clean and minimal blog theme for Lume, with support for\ntags and authors. It allows you to build your own blog <strong>in seconds</strong>, and\nprovides Atom and JSON feeds for your subscribers.</p>\n<p>The <strong>fastest and easiest</strong> way to configure this theme is the\n<a href=\"https://deno.land/x/lume_init\">Lume init command</a>, which you can also copy\neasily from the <a href=\"https://lume.land/theme/simple-blog/\">Simple Blog theme page</a>.\nRunning:</p>\n<pre><code class=\"language-bash\">deno run -A https://lume.land/init.ts --theme=simple-blog\n</code></pre>\n<p>will create a new project with Simple Blog configured. Edit the <code>_data.yml</code> file\nin your blog root folder with your data to customize the site title,\ndescription, and metadata.</p>\n<p>Posts must be saved in the <code>posts</code> folder. For example,\n<code>posts/my-first-post.md</code>.</p>\n<h2>Install as a remote theme</h2>\n<p>To add the theme to an existing Lume project, import it in your <code>_config.ts</code>\nfile as a remote module. Update it by changing the version number in the import\nURL:</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>Copy the\n<a href=\"https://github.com/lumeland/theme-simple-blog/blob/main/src/_data.yml\"><code>_data.yml</code></a>\nfile to your blog root folder and edit it with your data.</p>\n<h2>Customization</h2>\n<p>You can use <a href=\"https://lume.land/cms\">lumeCMS</a> to customize the blog and add\ncontent easily.</p>\n",
      "date_published": "2022-06-12T00:00:00Z"
    }
  ],
  "language": "en"
}
