<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ShengXing Chi</title>
    <description>The latest articles on DEV Community by ShengXing Chi (@ocxers).</description>
    <link>https://dev.to/ocxers</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559317%2Fb1bae47f-b681-4d57-a759-fce44737400d.png</url>
      <title>DEV Community: ShengXing Chi</title>
      <link>https://dev.to/ocxers</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ocxers"/>
    <language>en</language>
    <item>
      <title>Building a bilingual browser game with Astro and Cloudflare (and making the HTML actually cache)</title>
      <dc:creator>ShengXing Chi</dc:creator>
      <pubDate>Mon, 28 Sep 2026 19:53:40 +0000</pubDate>
      <link>https://dev.to/ocxers/building-a-bilingual-browser-game-with-astro-and-cloudflare-and-making-the-html-actually-cache-3o2b</link>
      <guid>https://dev.to/ocxers/building-a-bilingual-browser-game-with-astro-and-cloudflare-and-making-the-html-actually-cache-3o2b</guid>
      <description>&lt;p&gt;I recently shipped &lt;a href="https://cavienchien.net/en/" rel="noopener noreferrer"&gt;Cá Viên Chiên&lt;/a&gt;, a small cooking game about Vietnamese fried fish ball street carts. It runs in the browser, works best on phones, and ships in Vietnamese and English. Here are the parts that were more interesting than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Astro for a game
&lt;/h2&gt;

&lt;p&gt;Most of the site isn't the game — it's the landing page, guide, tips, menu and FAQ in two languages. Those pages matter for search, so they should be plain static HTML. Astro gives me that, and the game itself is a single island of vanilla JS (~26 KB) with no framework runtime. CSS is under 5 KB.&lt;/p&gt;

&lt;h2&gt;
  
  
  i18n routing: /vi/ and /en/
&lt;/h2&gt;

&lt;p&gt;Each language lives under its own prefix, with &lt;code&gt;hreflang&lt;/code&gt; alternates pointing at each other. The root &lt;code&gt;/&lt;/code&gt; redirects based on &lt;code&gt;Accept-Language&lt;/code&gt;, which turned out to be the one thing I couldn't cache cleanly (more below).&lt;/p&gt;

&lt;h2&gt;
  
  
  Saving without accounts
&lt;/h2&gt;

&lt;p&gt;The game saves to &lt;code&gt;localStorage&lt;/code&gt; after every purchase and at the end of each day. To move to another device, players copy a save code — the serialized state — and paste it on the other side. No backend, no sign-up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting HTML to actually hit the edge cache
&lt;/h2&gt;

&lt;p&gt;Static assets under &lt;code&gt;_astro/*&lt;/code&gt; are hashed and served with &lt;code&gt;max-age=31536000, immutable&lt;/code&gt;, so those were fine from day one. HTML was the slow part: every request showed &lt;code&gt;cf-cache-status: DYNAMIC&lt;/code&gt; and ~250 ms TTFB going back to origin.&lt;/p&gt;

&lt;p&gt;Adding a Cloudflare Cache Rule for &lt;code&gt;/vi/*&lt;/code&gt; and &lt;code&gt;/en/*&lt;/code&gt; flipped the status to &lt;code&gt;HIT&lt;/code&gt; — but TTFB barely moved. The origin was still sending &lt;code&gt;cache-control: public, max-age=0, must-revalidate&lt;/code&gt;, and with the rule set to respect origin headers, the edge TTL was effectively zero: every request revalidated.&lt;/p&gt;

&lt;p&gt;The fix I'm rolling out is &lt;code&gt;CDN-Cache-Control&lt;/code&gt;, which Cloudflare honours ahead of &lt;code&gt;Cache-Control&lt;/code&gt;, while browsers keep seeing &lt;code&gt;max-age=0&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vcl"&gt;&lt;code&gt;&lt;span class="o"&gt;/&lt;/span&gt;vi&lt;span class="o"&gt;/*&lt;/span&gt;
  CDN-Cache-Control: max-age&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;86400&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;en&lt;span class="o"&gt;/*&lt;/span&gt;
  CDN-Cache-Control: max-age&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;86400&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then purge &lt;code&gt;/vi/&lt;/code&gt; and &lt;code&gt;/en/&lt;/code&gt; in the deploy step so new releases show up immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Move the language redirect on &lt;code&gt;/&lt;/code&gt; to the edge so it can be cached per language&lt;/li&gt;
&lt;li&gt;Self-host the font to drop two extra origins&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to see the result, the game is at &lt;a href="https://cavienchien.net/en/" rel="noopener noreferrer"&gt;cavienchien.net&lt;/a&gt;. Feedback on the difficulty curve is very welcome.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>gamedev</category>
      <category>cloudflare</category>
      <category>webperf</category>
    </item>
  </channel>
</rss>
