What an HTML5 playable actually is, technically
Strip away the marketing and a playable ad is a self-contained document that a mobile app's ad SDK loads into an embedded WebView — no network access assumed, because the device may be offline or the network may sandbox it. That constraint drives everything about the format:
- No runtime external dependencies. Every image, sprite sheet, audio clip, font, and script has to already be inside the file (or ZIP) the network serves — not fetched from a CDN at play time.
- A single DOM/JS context. There's one document, one script scope. Whatever engine produced the content (Unity's WebGL/HTML5 export pipeline, in this case) has to run inside that one context via its own bundled JS, canvas, and asset-loading code — not as a native binary.
- A defined entry point. The ad SDK needs to know exactly which file to load first. Every network in practice enforces this by requiring a specific filename or a fixed folder layout, not just "an HTML file somewhere in the ZIP."
- A network-recognized click signal. Because the WebView is sandboxed, a plain
<a href>orwindow.locationredirect usually does nothing useful — the SDK has to intercept the click through its own API.
A Playworks/Luna export satisfies the first two points by default — it's already HTML5, already self-contained inside its own ZIP. Where it falls short is the last two: it's built to one shape, and every network defines that shape differently.
Why a single Unity export needs reshaping per network
Playworks/Luna generates one build for one target. Ad networks don't share a target. Looking at the actual requirements networks publish, the differences aren't stylistic — they're structural:
| Network | Package shape | Entry file | External requests |
|---|---|---|---|
| Google Ads | ZIP, ≤5 MB, ≤512 files | index.html | Only Google-approved hosted libraries (ExitApi, Fonts, jQuery/CreateJS/Greensock) |
| AppLovin | Single HTML file, ≤5 MB | applovin.html | Prohibited — everything inlined |
| Meta | Single HTML file, ≤5 MB (2 MB recommended) | meta.html | Prohibited — everything inlined |
| Unity Ads | Single HTML file, ≤5 MB | unity.html | Prohibited — everything inlined |
| Vungle | Single HTML file, ≤5 MB | vungle.html | Prohibited — everything inlined |
| Mintegral | ZIP, ≤5 MB, archive/folder/HTML names must match | mintegral/mintegral.html | Local ZIP resources allowed |
| TikTok | ZIP, ≤5 MB, first-level config.json required | index.html (first-level) | Prohibited — needs Playable-sdk.js instead |
Notice the split: Google, Mintegral, and TikTok want a ZIP with a specific internal layout; AppLovin, Meta, Unity Ads, and Vungle want a single monolithic HTML file with nothing external at all — not even a relative-path image reference. A Playworks/Luna export, being built for one destination, is naturally one or the other. Getting it correct for the other shape means restructuring the package, not just re-zipping it.
What actually changes during HTML5 conversion
Asset inlining: from file references to base64/data URIs
For AppLovin, Meta, Unity Ads, and Vungle, every image, audio clip, and font referenced by <img src="...">, url(...), or an Audio() call has to be rewritten as an embedded data URI (data:image/png;base64,...) or an equivalent encoded blob decoded by JS at load time, directly inside the one HTML file. There is no folder structure left for the browser to resolve — because prohibitsExternalRequests means there's no "load" step happening outside the document at all. For Google and Mintegral, assets can stay as local relative-path files inside the ZIP instead, since those networks explicitly allow ZIP resources — so the conversion there is closer to careful bundling than full inlining.
Click-through wiring: clickTag, MRAID, and network-native SDKs
This is the part that varies the most and breaks the most creatives when it's wrong. AppLovin and Unity Ads both require MRAID 2.0 and expect the CTA to call mraid.open(url), but only after the SDK signals MRAID is ready — calling it earlier, or using it against a network that doesn't expect MRAID at all, causes the click to silently fail. Vungle's primary path is window.parent.postMessage('download','*'), with MRAID open only as a fallback. Mintegral expects window.install(), again falling back to mraid.open(url), plus a separate window.gameEnd() callback networks test independently. TikTok explicitly prohibits the MRAID pattern and instead requires its own Playable-sdk.js and a window.openAppStore() call. Meta requires FbPlayableAd.onCTAClick() and explicitly disallows auto-redirects or window.open. Google uses its own ExitApi.exit() loaded from a Google-hosted script. None of this is one clickTag variable dropped into every build — it's six or seven distinct call conventions, several of which actively conflict with each other (MRAID vs. explicitly-prohibited MRAID, for instance).
Structural enforcement: DOCTYPE, orientation, and entry naming
Google specifically requires a proper DOCTYPE, an <html>/<body> structure, and an orientation meta tag on the entry file — a raw Unity WebGL export doesn't always emit these in the form Google's scanner expects. TikTok instead wants orientation declared in a first-level config.json rather than in the HTML at all. AppLovin requires the build to support both device orientations. Mintegral requires the ZIP's archive name, top-level folder name, and HTML filename to all match — a naming constraint that has nothing to do with functionality and everything to do with how Mintegral's ingestion pipeline unpacks the file.
Size compliance
Every network caps creatives at 5 MB, with Meta and Unity Ads specifically recommending 2 MB. Inlining assets as base64 inflates their size by roughly a third over the raw binary, so a build that fits comfortably as a ZIP with linked files can blow past the cap once it's converted to a single self-contained file for AppLovin or Meta. Compression has to happen alongside inlining, not before it — see playable ad compression for what that involves.
How PlayableKit automates this
PlayableKit takes one Playworks/Luna export and applies the network-specific transformation described above per target: inlining or bundling assets according to whether the network allows ZIP resources or requires a monolithic file, writing the correct click-through call (ExitApi, MRAID, SDK-native, or postMessage) with the readiness checks each one needs, enforcing entry-file naming and folder/archive matching, injecting orientation declarations in whichever form the network expects (meta tag vs. config.json), and compressing to stay under each network's byte cap. The output is a separate, correctly shaped package per network from the same source build — not one build hopefully-compatible with all of them.