<?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: ShawGames</title>
    <description>The latest articles on DEV Community by ShawGames (@shawgames_320899600986f8d).</description>
    <link>https://dev.to/shawgames_320899600986f8d</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%2F3894463%2Fef04bf99-30d7-4332-9ad7-0841f6fe5b4a.png</url>
      <title>DEV Community: ShawGames</title>
      <link>https://dev.to/shawgames_320899600986f8d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shawgames_320899600986f8d"/>
    <language>en</language>
    <item>
      <title>I built an unblocked games site with 250 games on my own CDN, here's what broke</title>
      <dc:creator>ShawGames</dc:creator>
      <pubDate>Sun, 02 Aug 2026 09:10:36 +0000</pubDate>
      <link>https://dev.to/shawgames_320899600986f8d/i-built-an-unblocked-games-site-with-250-games-on-my-own-cdn-heres-what-broke-2bc3</link>
      <guid>https://dev.to/shawgames_320899600986f8d/i-built-an-unblocked-games-site-with-250-games-on-my-own-cdn-heres-what-broke-2bc3</guid>
      <description>&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;I recently built &lt;a href="https://shawgames.com" rel="noopener noreferrer"&gt;ShawGames&lt;/a&gt;, a browser-based gaming site with more than 250 games.&lt;/p&gt;

&lt;p&gt;At first, the architecture looked simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build the frontend with Next.js&lt;/li&gt;
&lt;li&gt;Export it as a static site&lt;/li&gt;
&lt;li&gt;Store large game files in Cloudflare R2&lt;/li&gt;
&lt;li&gt;Serve everything through a custom CDN domain&lt;/li&gt;
&lt;li&gt;Deploy the frontend separately&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, the frontend was the easy part.&lt;/p&gt;

&lt;p&gt;The difficult part was serving hundreds of games reliably when every game had a different file structure, different asset-loading behavior, and different assumptions about where its files were hosted.&lt;/p&gt;

&lt;p&gt;This is the technical story of what worked, what broke, and what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;The site is based on a statically exported Next.js frontend.&lt;/p&gt;

&lt;p&gt;Instead of running a persistent Node.js server, I generate static HTML, JavaScript, CSS, and JSON files during the build process.&lt;/p&gt;

&lt;p&gt;The basic flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Next.js application
        |
        | next build + static export
        v
Static frontend hosting
        |
        | game metadata and launch URLs
        v
Cloudflare R2
        |
        | custom CDN domain
        v
Game files, assets, WebAssembly, audio, and textures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend contains the catalogue, search interface, categories, game pages, and navigation.&lt;/p&gt;

&lt;p&gt;The actual game files are stored separately in Cloudflare R2.&lt;/p&gt;

&lt;p&gt;This separation matters because some games are only a few megabytes, while others include large WebAssembly binaries, texture files, audio files, or Unity build artifacts.&lt;/p&gt;

&lt;p&gt;Putting all of that inside the main frontend deployment would make builds slower, deployments heavier, and asset management much harder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I used a static Next.js export
&lt;/h2&gt;

&lt;p&gt;A gaming catalogue is a good fit for static generation.&lt;/p&gt;

&lt;p&gt;Most pages do not require server-side rendering on every request. A game page mainly needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A title&lt;/li&gt;
&lt;li&gt;A description&lt;/li&gt;
&lt;li&gt;A thumbnail&lt;/li&gt;
&lt;li&gt;A category&lt;/li&gt;
&lt;li&gt;A launch URL&lt;/li&gt;
&lt;li&gt;Some related-game data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That information can be generated at build time.&lt;/p&gt;

&lt;p&gt;A static export also gave me a few useful properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No application server to maintain&lt;/li&gt;
&lt;li&gt;Fewer runtime failure points&lt;/li&gt;
&lt;li&gt;Easy deployment to edge hosting&lt;/li&gt;
&lt;li&gt;Fast delivery of HTML and JavaScript&lt;/li&gt;
&lt;li&gt;Lower infrastructure complexity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off is that anything dynamic has to be handled in the browser or through external services.&lt;/p&gt;

&lt;p&gt;It also means asset paths must be correct before deployment. A server-rendered application can sometimes rewrite or proxy broken paths at runtime. A static export cannot. If the generated URL is wrong, the browser simply requests the wrong file.&lt;/p&gt;

&lt;p&gt;That became important later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the games live in Cloudflare R2
&lt;/h2&gt;

&lt;p&gt;The game library contains many different kinds of browser games.&lt;/p&gt;

