Release checklist
Run these on the build folder (dist/) before zipping it. Each item says why, so a game with an unusual
build can apply the reason. The bundle is served from https://<slug>.<play domain>/<slug>/<version>/
through the CDN and from the preview host under a cookie scoped to that path; both carry the CSP of the
lifecycle page.
Structure
Section titled “Structure”index.htmlsits at the root of the folder. The upload refuses a zip without it at the root, and a zip of the folder itself puts it one level down: zip the content.- No
..in a path, no symbolic link, no encrypted entry, no file named_upload.zip. The upload refuses them. - Zip at most 200 MB, at most 500 MB unpacked, at most 10,000 files, at most 64 MB per file. A few MB loads fast on a phone.
- No source maps unless you want them public.
cd dist && zip -r ../gloop-1.4.0.zip . -x '*.map' && cd ..unzip -l gloop-1.4.0.zip | head -5 # index.html first, at the rootdu -sh dist && find dist -type f | wc -lcd dist && zip -r ../gloop-1.4.0.zip . -x '*.map' && cd ..unzip -l gloop-1.4.0.zip | head -5 # index.html first, at the rootdu -sh dist && find dist -type f | wc -l- Every URL in HTML, CSS and JS is relative:
assets/x.png,./main.js. Vite needsbase: './'. - No URL starts with
/. On the CDN it leaves/<slug>/<version>/; in the preview it also loses the cookie and answers 404. - Code that builds URLs at run time uses
new URL('assets/x.png', document.baseURI)orimport.meta.url, neverlocation.origin + '/...'. - Workers and WASM load from relative paths too:
new Worker(new URL('./w.js', import.meta.url), { type: 'module' }).
grep -rnoE '(src|href)="/[^/]' dist --include='*.html'grep -rnoE "url\(/[^/]" dist --include='*.css'grep -rnoE "[\"'\`]/(assets|src|img|audio|data)/" dist --include='*.js' | headgrep -rnoE '(src|href)="/[^/]' dist --include='*.html'grep -rnoE "url\(/[^/]" dist --include='*.css'grep -rnoE "[\"'\`]/(assets|src|img|audio|data)/" dist --include='*.js' | headScripts and the CSP
Section titled “Scripts and the CSP”- No
<script src="https://...">: the CSP blocks it. The SDK loads the CrazyGames, Poki and Playgama scripts on their own portal; remove the tags the old build had. - No inline
<script>with code, noonclick=""attribute, noeval, nonew Function, nosetTimeoutwith a string. The policy has nounsafe-inlineorunsafe-evalfor scripts: they throw. - No
fetch,XMLHttpRequest,WebSocketorEventSourceto a host other than the bundle’s own and the API. Analytics and error reporting go throughsdk.track(). - Fonts, images, audio and data files are inside the bundle. No Google Fonts link, no CDN library.
- The SDK is either bundled (ESM) or loaded by a relative script tag,
vendor/filudi-sdk-<version>.iife.js, before the game’s script. sdk-trace.jsand any other playground helper are gone.
grep -rnE "<script[^>]+src=.https?:" dist --include='*.html'grep -rnE '<script>[^<]|<script type="module">[^<]| on[a-z]+="' dist --include='*.html'grep -rnE '\beval\(|new Function\(' dist --include='*.js' | grep -v filudi-sdk | headgrep -rnE 'fonts.googleapis|cdn\.|unpkg|jsdelivr|sdk-trace' dist | headgrep -rnE "<script[^>]+src=.https?:" dist --include='*.html'grep -rnE '<script>[^<]|<script type="module">[^<]| on[a-z]+="' dist --include='*.html'grep -rnE '\beval\(|new Function\(' dist --include='*.js' | grep -v filudi-sdk | headgrep -rnE 'fonts.googleapis|cdn\.|unpkg|jsdelivr|sdk-trace' dist | headFrame and orientation
Section titled “Frame and orientation”- The game fills its frame: it reads
innerWidthandinnerHeightor aResizeObserver, notscreen.width, and redraws onresize. - The orientation declared on the game (
portrait,landscapeorany) matches what the layout handles. mobileandminutesare set on the game.- The game does not draw its own fullscreen button on CrazyGames (
result.host === 'crazygames'); elsewhere a button callssdk.fullscreen(). - Audio starts after a user gesture.
- A click in the game gives it the keyboard: a
pointerdownhandler that callspreventDefault()also callscanvas.focus(), withtabindex="0"on the canvas. Check it: click outside the game, click in the game, press an arrow key.
SDK use
Section titled “SDK use”sdk.init({ gameId, version }):gameIdis the game’s slug in the console,versionthe version you upload.sdk.ready()after the first frame, once.- Gameplay brackets around real play only; mute on
ads.on('start'); reward oncompletedonly. - Every board passed to
leaderboard.submit()is declared in the console with its bounds. - The CrazyGames leaderboard key comes from a build variable and is not in the repository.
- The game ran on each host of the playground with “Production CSP” on and no
CSP violationline.
Upload the zip, preview it, request its review: your first bundle. Tag the game’s repository with the version you uploaded, so a rollback (activating the previous version again) has a source to match.