Find what kind of frame this kit cannot draw yet, then build it — for an agent working inside content-agent-kit. Use when the user says "template mới cho hôm nay", "thêm template", "add a template", "we need a frame for X", or asks what is missing from video-templates/. Also covers pulling one from the upstream HyperFrames registry when that is the cheaper answer.
Installs into .claude/skills of the current project.
Sponsors
SSkills DirectorySkills Directory
Is that skill safe to install?
SSkills DirectorySkills Directory
Know which skills are safe — weekly.
Are you the author of New Template?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/xuho-it-new-template)
---
name: new-template
description: Find what kind of frame this kit cannot draw yet, then build it — for an agent working inside content-agent-kit. Use when the user says "template mới cho hôm nay", "thêm template", "add a template", "we need a frame for X", or asks what is missing from video-templates/. Also covers pulling one from the upstream HyperFrames registry when that is the cheaper answer.
---
# Add a template
Two different jobs share this name, and picking the wrong one wastes an hour.
| the user wants | do this |
|---|---|
| an effect, overlay, transition or caption style | **pull it** — one command, §1 |
| a kind of frame the kit cannot draw | **build it** — §2 onward |
## Hướng dẫn từng bước cho người dùng
> Chạy lệnh `node scripts/…` ở **thư mục gốc của kit** — thư mục có `AGENTS.md`, hai cấp trên file này (nếu skill được cài bằng `install-skills.mjs --kit`, đường dẫn nằm trong file `.kit-root` cạnh file này).
Dẫn người dùng theo đúng các bước dưới, bằng lời đơn giản. Trước khi bắt đầu: một câu nói skill sẽ làm gì, mất khoảng bao lâu, cần người dùng chuẩn bị gì. Đầu mỗi bước báo **"Bước n/N — <tên bước>"** kèm một câu đang làm gì. Khi cần người dùng làm gì, chỉ hỏi đúng một việc. Kết thúc: kết quả nằm ở đâu và việc nên làm tiếp.
| Bước | Agent làm gì | Người dùng cần làm |
|---|---|---|
| 1. Tìm chỗ còn thiếu | Liệt kê template có sẵn và chạy `motion-index.mjs` | Nói video của bạn đang thiếu kiểu khung nào |
| 2. Đề xuất | Đưa 2–3 phương án, mỗi phương án lấp một chỗ trống | Chọn một |
| 3. Lấy sẵn hoặc tự dựng | Kéo về bằng `add-template.mjs`, hoặc tự viết hai bản 9:16 và 16:9 | — |
| 4. Đo và ghi danh mục | Chạy `theme-probe.mjs --template <id>`, thêm mục vào `CATALOG.md` | — |
| 5. Xem bằng mắt | Render bằng `template-sheet.mjs <id>` cả hai khung hình (vài phút) | Duyệt ảnh |
| 6. Chạy test | `node --test tests/*.test.mjs` | — |
**Hay vướng:**
- Thiếu `NOTICE.md` hoặc thiếu mục trong `CATALOG.md` → test báo lỗi; thêm vào là hết.
- Chữ viết cứng trong HTML sẽ hiện trên mọi video → mọi chữ phải đi qua slot.
- Font không có dấu tiếng Việt → chạy `font-check.mjs` trước khi render.
## 0. Never start from "what would be cool"
A template with no gap behind it is a twenty-third way to show a headline. Find the gap
first, and be able to say it in one sentence before writing any CSS.
```bash
# what exists, and what each one is FOR
node -e "import('./scripts/video/lib/paths.mjs').then(m=>console.log(m.listTemplateIds().join('\n')))"
grep -n '^\*\*Role:\*\*' video-templates/CATALOG.md
# and how they MOVE — which techniques are well covered, which are nearly absent
node scripts/video/motion-index.mjs
```
The second command answers a question the first cannot. A gap is not only "no frame does this
job" — it is also "no frame moves this way". `mask-sweep` and `clip-reveal` have two samples
each; `draw-on` has thirteen. A new template that fills a thin row is worth more than one that
adds a fourteenth way to draw a line, and the counts tell you which is which.
Then read `templates/VIDEO_GENRES.template.json` and ask which beat has no good frame.
A real gap sounds like: *"`frame-chart-bars` compares magnitudes and `frame-split-compare`
compares states — nothing can strike a claim down."* A fake one sounds like *"we could use
another dark statement frame."*
**Say the gap out loud to the user before building.** They know what they publish; you are
guessing from a folder listing.
### If the user just says "a new template for today"
Do the survey, bring back **two or three candidates with the gap each one fills**, and let
them pick. Do not build five because "new templates" was plural — five templates nobody
needed is worse than one they will use.
### A SIGNATURE FRAME every episode — how to do it without rotting the library
A series can ask for one bespoke frame per episode so no two videos look alike. That is a
legitimate goal, and it is in tension with §0 above. Resolve it this way, not by suspending
the rule:
**The gap for a signature frame is the episode, not the library.** It sounds like *"this
episode turns on 40 days of quarantine against 40 days of fundraising, and nothing in the
library can hold two numbers that mirror each other and mean opposite things."* It does not
sound like *"episode 7 needs its own look."* If you cannot name the one image the episode
turns on, the episode does not need a frame — it needs a better script.
**Build ONE, for the beat that carries the video.** Usually the reveal or the twist. The other
fifteen scenes reuse library frames; that is what makes the signature one land.
**Name it for its job, not its episode.** `frame-mirror-numbers`, not `frame-poveglia-40`.
An episode-specific name guarantees nobody reuses it, which is how you end up with forty
single-use folders.
**Every contract in §2 still applies** — both aspects, both timeline keys, CATALOG entry,
ambient layer, the lot. A signature frame that renders black is not distinctive, it is broken.
**Review the pile every ~10 episodes.** Frames used twice or more graduate into the general
library and get documented as such. Frames used once and never again get deleted — leaving
them costs a CATALOG row, a theme probe, and a slower test suite for nothing. This review is
the whole reason the practice stays affordable; skip it and the library is landfill in a year.
## 1. Pulling from upstream is often the right answer
367 items live in the HyperFrames registry, and `registry-watch.mjs` reports what arrived
since the last check. For code-driven motion prompts and 12 visual style recipes (`productui`,
`explainer`, `collage`, `handdrawn`, `pixel`, `lowpoly`, `generative`, `photoreal`), also check
`opus-55-videos` (`yihui-dev/awesome-opus5-5-videos`) and `opus-video-styles` via
`node scripts/install-skills.mjs --list`.
```bash
node scripts/video/registry-watch.mjs # anything new upstream?
node scripts/video/add-template.mjs --list --grep caption # search
node scripts/video/add-template.mjs <name> <name> # pull
```
**What you get is building material, not a scene template.** The command says so itself:
blocks and components ship `compositions/` with no `index.html`, so `listTemplateIds()`
skips them and no `script.json` can name them. They are overlays and effects to compose
with. Turning one into a scene template still means writing both compositions by hand — at
which point you are in §2 anyway, just with a head start on the design.
Licence and attribution are handled: the installer writes a `NOTICE.md` with the source URL
and the pinned commit sha. Do not delete it.
## 2. Building one — the five files
Copy the shape of a recent template rather than inventing one. `frame-checklist` and
`frame-timeline` are the plainest.
```
video-templates/<id>/
├── index.html 16:9 — data-width="1920" data-height="1080"
├── compositions/portrait.html 9:16 — data-width="1080" data-height="1920"
├── meta.json { "id": "<id>", "name": "Human Name" }
├── hyperframes.json copy verbatim from any existing template
└── NOTICE.md who wrote it, why it exists, which slots
```
`id` is `frame-<kebab>`. The two compositions must expose the **same slot names** — a test
checks it, because two files drift the moment they are edited separately.
**Generate both from one source.** Every template added since v0.5.0 was emitted by a small
script that takes shared markup, shared JS and per-aspect layout CSS. Writing the same file
twice is how slot parity gets broken, and it is also how a fix lands in one aspect only.
### The rules, all enforced by `tests/templates.test.mjs`
- **Draw from data, never from a hardcoded value.** If a bar length and its printed number
can disagree, one day they will.
- **No caller-facing text in the markup.** Every visible string arrives through a slot and is
set in JS. Text sitting in the body renders on *every* video regardless of what the caller
passed — found three times here, twice after it reached a finished render. Decorative
punctuation (a quote mark, a tick) may be CSS `content`; a *word* may not.
- **An empty slot removes its element**, it does not render a placeholder. An empty heading
reads as missing data; an absent one reads as a deliberate omission.
- **No default is someone else's brand.** No real URLs, no channel names in
`data-composition-variables`.
- **Vietnamese-capable fonts.** Be Vietnam Pro, Lora, Alfa Slab One are known-good. Test with
`ữ ệ ợ ằ` — a font missing diacritics renders `bước` as `bc`.
### Three traps that pass every test and still look wrong or stall rendering
**1. A delayed animation with no start state shows its ending first.**
```css
.thing { animation: rise 0.5s ease 0.45s forwards; } /* wrong */
.thing { opacity: 0; animation: rise 0.5s ease 0.45s forwards; } /* base = the start */
.thing { animation: rise 0.5s ease 0.45s both; } /* or backwards fill */
```
`forwards` does nothing during the delay — the element renders with its normal style, which
for a `forwards` animation is where it *finishes*. Half a second of the finished state, then
a snap back. Use `both` when the start state differs between aspects.
**2. An animation must travel the same way as the thing it reveals.** A divider sweeping in from
the left while a `clip-path` uncovers from the right reads as two animations that happen to
end together. Both real bugs, both in shipped-looking code, neither catchable by a test.
**3. Missing sub-composition timeline registration (`window.__timelines["portrait"]`).**
Every composition (both `index.html` and `compositions/portrait.html`) MUST register its timeline under both IDs:
```javascript
window.__timelines = window.__timelines || {};
window.__timelines["main"] = window.__timelines["portrait"] = timeline;
```
If `compositions/portrait.html` only registers `"main"` or omits `"portrait"`, Puppeteer will wait **45 seconds** for the sub-composition ready signal, causing a 10x render slowdown. Always register both keys.
## 3. Measure, document, wire up
```bash
node scripts/video/theme-probe.mjs --template <id> # writes theme-map.json
```
Themes flip lightness only on dark canvases. An unmeasured template is flipped on a guess,
which renders — wrongly, in exactly the way the theme system exists to prevent.
Then:
- a `## <id>` section in `video-templates/CATALOG.md`: role, best-for, and a **slot table with
real character limits** you found by rendering long text, not by estimating
- raise the count in `.github/workflows/ci.yml` (`listTemplateIds().length < N`)
- add it to `NOTICE.md`'s list of templates original to this repo
- add it to a genre in `templates/VIDEO_GENRES.template.json` **only if it earns a beat there**
— a template in no genre is fine; a genre with a frame that does not fit is not
- update the counts in README, README.en, `docs/16` and `.github/repo-about.json` — a test
checks all seven against the folder
## 4. Look at it. The tests cannot.
```bash
node scripts/video/template-sheet.mjs <id> --out /tmp/check.jpg
node scripts/video/template-sheet.mjs <id> --aspect 16:9 --out /tmp/check-l.jpg
```
**Sample the animation, not just the settled frame** — tile three moments and open the image.
Everything visual ever caught in this repo was caught this way: two templates rendering
1920×1080 inside a 9:16 video with correct CSS and a correct viewport; a strike-through that
crossed only the first line of a two-line claim; an eleven-character name wrapping so a
shutter wiped two lines instead of one.
Render with **real content at realistic length**, and check the awkward cases: the longest
string a caller might pass, an empty optional slot, one item in a list that expects four.
> `data-composition-variables` does **not** reach the renderer. Those are the editor's preview
> defaults; `composeTemplate({ inputs: {} })` comes back blank. Pass inputs explicitly, or
> read the defaults out of the HTML and pass those.
## 5. Ship it
```bash
node --test tests/*.test.mjs
```
One PR per template batch, with the strip image in the description so a reviewer sees the
frames without checking out the branch. Say which gap each one fills — if that sentence is
hard to write, the template probably should not exist.