A drag-and-drop deploy is not a sync. It is a replacement. Whatever is in the folder becomes the site; whatever is not in the folder stops existing.
Obvious written down. Still the reason one drop can quietly destroy two working games and 111 working redirects.
Your build folder is not your site
Our static-site generator writes pages. It never deletes them - a generator that prunes is a generator that can eat your work. The side effect is that the output directory is cumulative. It holds every page the site has ever had.
When we counted, ours held 111 directories for games no longer in the catalogue. Those were retired months ago. Each URL has a 301 to its category page, none is in the sitemap, and the redirects work fine.
Here is the part that bites:
A static host serves an existing file in preference to a redirect rule.
Ship those 111 directories and you do not merely republish 111 dead games. You shadow the 111 redirects that were doing their job. The rules stay in your config, still look correct, and silently stop firing.
So the bundle cannot be a copy of the build folder. It has to be assembled from an explicit allowlist.
The files that live only on production
Two of our games are self-hosted rather than embedded. Their code is in the project. Their assets are not - images, audio and sprite sheets had never been committed. They existed only on the live site.
That is the trap. Build a bundle without them, drag it, and the deploy does exactly what you asked: it deletes them. The game keeps its page and stops loading.
The assets were recoverable, because production was still serving them. But recovering them means knowing every filename, and a game ships no manifest.
Enumerating a game's assets from its bundle
The game is one bundled JS file, 1.76 MB. Every asset it loads is referenced somewhere inside it. So: fetch the bundle from production, and search it for string literals that look like asset paths.
const pat = /["']([A-Za-z0-9_\-.\/]+\.(?:png|jpg|webp|mp3|ogg|wav|json))["']/g;
const paths = [...new Set([...src.matchAll(pat)].map(m => m[1]))];
// 104 unique paths
Which raises the only question that matters: is 104 all of them?
The check that makes the list trustworthy
A regex finds literal strings. It cannot find a path the code builds at runtime. "assets/level-" + n + "/bg.png" is invisible to it, and a bundle full of those returns a confident, badly incomplete answer.
So search for the construction itself:
src.match(/`[^`]*assets\/[^`]*`/g) // template literals -> null
src.match(/assets\/[^"']*(?:\$\{|"\s*\+)/g) // concatenation -> null
Both empty. This bundle builds no paths dynamically, which is what upgrades the list from plausible to complete.
Then HEAD every path against production: 103 of 104 returned 200. The miss was file.json, a generic library string.
The check people skip
We also requested a filename we invented: assets/this-does-not-exist-xyzzy.png. It returned 404.
Without that, "103 files returned 200" means nothing. A host with a catch-all returns 200 for everything, and you end up congratulating yourself on a list of files that do not exist. A checker that cannot fail is not a checker.
Our own notes were wrong
Our docs said this game was "~205 MB". Measured across the 103 real files: 142.5 MB. Nobody had ever added it up - the figure was written once and quoted for months.
The same notes said the other self-hosted game needed "js.js, style.css, assets". It has no assets directory at all. Three files, and its 3D library comes from a CDN.
Verify the bundle, not the deploy
The failure mode is silent: a bad bundle deploys perfectly and simply contains less. So check before it goes anywhere.
- Every catalogue slug has a page - 200/200
- Zero directories outside the allowlist - the 111 stayed out
- Both self-hosted games complete
- Every required root file - favicons, logos, redirects, robots - 14/14
- Nothing shipped that is not a page (no build notes, no paste-in snippets)
- Every sitemap URL resolves to a real file in the bundle - 243/243
That last one is the most useful check we have. A sitemap listing URLs the bundle does not contain is a promise to a crawler that your deploy is about to break.
Three takeaways
- Build from an allowlist, never from the output folder. The output folder remembers pages you deleted on purpose.
- Find out what exists only on production. If a file is not in version control and not in your build, the deploy is the thing that will delete it.
- Test your verification against something that should fail. Every "all 103 files are fine" is worthless until you have watched the same check return 404 for a file you made up.
None of this makes a deploy exciting. It makes it boring, which is the goal.
Written while rebuilding the deploy bundle for SlowDen, a free browser-game site.
Top comments (0)