+ Publish
GUIDE

Get your game running on Pogglo

Pogglo runs your game as plain static files in a secure sandbox — no servers to set up, nothing to configure. The easy path: copy the prompt below into your AI and it takes care of all of it for you. The rest of this page just explains what that prompt handles, in case you’re curious.

9 chapters · optional read
The short version: copy the prompt and paste it into your AI (Claude, Cursor, v0…). It bakes in every rule on this page, so your game works on the first try. Everything below is the why — read on only if you want to see what’s happening under the hood.

01 —How Pogglo runs your game

  • Fully static, no backend. Anything needing a server, database, or AI call won’t work — all logic runs in the browser.
  • It runs sandboxed. For safety your game is walled off from the platform — it can’t see Pogglo’s login, page, or storage. You don’t manage any of that; to save a player’s progress, use pogglo.save (see the Saves section).
  • Each game is mounted at /play/<your-game>/ — which is why paths must be relative, never ../ or a leading /.

One principle throughout: every platform signal is opt-in. The language param, hot-switch message, keyboard focus, rotation — a game that ignores all of them still runs, it just misses that one nicety. The only hard requirement is Packaging.

02 —LanguageRecommended

The platform appends the current language to your game URL as ?lang=xx. Because the sandbox can’t read platform state, this is your only reliable source. Players pick a language on the platform before entering — you read ?lang= and you’re already following it.

  • Support these 8 languages for player-facing text: en zh es pt ja ko fr de. Use a dictionary — don’t hardcode strings.
  • On startup, read the initial language once; fall back to your own default if absent or unknown — never throw.
  • Don’t read the platform language from cookie/localStorage (always empty in the sandbox). The only source is ?lang=.
Read the initial language · required
const S = ['en','zh','es','pt','ja','ko','fr','de'];
const q = new URLSearchParams(location.search).get('lang');
const nav = (navigator.languages || [navigator.language || ''])
  .map(s => s.toLowerCase().split('-')[0]);
const lang = S.includes(q) ? q : (nav.find(x => S.includes(x)) || 'en');
// initialize your UI in `lang`

03 —PackagingRequired

These are the must-haves for the zip you upload. If something’s off, it comes straight back to you with a plain, paste-to-your-AI fix — nothing is lost, just adjust and re-upload.

  1. Put index.html at the root of the zip. One game per zip — don’t nest folders or bundle multiple games (Pogglo picks the shallowest index.html).
  2. Keep every asset in the same folder, relative paths only. No ../ and no leading /. E_ASSET_PATH
  3. Every local file index.html references must be in the zip. E_ASSET_MISSING
  4. Build first: no .tsx/.ts/.jsx source or /src/ entry; no node_modules/. E_UNBUILT E_MISSING_DEPS
  5. Third-party libs — either copy the lib into the zip (simplest) or add a <script type="importmap">. A bare import 'x' with no importmap won’t resolve. E_BARE_IMPORT
  6. No secrets in the code (scanned, and public once live). E_SECRET
  7. Zip flat, unencrypted, forward-slash paths, ≤100MB.

Your game may freely fetch from other sites / CDNs (leaderboards, fonts, images) — bundling everything in still keeps it simplest and fully offline.

04 —Runtime

These won’t block your upload, but skip them and things quietly go wrong once people play — best to build them in from the start.

  • Persisting progress? Use pogglo.save — it stores one blob per player, device-local for guests and cloud-synced once they log in. The Saves section below has the API.
  • No blocking dialogs. alert() / confirm() / prompt() freeze the sandbox — use in-page UI.
  • Fill the iframe and be responsive. Add <meta name="viewport" content="width=device-width, initial-scale=1"> (without it phones render at desktop width). Use 100% / 100vw / 100dvh and recompute your canvas on resize. The container can briefly be 0-size on first paint — fix it on resize, not only on load (else the canvas stays 0×0 forever).
  • Support keyboard and touch. Don’t require Enter to start — tap / click must start it too (phones have no Enter key).
  • Gate audio. Start it only after the first tap / click / key; handle a suspended AudioContext.

