Test and troubleshoot
Test your graphic before you push it. npx ligr-graphic dev runs a local harness that needs no API key.
The viewer frames your graphic, sends the same message order as the LIGR overlay, and logs every message.
It needs no dashboard login. Preview controls change local test state; they do not publish or update a saved graphic.
The local harness
Section titled “The local harness”npm run devnpm run dev runs npx ligr-graphic dev in a scaffolded graphic. Open http://localhost:8080.
The harness sends HELLO on frame load, waits for READY, then sends LOAD_GRAPHIC,
and a PING every 10 seconds after that. That is the order and the cadence the
overlay uses. The panel next to the frame holds:
| Control | What it sends |
|---|---|
| Data → scenario picker | A GRAPHIC_UPDATE with the first step of the scenario |
| Previous, Next | A GRAPHIC_UPDATE with the sport data of that step. Step n is steps 0..n merged |
| Hide | GRAPHIC_HIDE, then a GRAPHIC_UPDATE with show: false. The log reports when HIDDEN arrives against exitDurationMs |
| Show | A GRAPHIC_UPDATE with show: true |
| Play, Pause | Advance scenario steps once per second, or stop playback |
| Controls | Edit declared control variables and send their current values |
| Data | Inspect the current sport data and scenario step |
| Files | Inspect the available local file names and sizes |
| Reset | Restore the first step, default controls, and visible state, then reload the frame |
| The log | Every message in both directions, with a warning for a seq gap and for a missing PONG |
With LIGR_API_KEY set, the harness loads the scenarios of the sport you pass with --sport and the
real JSON Schema of every surface from LIGR. Without a key it uses one bundled football scenario.
The scenarios are the ones the LIGR graphics builder uses. See
Schemas & scenarios.
With a start script in package.json, the harness runs it on the next free port and frames that server.
Any development server can use this path; it must serve runnable browser files on the supplied port. Without a
start script, the harness serves dist/ and reloads the frame when a file under dist/ changes.
npx ligr-graphic dev --port 8080 --sport footballWithout the CLI
Section titled “Without the CLI”Save this file as harness.html next to your dist/ folder. Serve the folder from any static
file server, then open the harness in a browser. It sends the same first messages as
npx ligr-graphic dev, with one sample payload and no scenarios.
python3 -m http.server 8080open http://localhost:8080/harness.html<!doctype html><html><head><meta charset="utf-8"><style> body { font: 14px monospace; margin: 0; display: flex; height: 100vh; } #stage { position: relative; width: 960px; height: 540px; background: #223; flex: none; } #stage iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; } #panel { flex: 1; display: flex; flex-direction: column; padding: 8px; } #log { flex: 1; overflow: auto; white-space: pre-wrap; background: #111; color: #9f9; padding: 8px; } button { margin: 0 4px 8px 0; }</style></head><body><div id="stage"><iframe id="frame" src="dist/index.html"></iframe></div><div id="panel"> <div> <button id="btnScore">Update score</button> <button id="btnHide">Hide</button> <button id="btnShow">Show</button> </div> <div id="log"></div></div><script> const PROTOCOL = 'ligr.gfx.v1' const frame = document.getElementById('frame') const log = document.getElementById('log') let sessionId = null, seq = 0, ready = false, pingTimer = null let homeScore = 0, awayScore = 0
const samplePayload = () => ({ graphicId: 'demo-graphic-1', show: true, sport: 'football', sportData: { '1': { score: homeScore, abbreviation: 'HOM', logoUrl: '', kit: { primaryColor: '#c00' } }, '2': { score: awayScore, abbreviation: 'AWY', logoUrl: '', kit: { primaryColor: '#00c' } }, clock: '12:00', lastClock: '12:00', clockRunning: true, periodShortName: 'First Half', }, controlVariables: { hide: false }, controlVariableData: { hide: { value: false } }, themeVariables: {}, externalData: {}, images: { player: [], team: [], competition: [] }, userExpressionValues: {}, })
function writeLog(dir, msg) { log.textContent += `${dir} ${msg.type} ${JSON.stringify(msg.payload)}\n` log.scrollTop = log.scrollHeight }
function send(type, payload) { const msg = { protocol: PROTOCOL, sessionId, seq: ++seq, type, payload } frame.contentWindow.postMessage(msg, '*') writeLog('→', msg) }
frame.addEventListener('load', () => { sessionId = `cg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` seq = 0 ready = false clearInterval(pingTimer) send('HELLO', { width: 1920, height: 1080 }) })
window.addEventListener('message', event => { const msg = event.data if (!msg || msg.protocol !== PROTOCOL || msg.sessionId !== sessionId) return writeLog('←', msg) if (msg.type === 'READY' && !ready) { ready = true send('LOAD_GRAPHIC', samplePayload()) pingTimer = setInterval(() => send('PING', {}), 10_000) } })
document.getElementById('btnScore').addEventListener('click', () => { homeScore += 1 send('GRAPHIC_UPDATE', { graphicId: 'demo-graphic-1', show: true, sport: 'football', sportData: samplePayload().sportData }) }) document.getElementById('btnHide').addEventListener('click', () => { send('GRAPHIC_HIDE', { graphicId: 'demo-graphic-1' }) }) document.getElementById('btnShow').addEventListener('click', () => { send('GRAPHIC_UPDATE', { graphicId: 'demo-graphic-1', show: true, sport: 'football' }) })</script></body></html>Empty schema objects are fine for a local test. The overlay sends full JSON Schema. For real sample
data, fetch GET /v2/scenarios/{sport} and paste one scenario into samplePayload.
Dashboard code viewer
Section titled “Dashboard code viewer”Open a code graphic from your theme to inspect it in the dashboard viewer. The dashboard viewer requires a session with access to the owning organization. It provides Controls, Data, and Files, plus scenario playback and Show, Hide, and Reset.
A working version uses the current saved draft. A numbered version loads that published snapshot and its pinned files. Preview controls only change the current preview. They do not edit the published version. Use Open working version when you need the draft after inspecting a published version.
Preview on an overlay
Section titled “Preview on an overlay”After you push and publish, open a control room of a competition that uses the theme. Your graphic appears in the graphic list with its control variables. Show it on the preview overlay of a test match before you use it on air.
Inspect the payload on an overlay
Section titled “Inspect the payload on an overlay”The overlay frames your graphic in a sandbox, so the browser cannot inspect the frame from the page.
Add ?debugCodeGraphics=1 to an overlay URL to see each data message that your graphic receives.
Use one of these overlay URLs from the control room of a test match:
| URL | Where to get it |
|---|---|
…/monitoring-{key}?debugCodeGraphics=1 | Copy monitoring link |
…/preview-{key}?debugCodeGraphics=1 | Preview Link in the control room header |
- Open the overlay URL with
?debugCodeGraphics=1in a desktop browser. - Open the browser console and show messages at the Verbose level.
- Find the
[CodeGraphicIframe] payloadline withtype: 'LOAD_GRAPHIC'. It holds the full payload of the load. - Paste this listener to print each later message as an object:
addEventListener('ligr:code-graphic-payload', e => console.log(e.detail))Each event has graphicId, type, seq, and payload. type is LOAD_GRAPHIC or GRAPHIC_UPDATE.
A GRAPHIC_UPDATE payload holds only the changed surfaces. seq is the same value that your graphic receives.
The overlay reads the flag when it loads a graphic. If you add the flag to an open overlay, reload the page. The flag changes no graphic output, and it sends no network request.
Checklist
Section titled “Checklist”Check every item before you push a version for a broadcast.
- The handshake,
HELLOthroughLOAD_GRAPHIC, completes within 2 seconds. - The graphic renders correctly from
LOAD_GRAPHICalone, with no earlierGRAPHIC_UPDATE. - The graphic updates correctly on a partial
GRAPHIC_UPDATE, for example a score change with no clock change. - The graphic sends
HIDDENwithinexitDurationMsofGRAPHIC_HIDE. - The browser console shows no error across a full show and hide cycle.
- The graphic makes no network call at runtime.
- The built
dist/folder is under 10 MiB, and no file is over 5 MiB. - The
<body>background stays transparent in every state. - The animation holds 60 frames per second during show and hide.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause | Fix |
|---|---|---|
| The frame stays blank | The entry file never sends READY, or a script error stopped it | Open the browser console. Check the harness log for a missing READY |
| The graphic never receives data | You echoed the wrong sessionId, or you never sent READY | Copy sessionId from HELLO exactly. Send READY before you expect LOAD_GRAPHIC |
| The graphic never hides | You never send HIDDEN, or exitDurationMs is shorter than your animation | Send HIDDEN when the out-animation ends. Raise exitDurationMs in the manifest |
| A message fails with a 1 MB error | A payload, usually externalData, is too large | Trim the data source. Load large assets from the bundle instead of the payload |
| Fonts do not load | A font path is absolute, or the font is missing from the bundle | Use relative paths. Ship the font file inside dist/ |
| The wrong team shows on the left | The home and away fields are swapped | sportData['1'] is the home team. sportData['2'] is the away team |
CODE_GRAPHIC_INVALID on push or publish | The manifest breaks a rule, or a file was not uploaded in the session you named | Read details[]. Each line is one problem. See The manifest |
409 LOCKED on push or publish | A person has the graphic open in the dashboard editor, or a stale lock has not expired | Read holder.userName. Wait up to 60 seconds, then retry |
404 on a theme you expect to reach | The API key belongs to another organization | Use a key of the organization that owns the theme |
| The frame is blank on the overlay but fine in the harness | The bundle did not upload, or the entry file is missing | GET …/code-graphics/{graphicId} lists assets. Check the entry file is there |
Report a problem
Section titled “Report a problem”Use Issues and support for code graphics, SDK, and CLI reports. General app problems, including broken pages, login, and billing, go to LIGR support.