&lt;p&gt;A simple HTML5 game might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;game/
├── index.html
├── game.js
├── style.css
└── assets/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Unity WebGL game might look more like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;game/
├── index.html
├── Build/
│   ├── game.loader.js
│   ├── game.framework.js.unityweb
│   ├── game.data.unityweb
│   └── game.wasm.unityweb
└── TemplateData/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other games dynamically load files from nested folders, construct asset URLs at runtime, or expect to run from the root of a domain.&lt;/p&gt;

&lt;p&gt;R2 gave me object storage without forcing those files into the frontend repository.&lt;/p&gt;

&lt;p&gt;Each game could be uploaded as its own directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/games/game-one/index.html
/games/game-one/assets/texture.png
/games/game-two/index.html
/games/game-two/build/game.wasm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend then launches the relevant &lt;code&gt;index.html&lt;/code&gt; file through the CDN domain.&lt;/p&gt;

&lt;p&gt;Conceptually, it sounds straightforward.&lt;/p&gt;

&lt;p&gt;The main problem is that browser games are not always portable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Serving games from R2 was the hardest part
&lt;/h2&gt;

&lt;p&gt;The biggest technical challenge was not uploading files. It was preserving the exact path structure each game expected.&lt;/p&gt;

&lt;p&gt;Consider this code inside a game:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./assets/level-1.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the game is loaded from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://cdn.example.com/games/my-game/index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the browser should request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://cdn.example.com/games/my-game/assets/level-1.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works as long as the document URL, base path, redirects, and object paths all remain consistent.&lt;/p&gt;

&lt;p&gt;But games often contain assumptions such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/level-1.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That leading slash changes everything. The browser now requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://cdn.example.com/assets/level-1.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://cdn.example.com/games/my-game/assets/level-1.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The game may work perfectly on the original publisher’s domain but fail when moved into a subdirectory.&lt;/p&gt;

&lt;p&gt;Other common problems included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Case-sensitive file names&lt;/li&gt;
&lt;li&gt;Hardcoded asset domains&lt;/li&gt;
&lt;li&gt;Missing MIME types&lt;/li&gt;
&lt;li&gt;Incorrect compression headers&lt;/li&gt;
&lt;li&gt;WebAssembly loading failures&lt;/li&gt;
&lt;li&gt;Nested paths that were not uploaded&lt;/li&gt;
&lt;li&gt;Files referenced dynamically but absent from the initial build&lt;/li&gt;
&lt;li&gt;Service workers caching outdated files&lt;/li&gt;
&lt;li&gt;Cross-origin restrictions&lt;/li&gt;
&lt;li&gt;Games expecting same-origin storage or cookies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single missing texture might only cause a visual glitch.&lt;/p&gt;

&lt;p&gt;A missing JavaScript bundle, &lt;code&gt;.wasm&lt;/code&gt; file, or game data file can prevent the game from starting entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why third-party embeds get blocked
&lt;/h2&gt;

&lt;p&gt;One early approach was to embed games directly from third-party websites.&lt;/p&gt;

&lt;p&gt;Technically, that is often as simple as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://third-party-game-site.example/game"&lt;/span&gt;
  &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt;
  &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem is that the game is still being loaded from the third-party domain.&lt;/p&gt;

&lt;p&gt;The page containing the iframe may be allowed, while the iframe source is blocked by a school or organizational filtering system.&lt;/p&gt;

&lt;p&gt;Filters can evaluate more than the domain shown in the browser’s address bar. They may inspect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The iframe source&lt;/li&gt;
&lt;li&gt;DNS requests&lt;/li&gt;
&lt;li&gt;Network destinations&lt;/li&gt;
&lt;li&gt;URL categories&lt;/li&gt;
&lt;li&gt;Page reputation&lt;/li&gt;
&lt;li&gt;Content classification&lt;/li&gt;
&lt;li&gt;Known gaming or entertainment domains&lt;/li&gt;
&lt;li&gt;Requests made by scripts after the page loads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates an important distinction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Main site domain: allowed
Embedded game domain: blocked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the user’s perspective, the site opens but the game frame remains blank or displays an error.&lt;/p&gt;

&lt;p&gt;Third-party embedding also creates operational risk. The external provider can change a URL, add iframe restrictions, remove the game, introduce advertising, or block embedding without notice.&lt;/p&gt;

&lt;p&gt;Hosting permitted game assets directly gave me more control over availability, performance, and file paths. It did not eliminate network filtering, but it removed the dependency on dozens of unrelated third-party game hosts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The asset outage
&lt;/h2&gt;

&lt;p&gt;The most serious recent issue was an asset outage caused by caching and CDN behavior.&lt;/p&gt;

&lt;p&gt;The frontend was still online.&lt;/p&gt;

&lt;p&gt;Game pages still loaded.&lt;/p&gt;