05 —Scores & leaderboards

The platform injects a global pogglo object into your game once it is live on Pogglo — no script tag, no import, no key, no setup. Report a score in one line and it is on the board; identity and anti-cheat are handled for you.

  • Report a score: pogglo.submitScore(value) — it goes to the default score board (created on first use). For more than one board, name it via the rank namespace: pogglo.rank.submit('level', value).
  • Runs anywhere: the object only exists on Pogglo, so paste the one-line no-op stub (below) at the top of your game for local dev. It covers scores, ads, and anything the platform adds later — paste once, never maintain it.
  • Show your own board: const top = await pogglo.getLeaderboard(); for a named board, await pogglo.rank.top('level'). Both give [{ rank, name, value }]. Don’t send player names or trust client scores — the platform ranks logged-in players by account, guests by device.
Add a leaderboard in one line
// ONE block at the top of your game. The real `pogglo` is injected by the platform
// once you're live (as a READ-ONLY global); this installs a safe no-op stub ONLY when it's
// absent (local dev). Never assign to pogglo directly — "pogglo = ..." throws on-platform
// (Cannot assign to read only property). It covers every current AND future capability
// (scores, ads, …), so you paste it once and never touch it again — no import, no key, no crash.
if (typeof globalThis.pogglo === 'undefined') {
  try { globalThis.pogglo = new Proxy(function(){}, { get:(t,k)=>k==='then'?undefined:globalThis.pogglo, apply:()=>Promise.resolve([]) }); } catch (e) {}
}

// Then just call it — clean, no guards, no redundant board name:

// Common case — report to the default 'score' board and read it back:
pogglo.submitScore(points);
const top = await pogglo.getLeaderboard(); // [{ rank, name, value }]

// More than one board? The rank namespace reads clean — submit / top as a pair:
pogglo.rank.submit('level', level);
const levels = await pogglo.rank.top('level'); // [{ rank, name, value }]

06 —Saves

Persist a player’s progress with one call — pogglo.save. Pogglo stores it per game and brings it back automatically: it survives reload and new sessions on the device, and syncs across the player’s devices once they log in (guests stay device-local). No storage code, no setup.

  • Save — pogglo.save.set(state). Pass one object holding your whole game state; each call overwrites the previous save (last write wins). Call it whenever progress changes.
  • Restore — const save = await pogglo.save.get(). Read it once at startup; it’s null the first time a player plays. Wipe a save with pogglo.save.clear().
  • Always validate what you load — a save can be empty, or from an older version of your game. Fall back to sensible defaults and never throw on a missing or unexpected value.
  • Keep it one compact blob — well under ~2 KB loads instantly; 1 MB is the hard cap. Like scores, pogglo.save only exists on Pogglo, so the one-line stub from the previous section keeps these calls a safe no-op while you develop locally.
Save & restore with pogglo.save
// Persist the player's progress with pogglo.save. Pogglo stores it per game
// (device-local, and cloud-synced across the player's devices once they log in)
// and brings it back automatically. pogglo.save only exists on Pogglo, so keep the
// one-line stub from the Scores section — these calls stay a harmless no-op locally.

// Save — pass your whole game state as ONE object; each call overwrites the last save:
pogglo.save.set({ level, coins, unlocked });

// Restore — read once at startup; null the first time this player plays:
const save = await pogglo.save.get();

// Always validate + fall back to defaults (a save can be empty or from an older version):
const state = (save && Number.isFinite(save.level))
  ? save
  : { level: 1, coins: 0, unlocked: [] };

// pogglo.save.clear();   // wipe the save

07 —Optional extras

Only when you need them — skipping them keeps the game fully working.

  • Hot-switch language without reloading (when the page has a switcher): announce on load, then listen for changes.
