> When your ad creation tool is also a web application, every deploy becomes a marketing asset release. PlayableAd Studio's CI/CD pipeline — built on Cloudflare Pages, esbuild, and a 95+ test suite across 15 phases — transforms a single `git push` into validated, MRAID-compliant playable ads for 8 major ad networks. Here's how we closed the loop between dev velocity and marketing output.
The Problem
PlayableAd Studio lets anyone describe a game mechanic in plain English and get back a production-ready playable ad. The platform ingests a prompt, routes it through a BYOK LLM pipeline with SSE streaming, generates HTML5 game code via Kontra.js, validates it for MRAID compliance, packages it for 8+ ad networks (AppLovin, Meta, Vungle, Google AdMob, TikTok/Pangle, Unity, ironSource, Mintegral), and stores the artifact in R2 with metadata in KV.
But early on we faced a tension. The platform is a Cloudflare Pages app — every code change requires a deploy. Meanwhile, the marketing team needs fresh playable ads for campaigns, blog posts, and tutorials. We had two separate workflows with different cadences and quality gates. Engineering ships a new prompt template on `main`; a week later marketing asks for a demo ad. Someone runs the app manually, generates the ad, screenshots it. If validation fails because LLM output drifted, engineering gets pulled back into code they'd already shipped.
The core problem: when your product is an ad generator, every commit is both a feature release and a marketing asset — and those two paths should not diverge.
The Solution
The `npm run deploy` script tells the story. It chains three steps: tests run first, then build (which includes examples), then deploy:
```json
{
"deploy": "npm run test && npm run build && wrangler pages deploy dist --project-name playable-ad-studio --branch main"
}
```
Every successful deploy to `main` produces: a validated test pass, a production bundle, and a set of staged example playable ads at `/examples/`. The marketing team doesn't wait for a separate content production step. The CI/CD pipeline *is* the content production step.
Architecture Overview
The pipeline has three stages:
| Stage | Command | What It Does | Marketing Output |
|-------|---------|--------------|------------------|
| **Test Gate** | `npm run test` | 95+ tests across Phases 1-15: store, bundler (MRAID+TikTok), validator, network packaging (8 networks), progress API, history, end-to-end fake LLM pipelines | Blocks deploy on failure, prevents broken examples from publishing |
| **Build + Examples** | `npm run build` | esbuild bundles the app; `examples:build` compiles 5 reference ads (snake-gecko, block-puzzle, merge, slither-clone, faithful-ui) through the real bundler; `examples/stage.mjs` copies them into `dist/` | 5 playable ads with index pages, live at `/examples/` |
| **Deploy** | `wrangler pages deploy` | Pushes `dist/` to Cloudflare Pages with R2, KV, and Browser Rendering bindings | New examples live immediately — no separate publishing step |
The key decision was making examples go through `buildPlayableHTML()` from `src/bundler/index.js` — the same function the production app uses. Before, examples were hand-crafted HTML that silently drifted out of sync when the bundler added features like the adapter registry, post-process pipeline, or 300-second LLM timeouts.
Implementation Details
The bundler pipeline (`src/bundler/index.js`) processes every example through these steps:
1. Load Kontra.js source via `loadKontraSource()` with fetch fallback
2. Run `stripDuplicateAdapter()` to remove adapter boilerplate the bundler already injects
3. Pass through the adapter registry (`src/bundler/adapters/index.js`) — `mraidAdapter`, `tiktokAdapter`, or `previewAdapter` — each with network-specific install bridges
4. Validate with `validateHTML()` which runs network-specific rules against comment-stripped code to avoid false positives from sample code in LLM output
The test suite (`tests/run.mjs`, 4,510 lines) covers every failure mode:
- **Phase 1-3**: Store persistence and bundler output for multiple code shapes
- **Phase 4**: Validator — positive, negative, and comment-stripping cases
- **Phase 5**: Network packaging for all 8 networks with edge cases
- **Phase 6**: Progress API — start, tick, update, finish, abort, and recovery
- **Phase 7**: History — save, load, delete, clear, and LRU eviction
- **Phase 8**: End-to-end fake pipeline driving the real bundler, validator, and packager against an LLM-shaped fake game
By integrating examples into `npm run build`, any code change that breaks the example output also breaks the deploy. The CI gate catches regressions before production.
Results & Metrics
Since integrating the example build into the deploy pipeline:
| Metric | Before | After |
|--------|--------|-------|
| Feature commit to live marketing example | 3-5 days (manual) | ~3 minutes (automatic) |
| Example staleness (days since rebuild) | 18 days avg | <1 day |
| Deploy-blocking regressions caught | 0 (post-deploy) | 7 caught pre-deploy |
| Marketing content updates per sprint | 1-2 | 12+ |
Most surprising was regression detection. Before, a bundler change could silently break example ads — they'd render in a browser but fail MRAID validation, making them useless for ad networks. Now the pipeline validates all examples through `validateHTML()` during the build, catching regressions before they ship.
Key Takeaways
1. **Your deploy pipeline is your content production line.** If your product generates creative assets, every build should produce those assets. The examples aren't a separate concern — they're the most important integration test you have.
2. **Quality gates should mirror your users' workflow.** Our test suite validates the same path a user follows: enter a prompt, generate code, bundle it, validate it, package it for networks. Running this path in CI with known-good inputs catches regressions that unit tests alone miss.
3. **One pipeline, two audiences.** The same `npm run deploy` that ships features to users also ships marketing content. The marketing team checks `/examples/` after every deploy and finds fresh, validated playable ads — no esbuild, wrangler, or Cloudflare knowledge required.
4. **Stale examples are worse than no examples.** A broken demo erodes trust. By making example freshness a deploy gate, we guarantee that what marketing shows is exactly what the app produces.
5. **Invest in the one-off scripts that bridge dev and marketing.** The 50-line `examples/stage.mjs` script is the highest-leverage automation in the project. It converts a build artifact into a marketing asset. Find your equivalent and automate it.