&lt;p&gt;The catalogue and thumbnails appeared normal.&lt;/p&gt;

&lt;p&gt;But some game assets failed to load from the CDN, which meant affected games either froze during startup, displayed a loading screen indefinitely, or opened with missing resources.&lt;/p&gt;

&lt;p&gt;This was difficult to diagnose because it did not look like a full outage.&lt;/p&gt;

&lt;p&gt;The application shell worked, and requests sometimes behaved differently depending on whether the asset had already been cached.&lt;/p&gt;

&lt;p&gt;That created inconsistent reports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A game worked for one user&lt;/li&gt;
&lt;li&gt;The same game failed for another&lt;/li&gt;
&lt;li&gt;A refreshed page sometimes behaved differently&lt;/li&gt;
&lt;li&gt;Newly uploaded files did not always appear immediately&lt;/li&gt;
&lt;li&gt;Some requests returned stale responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The failure was in the asset-delivery layer rather than the Next.js application itself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj9ngljliy8ljhbcrb8g2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj9ngljliy8ljhbcrb8g2.png" alt="Architecture diagram showing a static Next.js frontend sending game requests through a custom CDN to Cloudflare R2 object storage" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why CDN caching made debugging harder
&lt;/h2&gt;

&lt;p&gt;Caching is normally a performance feature.&lt;/p&gt;

&lt;p&gt;For static game assets, it is especially valuable because large JavaScript bundles, textures, audio files, and WebAssembly binaries do not need to be fetched from object storage on every request.&lt;/p&gt;

&lt;p&gt;A simplified request flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser
   |
   v
Cloudflare edge cache
   |
   | cache miss
   v
R2 object storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the cache contains a valid object, the response is fast.&lt;/p&gt;

&lt;p&gt;The problem appears when the cache contains an outdated or incorrect response.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An asset URL is requested before the file exists.&lt;/li&gt;
&lt;li&gt;The CDN caches the error response.&lt;/li&gt;
&lt;li&gt;The missing file is uploaded to R2.&lt;/li&gt;
&lt;li&gt;The browser requests the same URL again.&lt;/li&gt;
&lt;li&gt;The edge still serves the cached error.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The origin is now correct, but the user continues to receive the old result.&lt;/p&gt;

&lt;p&gt;A similar issue can happen when replacing a file without changing its URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/game/build/game.data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that URL is cached aggressively, uploading a new version does not guarantee that every edge location immediately serves the new object.&lt;/p&gt;

&lt;p&gt;This is why immutable asset naming is so useful.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;game.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;use something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;game.8f31c2.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the contents change, the URL changes as well. The CDN treats it as a new object rather than reusing a stale cached response.&lt;/p&gt;

&lt;p&gt;Many third-party games are not built with this deployment model in mind, so their asset names cannot always be changed safely.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I fixed it
&lt;/h2&gt;

&lt;p&gt;The fix involved treating cache state as part of the deployment process rather than assuming the CDN would immediately reflect the contents of R2.&lt;/p&gt;

&lt;p&gt;The main improvements were:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Verifying assets directly at the origin layer
&lt;/h3&gt;

&lt;p&gt;I checked whether affected files actually existed in R2 and whether their object keys matched the requested URLs exactly.&lt;/p&gt;

&lt;p&gt;This helped separate storage problems from cache problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Purging stale cached responses
&lt;/h3&gt;

&lt;p&gt;Cached errors and outdated assets needed to be invalidated so edge locations would request the current object again.&lt;/p&gt;

&lt;p&gt;Without this step, fixing the object in storage was not always enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Auditing cache rules
&lt;/h3&gt;

&lt;p&gt;Not every response should have the same cache policy.&lt;/p&gt;

&lt;p&gt;Long-lived caching is useful for versioned static assets.&lt;/p&gt;

&lt;p&gt;It is less appropriate for error responses, frequently replaced files, or launch documents such as &lt;code&gt;index.html&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A safer pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Versioned game assets:
Cache aggressively

Game index files:
Use shorter caching

Error responses:
Avoid long cache lifetimes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Testing from a clean browser session
&lt;/h3&gt;

&lt;p&gt;A normal refresh is not always enough because the browser may have its own cached copy.&lt;/p&gt;

&lt;p&gt;Testing needed to include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incognito sessions&lt;/li&gt;
&lt;li&gt;Disabled browser cache&lt;/li&gt;
&lt;li&gt;Direct asset requests&lt;/li&gt;
&lt;li&gt;Multiple devices or networks&lt;/li&gt;
&lt;li&gt;Cache-status response headers&lt;/li&gt;
&lt;li&gt;Requests with temporary cache-busting query parameters&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Adding post-upload validation
&lt;/h3&gt;

