Files
cycle-atlas/.agents/skills/testimonial-video/references/batch-and-typography.md
T

141 lines
6.3 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Batch & Typography
How to feed many reviews into one card design, plus the quote-typography craft that separates a credible testimonial from a sloppy one.
## Review prop schema
One object per testimonial. Keep it flat so a CSV row maps to it directly.
```ts
type Review = {
quote: string; // verbatim — never paraphrased or trimmed mid-sentence
highlight?: string; // EXACT substring of `quote`; one phrase only
rating: number; // 0–max, halves allowed (4.5)
max?: number; // default 5
name: string; // required — a quote with no name is not social proof
role?: string;
company?: string;
avatar?: string; // real photo URL, or omit (never a stock face)
};
```
### Validation (fail loud, before rendering 50 videos)
```ts
function validate(r: Review): string[] {
const errs: string[] = [];
if (!r.quote?.trim()) errs.push("quote is empty");
if (!r.name?.trim()) errs.push("name is required (no anonymous testimonials)");
const max = r.max ?? 5;
if (r.rating < 0 || r.rating > max) errs.push(`rating ${r.rating} out of 0–${max}`);
if (r.highlight && !r.quote.includes(r.highlight))
errs.push(`highlight "${r.highlight}" is not a substring of the quote`);
return errs;
}
```
The `highlight ⊂ quote` check is the important one: it guarantees you only ever emphasize words the customer actually wrote. If the highlight isn't an exact substring, the emphasis silently disappears in the card — so block the render instead.
## CSV → props
Reviews usually arrive as a spreadsheet export. Map columns to the schema and coerce the rating.
```js
import { parse } from "csv-parse/sync";
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
// CSV columns: quote,highlight,rating,name,role,company,avatar
const rows = parse(readFileSync("reviews.csv"), { columns: true, trim: true });
mkdirSync("reviews", { recursive: true });
rows.forEach((row, i) => {
const r = {
quote: row.quote,
highlight: row.highlight || undefined,
rating: Number(row.rating),
name: row.name,
role: row.role || undefined,
company: row.company || undefined,
avatar: row.avatar || undefined,
};
const errs = validate(r);
if (errs.length) { console.error(`Row ${i + 1} (${r.name}):`, errs); process.exit(1); }
writeFileSync(`reviews/${String(i).padStart(3, "0")}-${slug(r.name)}.json`,
JSON.stringify(r, null, 2));
});
const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
```
## Batch render — one video per review
```bash
mkdir -p out
for f in reviews/*.json; do
name=$(basename "$f" .json)
npx remotion render Testimonial "out/$name.mp4" --props="$f"
done
```
Everything visual lives in the single `theme` object in the composition (colors, fonts, quote-mark glyph, spacing). Because only the props change per render, 50 testimonials stay pixel-for-pixel brand-locked while the words and score vary. To re-theme the whole set, edit one object and re-run the loop.
For multi-aspect (9:16 feed + 1:1 post + 16:9 site), register the composition at each size with a shared center-safe layout and loop over both `reviews/*.json` and the aspect list.
## Quote typography craft
Motion is the easy part; legible, well-set type is what makes the quote trustworthy.
### Smart quotes, always
Straight quotes (`"`) read as un-proofed. Convert to typographic quotes before rendering.
```js
const smarten = (s) => s
.replace(/(^|[\s([{])"/g, "$1“").replace(/"/g, "”") // “ ”
.replace(/(^|[\s([{])'/g, "$1‘").replace(/'/g, "’") // ‘ ’
.replace(/--/g, "—"); // em dash
```
### Hanging quotation marks
A leading quote glyph indents the first line and breaks the flush-left edge. Hang it into the margin so the text block stays optically aligned — the gold standard for set quotes.
```css
blockquote { hanging-punctuation: first; } /* Safari/WebKit */
/* Cross-engine fallback: pull the opening mark left by its own width */
blockquote::before { content: "\201C"; margin-left: -0.55em; color: var(--muted); }
```
When the card uses a large decorative quote mark (as in `quote-card.md`), drop the inline opening glyph so the mark isn't doubled.
### Splitting into lines / clauses
Reveal in reading order means splitting on meaning, not on a fixed character count. Prefer clause boundaries (`. ! ? , ; —`), then pack to a comfortable measure (~32–40 chars at card size). The `toLines` helper in `quote-card.md` does exactly this. Never split a short quote into one-word-per-line — that reads as a ransom note, not a testimonial.
### Orphans & widows
A single dangling word on the last line looks broken. Bind the last two words with a non-breaking space so they wrap together.
```js
const noOrphan = (s) => s.replace(/ ([^ ]+)\s*$/, " $1");
```
### Legibility in motion
- Use a font that stays readable while moving — avoid thin weights and tight tracking; a serif or a humanist sans at 500–600 weight holds up best on a rising line.
- Animate **opacity + a small translate** only. Blur, rotation, and scale on body text hurt readability mid-reveal.
- Contrast: body text ≥ 4.5:1 against the card; the highlight accent should pass against both the card and the ink it sits behind.
- One emphasized span per quote. A second highlight halves the weight of the first.
## Star-rating accessibility & correctness
- The visual stars are decorative; expose the value to assistive tech with `role="img"` and `aria-label="4.5 out of 5 stars"` on the container (already in `quote-card.md`).
- For fractional scores, the width-clip method (`rating / max * 100%`) is exact for any decimal — no rounding to the nearest half-star, so 4.3 renders as 4.3.
- Never animate the stars before the quote is read, and never show more filled stars than the data supports. A faked rating is the fastest way to lose the trust the testimonial was meant to build.
- If a review has no rating, omit the star row entirely rather than defaulting to 5 — an invented score is worse than none.
## Pacing notes
- Hold the finished card still for the back half of the clip (≥3s) so it's screenshot-able and re-shareable — shares are the whole point of social proof.
- For a sequence of testimonials in one video, give each card a full read (~3–4s) and hard-cut between them; cross-dissolving quotes makes both unreadable.