Language hot-switch · optional
// announce support on load
parent.postMessage({ type: 'pogglo:hotlang' }, '*');
// hot-switch when the platform changes language
addEventListener('message', e => {
  if (e.data?.type === 'pogglo:setLang' && S.includes(e.data.lang)) setLang(e.data.lang);
});
  • Threads / SharedArrayBuffer. Only some engines need this (WASM threads, emulators). If yours does, add <meta name="pogglo:coi" content="1"> to index.html. Not sure? You don’t need it.
  • Orientation. landscape fills the player — use it for landscape games and any game that adapts to the window (most games). portrait locks the player to 9:16 on every device, so desktop shows black bars — only pick it for games that must stay phone-shaped.
  • Title. Set <title> to the game name (tab title and fallback name).

08 —One-shot compliance prompt

This is the whole guide, packed for your AI. Paste it into Claude, Cursor, v0… and whatever it builds is ready to publish from the first line — no need to read anything above.

The one prompt you need
Paste into your AI tool — every rule on this page is already baked in.
[Pogglo game compliance — follow this when generating or adapting a game]
Your game is hosted on Pogglo (fully static, no backend, cross-origin sandboxed iframe, mounted at /play/<slug>/). Meet all of this from the start:

A. Packaging (or the upload is rejected)
1. Put index.html at the ROOT of the zip. One game per zip — don't nest folders or bundle multiple games (Pogglo picks the shallowest index.html; multiple entries get picked wrong).
2. Keep every asset in the SAME folder as index.html and use relative paths only. No "../" (escapes the root) and no leading "/" (absolute). [E_ASSET_PATH]
3. Every local file index.html references must be inside the zip. [E_ASSET_MISSING]
4. Build first: don't ship .tsx/.ts/.jsx source or a /src/ entry, and don't reference node_modules/. [E_UNBUILT / E_MISSING_DEPS]
5. For third-party libs pick one: (a) copy the lib into the zip and reference it relatively (simplest); (b) add a <script type="importmap">. A bare import 'x' with no importmap is rejected. [E_BARE_IMPORT]
6. No secrets in the code (they're scanned, and public once live). Anything needing a server/AI won't work (no backend).
7. Zip must be flat, unencrypted, forward-slash paths, <=100MB.

B. Runtime (breaks silently if ignored)
8. Saves — persist a player's progress with pogglo.save (the platform's save API); do NOT roll your own. Pogglo stores one blob per game — device-local for guests, cloud-synced across a logged-in player's devices — and restores it automatically. Requires the section-D one-line stub (which also makes it a safe no-op during local dev).
   - Save: pogglo.save.set(state) — pass ONE object holding your whole game state; each call OVERWRITES the previous save (last write wins). Call it whenever progress changes.
   - Restore: const save = await pogglo.save.get() — read once at startup; it's null the first time this player plays. pogglo.save.clear() wipes it.
   - Always validate what you load (it may be empty or from an older version of your game) and fall back to sensible defaults — never throw. Keep it ONE compact blob: well under ~2KB loads instantly, 1MB is the hard cap.
   - A save is per-player progress, not shared: for a global or cross-player high-score leaderboard use scores (section D), not a save.
9. No blocking dialogs: alert()/confirm()/prompt() freeze the sandbox. Use in-page UI.
10. Include <meta name="viewport" content="width=device-width, initial-scale=1"> in index.html (without it phones render at desktop width and 100vw is wrong), then fill the iframe responsively: use 100%/100vw/100dvh, recompute your canvas on resize, don't hardcode pixels. The container can briefly be 0-size on first paint — fix it on resize, not only on load (else the canvas stays 0x0 forever).
11. Support keyboard AND touch; don't require Enter to start — tap/click must start it too (phones have no Enter key).
12. Start audio only after the first tap/click/key (autoplay policy); handle a suspended AudioContext.

