# Description Engine ## User Perspective The description engine generates video descriptions from reusable building blocks. Instead of writing descriptions manually for each video, users define blocks (intro, CTA, social links, sponsor copy) and assemble them into templates. Each video can then use a template as its starting point, overriding individual blocks or variables as needed. ### Blocks (`/blocks`) A block is a named, reusable piece of description content. Types: - **Static**: Plain text, rendered as-is. No token substitution. Suitable for boilerplate that never changes. - **Variable**: Text with `{token}` placeholders. Tokens are resolved from team variables, video fields, or collaborator data. - **Campaign**: Like Variable but linked to a Campaign. Auto-included in descriptions during the campaign's date window — no manual placement needed. - **Collaborator**: Expanded once per assigned collaborator. The block's content is used as a template, cloned for each collaborator in `VideoConfig.collaboratorIds`, with `{collab.*}` tokens resolved per-collaborator. Clones are joined with a blank line (or single newline if compact). - **Conditional**: Shown only when its condition evaluates to true. Blocks have a **compact** toggle: when compact is off, a blank line is inserted before the block's content during rendering. ### Templates (`/templates`) A template defines the default block assembly for a type of video. It contains: - **Default block order**: which blocks appear and in what sequence - **Default variable values**: pre-filled values for variable tokens - **Video fields**: default metadata values (privacy, category, tags, etc.) applied when assigned - **Rules**: required links for lint checking Applying a template to a video copies the block order, variable values, and optionally the video field defaults. ### Variable Tokens In Variable-type blocks, `{token}` placeholders are replaced at render time: - `{video.title}`, `{video.tags}`, `{video.category}`, `{video.gameTitle}`, `{video.language}` — from the video record - `{video.scheduledAt}`, `{video.recordingDate}` — dates; support inline format override (see below) - `{video.playlists}` — comma-separated list of playlist **titles** the video belongs to - `{video.playlistLinks}` — comma-separated list of full YouTube playlist **URLs** the video belongs to - `{collab.name}`, `{collab.youtube}`, `{collab.twitch}`, `{collab.instagram}`, `{collab.tiktok}`, `{collab.twitter}`, `{collab.bluesky}`, `{collab.discord}`, `{collab.aliases}`, `{collab.notes}` — from the **first** assigned collaborator only (see below). Note: `{collab.youtube}` resolves to the full URL (`https://www.youtube.com/@handle`), not just the handle. - `{my_variable}` — from team-level variables or video-level variable value overrides ### Collaborator Token Behavior with Multiple Collaborators **In VARIABLE, CONDITIONAL, CAMPAIGN, and freetext blocks:** `{collab.*}` tokens always resolve to the **first** collaborator in the video's `collaboratorIds` list. If a video has multiple collaborators, tokens in non-COLLABORATOR blocks only reflect collaborator #1. **In COLLABORATOR blocks:** the block is cloned once per collaborator, each clone resolved against that collaborator's data, then joined with a blank line. This is the correct way to list multiple collaborators — one COLLABORATOR block produces one entry per person. **Practical rule:** use a COLLABORATOR block whenever the content should repeat for each collaborator. Use `{collab.*}` tokens in other block types only when you have exactly one collaborator, or you intentionally want only the first. ### Date Token Format Overrides Date tokens (`{video.scheduledAt}` and `{video.recordingDate}`) support an inline format string using `|` as a separator: ``` {video.scheduledAt|DD.MM.YYYY} → e.g. 05.03.2024 {video.recordingDate|MMMM D, YYYY} → e.g. March 5, 2024 ``` Available format tokens: `YYYY` (4-digit year), `YY` (2-digit), `MMMM` (full month), `MMM` (short month), `MM` (zero-padded month), `M` (month), `DD` (zero-padded day), `D` (day). Without an inline override, the team's date format setting is used. If no team format is set, the default is `YYYY-MM-DD`. If the date field is not set on the video, the token resolves to an empty string. No other tokens support the `|format` syntax — it is silently ignored on non-date tokens. ### Freetext Entries In the video editor, users can add freetext sections directly in the block order without creating a named block. These appear as free-form text areas in the editor and are stored with IDs prefixed `freetext:`. ## Developer Perspective ### Render Path 1. `VideoRenderService.render(videoId)` fetches all data 2. Builds ordered list of blocks from `VideoConfig.blockOrder` 3. Appends active CAMPAIGN blocks at the end (regardless of order) 4. For each block: applies `blockOverrides`, resolves tokens 5. Joins blocks with blank lines (unless `compact: true`) 6. Returns `{ rendered: string, hash: string }` ### Key Files - `backend/src/shared/render-engine/video-render.service.ts` — data fetching + orchestration - `backend/src/shared/render-engine/render-engine.service.ts` — pure rendering logic - `backend/src/shared/system-variables/system-variables.registry.ts` — system token registry ### Adding a New System Token 1. Add the token to `SYSTEM_VARIABLES[]` in `system-variables.registry.ts` 2. Add it to `SYSTEM_VARIABLE_TOKENS` Set 3. Implement resolution in the appropriate resolver in `RenderEngineService` 4. Without step 2, `resolveVariables()` will attempt to resolve it as a team/video variable and fail silently ### Block Versioning `DescriptionBlock` has a `BlockVersion` history table. Every content change creates a new version snapshot. Version number increments on each save. ## Related - [[07 - Render Engine]] (architecture) - [[03 - Blocks API]] - [[04 - Templates API]] - [[06 - Collaborators]]