@shotstack/cli 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/shotstack.js +15 -15
- package/package.json +2 -4
- package/{SKILL.md → skills/shotstack/SKILL.md} +1 -1
- package/{references → skills/shotstack/references}/html5-snippets.md +13 -11
- package/{references → skills/shotstack/references}/html5.md +7 -4
- package/{references → skills/shotstack/references}/motion.md +2 -2
- package/{references → skills/shotstack/references}/troubleshooting.md +26 -1
- package/{shared → skills/shotstack/shared}/agent-core.md +48 -3
- /package/{references → skills/shotstack/references}/asset-library.md +0 -0
- /package/{references → skills/shotstack/references}/caption.md +0 -0
- /package/{references → skills/shotstack/references}/fonts.md +0 -0
- /package/{references → skills/shotstack/references}/ingest.md +0 -0
- /package/{references → skills/shotstack/references}/positioning.md +0 -0
- /package/{references → skills/shotstack/references}/svg.md +0 -0
- /package/{references → skills/shotstack/references}/timeline.md +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shotstack/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Command-line interface for the Shotstack video rendering API.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/shotstack/shotstack-cli",
|
|
@@ -17,9 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist/shotstack.js",
|
|
20
|
-
"
|
|
21
|
-
"shared",
|
|
22
|
-
"references"
|
|
20
|
+
"skills"
|
|
23
21
|
],
|
|
24
22
|
"scripts": {
|
|
25
23
|
"start": "bun run src/index.ts",
|
|
@@ -158,7 +158,7 @@ This skill ships sub-references for the gnarly bits:
|
|
|
158
158
|
- [`references/svg.md`](references/svg.md) — required attrs, supported elements
|
|
159
159
|
- [`references/motion.md`](references/motion.md) — **the house motion language**: one duration scale, one ease, one stagger; choreography recipes (GSAP/CSS), the rich-text/transition mappings, and the brand kit. Read before composing any animation.
|
|
160
160
|
- [`references/html5.md`](references/html5.md) — HTML5 asset: fields, preloaded libs (gsap/d3/anime/lottie), browser harness, sizing, worked examples
|
|
161
|
-
- [`references/html5-snippets.md`](references/html5-snippets.md) — copy-paste motion-graphic clips: kinetic headline,
|
|
161
|
+
- [`references/html5-snippets.md`](references/html5-snippets.md) — copy-paste motion-graphic clips: kinetic headline, value reveal, shine sweep, pulsing CTA, film grain
|
|
162
162
|
- [`references/fonts.md`](references/fonts.md) — built-in fonts, Google Fonts URL pattern, custom-font workflow
|
|
163
163
|
- [`references/asset-library.md`](references/asset-library.md) — placeholder videos, images, music
|
|
164
164
|
- [`references/troubleshooting.md`](references/troubleshooting.md) — common errors and fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# HTML5 snippet pack — drop-in motion graphics
|
|
2
2
|
|
|
3
|
-
Copy-paste `html5` clips that "pop" — kinetic type,
|
|
3
|
+
Copy-paste `html5` clips that "pop" — kinetic type, value reveals, shine sweeps,
|
|
4
4
|
pulsing CTAs, grain. Each is a **single clip**: paste it into a track's
|
|
5
5
|
`clips[]`, set `start`/`length`, and position with `offset`.
|
|
6
6
|
|
|
@@ -14,12 +14,14 @@ easings and durations. Read [`motion.md`](motion.md) for the why and the full re
|
|
|
14
14
|
|
|
15
15
|
Read [`html5.md`](html5.md) for the rules these obey. The non-negotiables:
|
|
16
16
|
|
|
17
|
-
- **Seekable animation only.** GSAP timelines, GSAP tweens
|
|
17
|
+
- **Seekable animation only.** GSAP timelines, GSAP tweens, anime.js, Lottie, or CSS `@keyframes`. **Never** `setTimeout`/`setInterval`/`requestAnimationFrame`/`Date.now()`/`gsap.call()` — the renderer seeks by absolute time, it doesn't play.
|
|
18
|
+
- **`onUpdate` does not fire under seek.** GSAP `onUpdate` callbacks are **not** invoked when the harness seeks to a frame, so any DOM mutations made inside them (`textContent`, `innerHTML`, class swaps) won't appear in the rendered video. Animate **CSS properties only** (opacity, transform, filter, scale). Bake final values into the HTML at generation time and reveal them with opacity/transform tweens.
|
|
18
19
|
- **Size the clip to the content, not the canvas.** `html, body` pinned to the clip's `width`/`height`; place with `offset` (`{x:0,y:0}` is centred, `y` positive is up). Use **px**, never `vw`/`vh`/`%`.
|
|
19
20
|
- **The body is transparent by default** — the clip composites over the layers below (only set an opaque background if you want one).
|
|
20
21
|
- **No `<canvas>`** — Studio capture serialises the DOM; canvas bitmaps come through empty. Use SVG or positioned DOM.
|
|
21
22
|
- **No network.** gsap/anime/d3/lottie are preloaded; external `<script src>`, `fetch`, remote `<img>`, and remote fonts are all CSP-blocked — inline everything as `data:` URIs.
|
|
22
23
|
- **Fonts:** `timeline.fonts[]` does **not** reach `html5` and remote `@font-face` is blocked. These snippets name a display font first but render in the `system-ui` fallback unless you inline the font as a `data:` `@font-face`.
|
|
24
|
+
- **Use unique element IDs for GSAP targets, not `:nth-child` or compound CSS selectors.** Studio preview doesn't resolve `.parent:nth-child(n) .child` patterns; `#id` works everywhere. When animating repeated elements (list rows, bars, cards), generate a unique ID per item (`id="row0"`, `id="bar0"`) rather than relying on structural selectors.
|
|
23
25
|
|
|
24
26
|
Coordinates below assume a **1080×1920 vertical** canvas; adjust `offset` for other sizes.
|
|
25
27
|
|
|
@@ -79,19 +81,19 @@ Each word starts `translateY(120%)`, opacity 0; `0.6 s` rise on `power3.out`, wo
|
|
|
79
81
|
|
|
80
82
|
---
|
|
81
83
|
|
|
82
|
-
## 3.
|
|
84
|
+
## 3. Value reveal — bake and fade-in
|
|
83
85
|
|
|
84
|
-
**Category** data · **Use when** revealing a value, price, stat or metric · **Canvas** 620×220 · **Tags** number,
|
|
86
|
+
**Category** data · **Use when** revealing a value, price, stat or metric · **Canvas** 620×220 · **Tags** number, price, stat, data · **Merge-friendly** target value
|
|
85
87
|
|
|
86
|
-
|
|
88
|
+
Bake the final value into the HTML and reveal it with opacity + blur + rise. The value is always present in the DOM — the animation controls only its visibility — so every captured frame shows the correct number. **Never use `onUpdate` to mutate `textContent`**: the seek harness doesn't fire `onUpdate` callbacks, so the value stays at its initial state (`$0`) in every frame.
|
|
87
89
|
|
|
88
90
|
```json
|
|
89
91
|
{
|
|
90
92
|
"asset": {
|
|
91
93
|
"type": "html5",
|
|
92
|
-
"html": "<div class=\"wrap\"><span class=\"cur\">$</span><span
|
|
93
|
-
"css": "html,body{margin:0;width:620px;height:220px;overflow:hidden;background:transparent;font-family:system-ui,sans-serif}.wrap{display:flex;align-items:baseline;justify-content:center;width:620px;height:220px;color:#141414;font-weight:800;font-variant-numeric:tabular-nums}.cur{font-size:70px;margin-right:6px}
|
|
94
|
-
"js": "
|
|
94
|
+
"html": "<div class=\"wrap\"><span class=\"cur\">$</span><span class=\"n\">395</span></div>",
|
|
95
|
+
"css": "html,body{margin:0;width:620px;height:220px;overflow:hidden;background:transparent;font-family:system-ui,sans-serif}.wrap{display:flex;align-items:baseline;justify-content:center;width:620px;height:220px;color:#141414;font-weight:800;font-variant-numeric:tabular-nums;opacity:0;transform:translateY(16px);filter:blur(10px)}.cur{font-size:70px;margin-right:6px}.n{font-size:150px;letter-spacing:-2px}",
|
|
96
|
+
"js": "gsap.to('.wrap',{opacity:1,y:0,filter:'blur(0px)',duration:0.8,ease:'power3.out'});gsap.to({},{duration:1.5});"
|
|
95
97
|
},
|
|
96
98
|
"start": 0,
|
|
97
99
|
"length": 3,
|
|
@@ -101,7 +103,7 @@ Animate a value from 0 to its target. Seek-safe because the count lives in a **t
|
|
|
101
103
|
}
|
|
102
104
|
```
|
|
103
105
|
|
|
104
|
-
`slow` (0.8 s)
|
|
106
|
+
`slow` (0.8 s) reveal on `power3.out` (a decelerating settle reads right for a value), then a `hold`. Swap `395` for any value at generation time; for thousands separators format the string before baking it (`$63,642` not `63642`). Pair with a static label on an adjacent `rich-text` track ("FROM", "AUD").
|
|
105
107
|
|
|
106
108
|
---
|
|
107
109
|
|
|
@@ -185,7 +187,7 @@ A `data:` URI is fine **inside an html5 asset's CSS** (it's iframe content, not
|
|
|
185
187
|
|
|
186
188
|
## Brand kit — re-skin every snippet at once
|
|
187
189
|
|
|
188
|
-
The snippets share one palette (ink `#141414`, accent `#D96B82`) so a set already looks coherent. To re-skin a whole edit to a brand in one place, lift the colours/font into top-level `merge[]` and reference the tokens in each clip's `css` — `merge` find/replace runs over
|
|
190
|
+
The snippets share one palette (ink `#141414`, accent `#D96B82`) so a set already looks coherent. To re-skin a whole edit to a brand in one place, lift the colours/font into top-level `merge[]` and reference the tokens in each clip's `html`, `css`, and `js` — `merge` find/replace runs over all three strings:
|
|
189
191
|
|
|
190
192
|
```json
|
|
191
193
|
"merge": [
|
|
@@ -202,5 +204,5 @@ Then in any snippet's CSS, swap the literal hex for the token: `color:{{ink}}`,
|
|
|
202
204
|
- Each snippet is one clip on its own track. Layer order is top-track-first (see `agent-core.md`) — grain and shine go in **early** tracks, backgrounds in **late** ones.
|
|
203
205
|
- They don't overlap on a single track, so `shotstack validate <file>` stays clean. Run it before rendering.
|
|
204
206
|
- Reuse text via top-level `merge[]` (`{{title}}` in the HTML) — see the lower-third example in `html5.md`.
|
|
205
|
-
- Mix calm and punchy deliberately: a `blur-reveal` title, a `kinetic-headline` hero line, a `
|
|
207
|
+
- Mix calm and punchy deliberately: a `blur-reveal` title, a `kinetic-headline` hero line, a `value-reveal` stat, a `shine` on the product, a pulsing CTA — all on the same tokens, so the set reads as one piece.
|
|
206
208
|
- Heavier motion = longer render. Preview in `shotstack studio <file>` before spending credits.
|
|
@@ -23,8 +23,8 @@ This is the **modern replacement for the deprecated `html` asset.** `html5` runs
|
|
|
23
23
|
|---|---|---|---|
|
|
24
24
|
| `type` | Yes | `"html5"` | Discriminator. |
|
|
25
25
|
| `html` | Yes | string | Body markup. Supports merge fields (`{{title}}`). |
|
|
26
|
-
| `css` | No | string | Stylesheet. Inlined into the iframe `<head>`. |
|
|
27
|
-
| `js` | No | string | Script. Runs after libraries are preloaded. |
|
|
26
|
+
| `css` | No | string | Stylesheet. Inlined into the iframe `<head>`. Supports merge fields. |
|
|
27
|
+
| `js` | No | string | Script. Runs after libraries are preloaded. Supports merge fields. |
|
|
28
28
|
|
|
29
29
|
Clip-level `width` and `height` set the iframe's pixel dimensions. They default to the edit's natural size.
|
|
30
30
|
|
|
@@ -56,7 +56,9 @@ These cover ~95% of motion-graphics use cases. **You can't load other libraries
|
|
|
56
56
|
|
|
57
57
|
## The browser harness (deterministic auto-seek)
|
|
58
58
|
|
|
59
|
-
Frames are captured by **seeking** the animation to each timestamp, not by playing in real time — so your animation must be **seekable**. GSAP (timelines or tweens), anime.js, Lottie, and CSS (`@keyframes`, transitions, `Element.animate()`) are all driven automatically. Anything time-driven that isn't seekable gives a frozen or wrong frame: never use `setTimeout`, `setInterval`, `requestAnimationFrame` loops, `Date.now()` / `performance.now()`, or `gsap.call()`. For "different content at different times" (countdowns, tickers, scene swaps) use the staggered-CSS pattern (see the countdown example) or
|
|
59
|
+
Frames are captured by **seeking** the animation to each timestamp, not by playing in real time — so your animation must be **seekable**. GSAP (timelines or tweens), anime.js, Lottie, and CSS (`@keyframes`, transitions, `Element.animate()`) are all driven automatically. Anything time-driven that isn't seekable gives a frozen or wrong frame: never use `setTimeout`, `setInterval`, `requestAnimationFrame` loops, `Date.now()` / `performance.now()`, or `gsap.call()`. For "different content at different times" (countdowns, tickers, scene swaps) use the staggered-CSS pattern (see the countdown example) or bake values into the HTML and animate their visibility (see the value-reveal snippet).
|
|
60
|
+
|
|
61
|
+
**`onUpdate` callbacks do not fire under seek.** The harness seeks the GSAP timeline to each frame's timestamp without playing through, so `onUpdate` handlers are not invoked. Any DOM mutation made inside an `onUpdate` callback (`textContent`, `innerHTML`, class swaps, attribute changes) will not appear in the rendered video — the element stays at its initial state. **Animate CSS properties only** (opacity, transform, filter, scale) — those are applied directly by GSAP's seek. To display a value, bake it into the HTML at generation time and reveal it with an opacity/transform tween.
|
|
60
62
|
|
|
61
63
|
**Duration comes from the clip's `length`** — there's no animation-duration auto-detection. Size your animation to run within (or fill) the clip's `length`.
|
|
62
64
|
|
|
@@ -156,7 +158,7 @@ Slide-in name + role bar with subtle accent. **Clip sized to the bar (560×120),
|
|
|
156
158
|
- **Clip is the size of the bar, not the canvas** — placement is one `offset` change.
|
|
157
159
|
- **`html, body, .bar` all 560×120.** No absolute positioning inside the iframe — the bar IS the iframe content.
|
|
158
160
|
- One GSAP timeline drives every animation.
|
|
159
|
-
- Merge fields (`{{name}}`, `{{role}}`) in the HTML, populated by **top-level** `merge[]` (sibling of `timeline`/`output`, NOT a clip property). Keeps the asset reusable.
|
|
161
|
+
- Merge fields (`{{name}}`, `{{role}}`) in the HTML, populated by **top-level** `merge[]` (sibling of `timeline`/`output`, NOT a clip property). Keeps the asset reusable. Merge fields also resolve in `css` and `js` — use `{{accent}}` in CSS for brand colours, or `{{targetValue}}` inside a JS string literal for data-driven animation targets.
|
|
160
162
|
- A trailing `.to({}, { duration: 3.5 })` holds the final state before the clip ends.
|
|
161
163
|
|
|
162
164
|
## Worked example: animated bar chart (D3 + GSAP)
|
|
@@ -236,6 +238,7 @@ The same pattern scales to scene transitions (each scene is a `<section>` with i
|
|
|
236
238
|
|
|
237
239
|
If you're building something that genuinely cannot be expressed without canvas, render it as a `<video>` or `<image>` asset instead of an `html5` clip.
|
|
238
240
|
2. **Mismatched dimensions.** If `clip.width = 1920` and your CSS sets `body { width: 1280px }`, content gets cropped or stretched. Pin the iframe's `html, body` dimensions to the clip dimensions.
|
|
241
|
+
3. **JS syntax or runtime errors produce a blank clip with no render error.** If `asset.js` throws (syntax error or uncaught runtime error), the entire clip renders as a blank frame. The render still reports `status: "done"` with no error — there is no feedback loop. `shotstack validate` catches JS syntax errors offline; runtime errors (e.g. referencing a DOM element that doesn't exist) are silent. If a clip is blank, check the JS first: run `node --check` on the string, or wrap suspect code in `try/catch` to surface the error.
|
|
239
242
|
|
|
240
243
|
## When to use `html5` vs `rich-text`/`svg`
|
|
241
244
|
|
|
@@ -113,7 +113,7 @@ tl.from('.el', { opacity: 0, y: 12, duration: 0.6, ease: 'power3.out' }) // in
|
|
|
113
113
|
|
|
114
114
|
## Brand kit — set the palette once, re-skin everything
|
|
115
115
|
|
|
116
|
-
Don't hardcode hex and font per snippet. Declare the brand as top-level `merge[]` fields and reference the tokens in every clip's `css`/`
|
|
116
|
+
Don't hardcode hex and font per snippet. Declare the brand as top-level `merge[]` fields and reference the tokens in every clip's `html`/`css`/`js` — one edit re-skins the whole video — brand tokens applied once, via Shotstack merge:
|
|
117
117
|
|
|
118
118
|
```json
|
|
119
119
|
"merge": [
|
|
@@ -129,7 +129,7 @@ Don't hardcode hex and font per snippet. Declare the brand as top-level `merge[]
|
|
|
129
129
|
.cta { background: {{accent}}; }
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
-
`merge` find/replace runs over the whole edit, including `html`/`css` strings, so `{{accent}}` resolves everywhere. Keep one accent and use it sparingly — a headline word, a number, a CTA, one glow — everything else neutral. **Colour is earned, never sprinkled.**
|
|
132
|
+
`merge` find/replace runs over the whole edit, including `html`/`css`/`js` strings, so `{{accent}}` resolves everywhere. Keep one accent and use it sparingly — a headline word, a number, a CTA, one glow — everything else neutral. **Colour is earned, never sprinkled.**
|
|
133
133
|
|
|
134
134
|
## Motion checklist
|
|
135
135
|
|
|
@@ -6,12 +6,14 @@ Common errors and the fix for each. **Catch most of them before rendering:** `sh
|
|
|
6
6
|
|
|
7
7
|
- "Unknown property: alignment" / wrong property names
|
|
8
8
|
- "Invalid option: expected one of top|middle|bottom"
|
|
9
|
-
- Video looks stretched /
|
|
9
|
+
- Video looks stretched / squishing after rendering
|
|
10
10
|
- "Font not found"
|
|
11
11
|
- Captions cover the whole frame
|
|
12
12
|
- Clips flicker / "clips overlap" on one track
|
|
13
13
|
- Timeline renders but layers are wrong
|
|
14
14
|
- "Invalid asset URL"
|
|
15
|
+
- html5 clip is blank but render succeeded
|
|
16
|
+
- html5 clip animates in the rendered MP4 but not in Studio preview
|
|
15
17
|
- "Render failed" with no other detail
|
|
16
18
|
- Render takes much longer than expected
|
|
17
19
|
- Credits exhausted on stage environment
|
|
@@ -82,6 +84,29 @@ The `src` is not a public HTTPS URL, or it's a `data:` URI, or it's a local path
|
|
|
82
84
|
|
|
83
85
|
**Fix:** host the asset at a public HTTPS URL or use a presigned URL with credentials in the URL itself. For tests, pull from `references/asset-library.md`.
|
|
84
86
|
|
|
87
|
+
## html5 clip is blank but render succeeded
|
|
88
|
+
|
|
89
|
+
An `html5` clip renders as a completely blank frame, but the render reports `status: "done"` with no error. This is a **silent failure** — there is no feedback loop.
|
|
90
|
+
|
|
91
|
+
Two causes:
|
|
92
|
+
|
|
93
|
+
1. **JS syntax error in `asset.js`.** A missing semicolon, unbalanced brace, or any other parse error crashes the entire script before any animation runs. `shotstack validate <file>` catches this offline — it runs each `asset.js` string through a syntax check and reports the error with the clip path. **Always validate before rendering.**
|
|
94
|
+
|
|
95
|
+
2. **JS runtime error.** Referencing a DOM element that doesn't exist (`document.getElementById("missing")`), calling a method on `null`, or any other uncaught exception — the script stops mid-execution and the clip stays in its initial state (typically invisible if elements start at `opacity:0`). These are not caught by `validate` (they require the runtime DOM). To debug, wrap suspect code in `try/catch` and log to a visible element, or simplify the JS to isolate the failing line.
|
|
96
|
+
|
|
97
|
+
A third variant: values stuck at `$0` or their initial state. This happens when `onUpdate` callbacks are used to mutate `textContent` — the seek harness doesn't fire `onUpdate`, so the DOM never updates. Bake final values into the HTML and animate opacity/transform instead. See `references/html5.md` → "The browser harness".
|
|
98
|
+
|
|
99
|
+
## html5 clip animates in the rendered MP4 but not in Studio preview
|
|
100
|
+
|
|
101
|
+
Studio preview and the cloud render use different capture mechanisms. The cloud render (puppeteer) properly handles all GSAP properties; Studio's client-side preview does not. This is a parity gap, not a render bug — the MP4 is the ground truth.
|
|
102
|
+
|
|
103
|
+
Known differences:
|
|
104
|
+
|
|
105
|
+
- **`:nth-child` and compound CSS selectors** as GSAP targets don't resolve in Studio preview. `.brow:nth-child(1) .bar` silently matches nothing; rows and bars stay in their initial state. Use unique element IDs (`#row0`, `#bar0`) instead — they work in both.
|
|
106
|
+
- **`scale` / `scaleX` / `scaleY` transforms** don't animate in Studio preview. Elements starting at `scale(0)` or `scaleX(0)` stay invisible. Use `width` (for bars), `opacity`, or `y` transitions instead.
|
|
107
|
+
|
|
108
|
+
If a clip looks wrong in Studio but the rendered MP4 is correct, don't waste credits re-rendering — check whether the JS uses structural selectors or scale transforms, and swap for ID-based targets and width/opacity animations.
|
|
109
|
+
|
|
85
110
|
## "Render failed" with no other detail
|
|
86
111
|
|
|
87
112
|
`shotstack status <id> --output json` returns the full response including the error message. The `--watch` text view truncates.
|
|
@@ -128,7 +128,7 @@ Compose from this rather than round-tripping the full schema — these are the v
|
|
|
128
128
|
|
|
129
129
|
`align.horizontal` = `left|center|right`; `align.vertical` = `top|middle|bottom` (**not** `center`). `animation.preset` = `fadeIn` `slideIn` `typewriter` `ascend` `shift` `movingLetters`; `animation.style` = `character|word`; `animation.direction` = `left|right|up|down`. Give the clip a `width`/`height` box so text wraps and aligns where you expect.
|
|
130
130
|
|
|
131
|
-
For motion beyond this (kinetic type,
|
|
131
|
+
For motion beyond this (kinetic type, value reveals, shine sweeps, grain, pulsing CTAs) reach for `html5` — see [`references/html5-snippets.md`](../references/html5-snippets.md).
|
|
132
132
|
|
|
133
133
|
## Motion language (house tokens)
|
|
134
134
|
|
|
@@ -163,6 +163,51 @@ Pick `output.resolution` (preset) OR `output.size.width`+`output.size.height` (c
|
|
|
163
163
|
|
|
164
164
|
Custom sizes must be divisible by 2.
|
|
165
165
|
|
|
166
|
+
## Merge fields
|
|
167
|
+
|
|
168
|
+
Top-level `merge[]` (sibling of `timeline`/`output`, NOT a clip property) is a simple find-and-replace that runs over the entire Edit JSON **before** any asset is processed. Each entry is `{ "find": "<TOKEN>", "replace": "<value>" }` — the `find` value is the token name **without** braces; every `{{TOKEN}}` occurrence in any string throughout the Edit is swapped for the value.
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"merge": [
|
|
173
|
+
{ "find": "TITLE", "replace": "Q4 Results" },
|
|
174
|
+
{ "find": "REVENUE", "replace": "$1,247,890" },
|
|
175
|
+
{ "find": "ACCENT", "replace": "#22D3EE" }
|
|
176
|
+
],
|
|
177
|
+
"timeline": {
|
|
178
|
+
"tracks": [{
|
|
179
|
+
"clips": [{
|
|
180
|
+
"asset": { "type": "rich-text", "text": "{{TITLE}}", "font": { "family": "Roboto", "size": 60, "color": "{{ACCENT}}" } },
|
|
181
|
+
"start": 0, "length": 3
|
|
182
|
+
}]
|
|
183
|
+
}]
|
|
184
|
+
},
|
|
185
|
+
"output": { "format": "mp4", "resolution": "1080" }
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Rules:**
|
|
190
|
+
- Whitespace inside braces is ignored: `{{ TITLE }}` and `{{TITLE}}` are the same token.
|
|
191
|
+
- Token names are **case sensitive**. Use `UPPER_SNAKE_CASE` (the Shotstack convention).
|
|
192
|
+
- The `{{` delimiter cannot be changed.
|
|
193
|
+
- One `merge[]` entry can replace multiple `{{TOKEN}}` occurrences across the entire Edit.
|
|
194
|
+
- The replacement is a dumb string swap — no conditionals, no loops, no escaping.
|
|
195
|
+
|
|
196
|
+
**Where merge fields resolve:** every string value in the Edit JSON — `rich-text` `text` and `font.color`; `html5` `html`, `css`, and `js`; `svg` markup; `asset.src` URLs; `clip.transition` names; any string anywhere. This is an Edit-wide capability, not an html5 feature.
|
|
197
|
+
|
|
198
|
+
**When to use merge vs baking at generation time:**
|
|
199
|
+
|
|
200
|
+
| Scenario | Use |
|
|
201
|
+
|---|---|
|
|
202
|
+
| Reusable template rendered many times with different data (reports, personalised videos, per-user wraps) | `merge[]` — keep one Edit JSON, swap the `merge[]` per render |
|
|
203
|
+
| Brand re-skinning (palette, font, accent across every clip) | `merge[]` — change one `merge[]` entry to re-skin the whole video |
|
|
204
|
+
| One-off render or data unique to a single video | Bake values directly into the asset at generation time — simpler, no merge indirection |
|
|
205
|
+
| Generating multiple edits programmatically (e.g. one per user from a CSV) | Either — merge keeps the template reusable, baking is more direct. If the template won't be reused, bake. |
|
|
206
|
+
|
|
207
|
+
**Checking merge results:** pass `?data=true&merged=true` on the status request to see the merged Edit JSON in the response — useful for debugging unresolved placeholders.
|
|
208
|
+
|
|
209
|
+
**Validate caveat:** `shotstack validate` checks `html5` JS syntax **before** merge resolution. A `{{TOKEN}}` inside a JS string literal (e.g. `color: "{{ACCENT}}"`) is valid JS and passes. A `{{TOKEN}}` in JS *code* position (not inside a string) may fail validation pre-merge but work at render time post-merge — keep merge fields inside string literals to avoid false positives.
|
|
210
|
+
|
|
166
211
|
## Asset types
|
|
167
212
|
|
|
168
213
|
Use only the **current** asset types; the deprecated ones still parse but should not be used in new templates.
|
|
@@ -211,9 +256,9 @@ One overlay is a `rich-text` job. **Several videos "in different styles" is not*
|
|
|
211
256
|
|---|---|---|
|
|
212
257
|
| 1 — type & layout | `rich-text` | Titles, lower-thirds, kickers, captions, price/CTA pills. Fast and reliable; the right default for static styled text. |
|
|
213
258
|
| 2 — shapes | `svg` | Colour panels, rules, badges, frames, geometric accents behind or around type. |
|
|
214
|
-
| 3 — motion graphics | `html5` | Kinetic type,
|
|
259
|
+
| 3 — motion graphics | `html5` | Kinetic type, value reveals, shine sweeps, animated gradients, film grain, masked reveals, data-driven overlays — anything that should *move* beyond a `transition` or a Ken-Burns `effect`. gsap / anime / d3 / lottie are preloaded. See [`references/html5.md`](../references/html5.md) and the copy-paste clips in [`references/html5-snippets.md`](../references/html5-snippets.md). |
|
|
215
260
|
|
|
216
|
-
**When the brief asks for a *range*, deliberately spread across the ladder.** A strong set: a couple of clean `rich-text` studio cuts, one or two `svg` colour-block promos, and several `html5` pieces carrying the real motion (kinetic headline,
|
|
261
|
+
**When the brief asks for a *range*, deliberately spread across the ladder.** A strong set: a couple of clean `rich-text` studio cuts, one or two `svg` colour-block promos, and several `html5` pieces carrying the real motion (kinetic headline, value reveal, shine-swept CTA, grain-graded teaser). Reserve the elaborate `html5` treatments for the hero / hype cuts where motion sells the product. If every clip in a "variety" brief is `rich-text`, you have not delivered variety.
|
|
217
262
|
|
|
218
263
|
## Fonts
|
|
219
264
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|