C. Language (required — follow the platform language)
13. Support these 8 languages for player-facing text: en zh es pt ja ko fr de (use a dictionary, don't hardcode strings).
14. On startup read the initial language once; fall back to your own default if absent/unknown — never throw:
    const S=['en','zh','es','pt','ja','ko','fr','de'];
    const q=new URLSearchParams(location.search).get('lang');
    const nav=(navigator.languages||[navigator.language||'']).map(s=>s.toLowerCase().split('-')[0]);
    const lang = S.includes(q) ? q : (nav.find(x=>S.includes(x)) || 'en');
15. Don't read the platform language from cookie/localStorage (unreadable in the cross-origin sandbox). The only source is ?lang=.

D. Leaderboards & scores (optional — one block, zero setup)
The platform injects a global "pogglo" object once your game is live on Pogglo, as a READ-ONLY property. It does NOT exist when you run the game locally. Add this block at the very top of your game — it installs a safe no-op stub ONLY when pogglo is absent (local dev). CRITICAL: never assign to pogglo directly (do NOT write "globalThis.pogglo = pogglo || ...", "window.pogglo = ..." — assigning to it throws "Cannot assign to read only property 'pogglo'" on-platform and kills your whole script). Guard on existence + try/catch. Then call pogglo.* freely (never a ReferenceError, never a crash, on-platform it's the real thing):
    if (typeof globalThis.pogglo === 'undefined') { try { globalThis.pogglo = new Proxy(function(){}, { get:(t,k)=>k==='then'?undefined:globalThis.pogglo, apply:()=>Promise.resolve([]) }); } catch (e) {} }
16. Report a score whenever the player earns one: pogglo.submitScore(value) — goes to the default 'score' board, created automatically on first use. For more than one board, name it via the rank namespace: pogglo.rank.submit('level', value).
17. Read the top entries to show your own board: const top = await pogglo.getLeaderboard(); for a named board use the rank namespace: await pogglo.rank.top('level'). Both give [{ rank, name, value }].
18. Do NOT send player names or trust client scores yourself — identity (logged-in players by account, guests by device) and anti-cheat are handled by the platform. Just report the number.

E. Optional platform signals (works fine if you skip it)
19. Hot-switch language without reloading (when the page offers a switcher): on load parent.postMessage({type:'pogglo:hotlang'},'*');
    then addEventListener('message',e=>{ if(e.data?.type==='pogglo:setLang'&&S.includes(e.data.lang)) setLang(e.data.lang) }).
20. Need SharedArrayBuffer/threads: add <meta name="pogglo:coi" content="1"> to index.html; otherwise don't.
21. Portrait games: declare portrait at publish (Pogglo letterboxes to 9:16). Set <title> to the game name.

Principle: every platform signal (?lang=, pogglo:setLang, pogglo.submitScore, keyboard focus, rotation) is opt-in — a game that ignores all of them still runs. The only hard requirement is section A (Packaging).

09 —Error codes & fixes

If an upload needs a change, you’ll get one of these back — each with a one-line fix you can hand to your AI.

CodeMeansFix
E_ASSET_PATHReferences a file outside the folder (../ or absolute /)Put it in the same folder; use ./x
E_ASSET_MISSINGA referenced local script isn’t in the zipInclude it, or fix the path
E_UNBUILTUncompiled source (.tsx/.ts/.jsx or /src/)Build first; publish the output folder
E_MISSING_DEPSReferences node_modules/Bundle it, or copy libs into vendor/
E_BARE_IMPORTBare import 'x', no importmapAdd an importmap, or a local lib file
E_SECRETA secret / API key in the codeRemove it
E_NO_INDEXNo index.html in the zipZip the web build folder
E_TOO_BIGOver 100MBCompress textures / audio

Every platform signal is backward-compatible: not adapting never stops your game from running. Questions: txqy0831@gmail.com · Publish a game →