&lt;p&gt;Uploading a game is not the same as confirming it works.&lt;/p&gt;

&lt;p&gt;A better deployment process checks the launch document and critical assets after upload.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;criticalFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;index.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;game.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;build/game.wasm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;build/game.data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;criticalFiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; returned &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact critical files differ by game, but even a basic validation step catches many failures before users do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance after the fix
&lt;/h2&gt;

&lt;p&gt;Once the CDN and cache behavior were corrected, game loading became noticeably faster and more consistent.&lt;/p&gt;

&lt;p&gt;The improvement came from several layers working together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Static Next.js pages load quickly&lt;/li&gt;
&lt;li&gt;Game assets are geographically cached&lt;/li&gt;
&lt;li&gt;Large files are served separately from the frontend&lt;/li&gt;
&lt;li&gt;Repeat requests can be fulfilled from the edge&lt;/li&gt;
&lt;li&gt;The main application deployment remains small&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest benefit was not just raw speed.&lt;/p&gt;

&lt;p&gt;It was predictability.&lt;/p&gt;

&lt;p&gt;A fast game that randomly fails is worse than a slightly slower game that loads every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do differently
&lt;/h2&gt;

&lt;p&gt;If I were rebuilding the asset pipeline from the beginning, I would make validation and cache management first-class parts of the system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Normalize every game before upload
&lt;/h3&gt;

&lt;p&gt;Each game should pass through an ingestion process that checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether &lt;code&gt;index.html&lt;/code&gt; exists&lt;/li&gt;
&lt;li&gt;Whether referenced local assets exist&lt;/li&gt;
&lt;li&gt;Whether paths are relative or absolute&lt;/li&gt;
&lt;li&gt;Whether external domains are required&lt;/li&gt;
&lt;li&gt;Whether the game uses WebAssembly&lt;/li&gt;
&lt;li&gt;Whether required MIME types are supported&lt;/li&gt;
&lt;li&gt;Whether filenames differ only by case&lt;/li&gt;
&lt;li&gt;Whether service workers are present&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Maintain a game manifest
&lt;/h3&gt;

&lt;p&gt;Each game could have a manifest such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"slug"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example-game"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"index.html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"engine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unity-webgl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"criticalAssets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Build/game.loader.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Build/game.data.unityweb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Build/game.wasm.unityweb"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requiresExternalNetwork"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This would make automated testing much easier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separate deployments from cache invalidation
&lt;/h3&gt;

&lt;p&gt;A complete deployment should include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Upload files
    |
Validate object keys
    |
Test critical URLs
    |
Invalidate affected cache entries
    |
Run launch check
    |
Mark game as available
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A game should not appear publicly until every stage succeeds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add synthetic monitoring
&lt;/h3&gt;

&lt;p&gt;A scheduled checker could request a sample of games and verify that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The launch page returns &lt;code&gt;200&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Critical JavaScript loads&lt;/li&gt;
&lt;li&gt;WebAssembly assets are accessible&lt;/li&gt;
&lt;li&gt;Response content types are correct&lt;/li&gt;
&lt;li&gt;CDN responses are not stale errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This would detect partial asset outages before they affect a large number of sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The main lesson
&lt;/h2&gt;

&lt;p&gt;Building a site with hundreds of browser games is not primarily a frontend challenge.&lt;/p&gt;

&lt;p&gt;The frontend can be statically generated and deployed easily.&lt;/p&gt;

&lt;p&gt;The real complexity is in asset portability and delivery:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every game has different assumptions&lt;/li&gt;
&lt;li&gt;Relative paths matter&lt;/li&gt;
&lt;li&gt;CDN caches can preserve failures&lt;/li&gt;
&lt;li&gt;Browser caches can hide fixes&lt;/li&gt;
&lt;li&gt;Third-party embeds introduce external dependencies&lt;/li&gt;
&lt;li&gt;A successful upload does not guarantee a successful launch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cloudflare R2 and a static Next.js export turned out to be a good combination, but only after I treated the CDN as an active part of the system rather than a transparent layer in front of storage.&lt;/p&gt;

&lt;p&gt;The recent outage reinforced one rule I now consider essential:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Never consider an asset deployment complete until it has been tested through the same public CDN URL that users will request.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The current version is live at &lt;a href="https://shawgames.com" rel="noopener noreferrer"&gt;shawgames.com&lt;/a&gt;, and the next step is making the ingestion and validation pipeline as reliable as the frontend itself.&lt;/p&gt;

&lt;p&gt;Have you dealt with stale CDN responses, cached 404s, or browser games that broke after being moved to a different host? I would be interested to hear how you handled it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>javascript</category>
      <category>gamedev</category>
    </item>
  </channel>
</rss>
