248 lines
13 KiB
Markdown
248 lines
13 KiB
Markdown
---
|
|
phase: 43-documents-branding
|
|
plan: 02
|
|
type: execute
|
|
wave: 2
|
|
depends_on: ["43-01"]
|
|
files_modified:
|
|
- server/src/services/renderers/brand-renderer.ts
|
|
- server/src/services/content-job-runner.ts
|
|
- server/src/__tests__/brand-renderer.test.ts
|
|
autonomous: true
|
|
requirements: [BRAND-01, BRAND-02, BRAND-03, BRAND-04, BRAND-05, BRAND-06]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "renderBrandKit returns a brand-kit-bundle with all required fields populated"
|
|
- "Logo SVG is generated via LLM and cleaned with SVGO"
|
|
- "Avatar PNGs are rasterized at 5 sizes: 512, 256, 128, 64, 32"
|
|
- "Social images are generated for twitter-profile, twitter-banner, linkedin-profile, linkedin-banner, instagram-profile"
|
|
- "Email signature and letterhead are generated as HTML strings"
|
|
- "Brand guidelines PDF is generated via Playwright"
|
|
- "All assets are packaged into a ZIP via archiver with correct folder structure"
|
|
- "content-job-runner dispatches brand-kit jobType to brand-renderer"
|
|
artifacts:
|
|
- path: "server/src/services/renderers/brand-renderer.ts"
|
|
provides: "Brand kit orchestration renderer"
|
|
exports: ["renderBrandKit"]
|
|
- path: "server/src/__tests__/brand-renderer.test.ts"
|
|
provides: "Unit tests for brand renderer"
|
|
min_lines: 60
|
|
key_links:
|
|
- from: "server/src/services/content-job-runner.ts"
|
|
to: "server/src/services/renderers/brand-renderer.ts"
|
|
via: "dynamic import in renderContent switch"
|
|
pattern: 'case "brand-kit"'
|
|
- from: "server/src/services/renderers/brand-renderer.ts"
|
|
to: "archiver"
|
|
via: "import archiver for ZIP packaging"
|
|
pattern: "archiver"
|
|
- from: "server/src/services/renderers/brand-renderer.ts"
|
|
to: "server/src/services/renderers/diagram-renderer.js"
|
|
via: "resolveBrowserPath for Playwright PDF"
|
|
pattern: "resolveBrowserPath"
|
|
---
|
|
|
|
<objective>
|
|
Brand identity kit renderer that orchestrates logo, avatars, social images, templates, guidelines PDF, and ZIP packaging.
|
|
|
|
Purpose: Enable full brand identity generation from a single conversation — produces logo, avatars, social platform images, email signature, letterhead, guidelines PDF, and a downloadable ZIP of everything.
|
|
Output: Working brand-renderer.ts with job-runner wiring and tests.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/43-documents-branding/43-RESEARCH.md
|
|
@.planning/phases/43-documents-branding/43-01-SUMMARY.md
|
|
|
|
@server/src/services/renderers/types.ts
|
|
@server/src/services/content-job-runner.ts
|
|
@server/src/services/renderers/diagram-renderer.ts
|
|
@server/src/services/renderers/icon-renderer.ts
|
|
@server/src/services/puter-inference.ts
|
|
|
|
<interfaces>
|
|
<!-- Key types from Plan 01 output -->
|
|
|
|
From server/src/services/renderers/types.ts (after Plan 01):
|
|
```typescript
|
|
export interface BrandKitBundle {
|
|
type: "brand-kit-bundle";
|
|
spec: { name: string; tagline: string; primaryColor: string; secondaryColor: string; fontStyle: string; industry: string; };
|
|
logoSvgBase64: string;
|
|
avatarPngs: Record<string, string>; // "512"|"256"|"128"|"64"|"32" -> base64
|
|
socialImages: Record<string, string>; // "twitter-profile"|"twitter-banner"|"linkedin-profile"|"linkedin-banner"|"instagram-profile" -> base64
|
|
signatureHtml: string;
|
|
letterheadHtml: string;
|
|
guidelinesPdfBase64: string;
|
|
zipBase64: string;
|
|
}
|
|
export interface RenderResult { filename: string; contentType: string; buffer: Buffer; }
|
|
```
|
|
|
|
From server/src/services/renderers/diagram-renderer.ts:
|
|
```typescript
|
|
export function resolveBrowserPath(): string;
|
|
```
|
|
|
|
From server/src/services/renderers/icon-renderer.ts:
|
|
```typescript
|
|
export function validateAndCleanSvg(raw: string): { svg: string; warnings: string[] };
|
|
```
|
|
|
|
From server/src/services/puter-inference.ts:
|
|
```typescript
|
|
export async function puterChatComplete(messages: ChatMessage[], model?: string): Promise<string>;
|
|
```
|
|
|
|
From server/src/services/renderers/wallpaper-renderer.ts:
|
|
```typescript
|
|
export const PLATFORM_DIMENSIONS: Record<string, { width: number; height: number }>;
|
|
// Includes twitter-profile (400x400), twitter-banner (1500x500), linkedin-profile (400x400), linkedin-banner (1584x396), instagram-1080 (1080x1080)
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Create brand-renderer with orchestrated sub-renders and ZIP packaging</name>
|
|
<files>server/src/services/renderers/brand-renderer.ts, server/src/__tests__/brand-renderer.test.ts</files>
|
|
<read_first>
|
|
- server/src/services/renderers/types.ts (BrandKitBundle type from Plan 01)
|
|
- server/src/services/renderers/icon-renderer.ts (validateAndCleanSvg for SVG cleanup, LLM SVG prompt pattern)
|
|
- server/src/services/renderers/wallpaper-renderer.ts (PLATFORM_DIMENSIONS export, sharp usage for SVG-to-PNG, stripMarkdownFences)
|
|
- server/src/services/renderers/diagram-renderer.ts (resolveBrowserPath, Playwright launch + page.pdf pattern)
|
|
- server/src/services/puter-inference.ts (puterChatComplete)
|
|
- server/src/__tests__/diagram-renderer.test.ts (mock patterns)
|
|
</read_first>
|
|
<behavior>
|
|
- Test 1: renderBrandKit returns brand-kit-bundle with all required top-level fields present (spec, logoSvgBase64, avatarPngs, socialImages, signatureHtml, letterheadHtml, guidelinesPdfBase64, zipBase64)
|
|
- Test 2: spec contains name, tagline, primaryColor, secondaryColor, fontStyle, industry
|
|
- Test 3: avatarPngs has keys "512", "256", "128", "64", "32" — all non-empty base64 strings
|
|
- Test 4: socialImages has keys twitter-profile, twitter-banner, linkedin-profile, linkedin-banner, instagram-profile — all non-empty
|
|
- Test 5: signatureHtml and letterheadHtml are non-empty strings
|
|
- Test 6: guidelinesPdfBase64 is a non-empty string
|
|
- Test 7: zipBase64 decodes to a buffer starting with PK (0x50, 0x4B — ZIP magic bytes)
|
|
</behavior>
|
|
<action>
|
|
Create server/src/services/renderers/brand-renderer.ts with these internal functions:
|
|
|
|
**BrandSpec interface** (local, not exported — matches BrandKitBundle.spec shape):
|
|
```typescript
|
|
interface BrandSpec {
|
|
name: string; tagline: string; primaryColor: string; secondaryColor: string;
|
|
fontStyle: string; industry: string; logoDescription: string;
|
|
}
|
|
```
|
|
|
|
**extractBrandSpec(prompt)** — LLM call with system prompt instructing JSON output with the BrandSpec fields. Parse JSON from response (strip markdown fences first). If parsing fails, provide sensible defaults.
|
|
|
|
**generateLogoSvg(spec)** — LLM call asking for a simple, clean SVG logo mark based on spec.logoDescription, spec.primaryColor, spec.secondaryColor. System prompt: "Output ONLY valid SVG. No text explanations. Simple geometric shapes. viewBox 0 0 512 512. Use only the provided colors." Clean output with validateAndCleanSvg from icon-renderer.
|
|
|
|
**rasterizeAvatars(logoSvg)** — Use sharp to resize the SVG to [512, 256, 128, 64, 32]px PNG. Return Record<string, string> mapping size to base64. Use `sharp(Buffer.from(logoSvg)).resize(size, size).png().toBuffer()` for each size.
|
|
|
|
**generateSocialImages(spec, logoSvg)** — For each of 5 platforms (twitter-profile 400x400, twitter-banner 1500x500, linkedin-profile 400x400, linkedin-banner 1584x396, instagram-profile 1080x1080): create an SVG template with brand-colored background + centered logo, rasterize with sharp to the target dimensions. Return Record<string, string>.
|
|
|
|
**generateTemplates(spec)** — Two LLM calls:
|
|
1. Email signature HTML — system prompt: "Generate a professional HTML email signature with name, tagline, colors. Inline CSS only. No external resources. Keep under 200 lines."
|
|
2. Letterhead HTML — system prompt: "Generate a professional HTML letterhead template with header, footer, placeholder body. Inline CSS only. No external resources."
|
|
Strip markdown fences from both.
|
|
|
|
**generateGuidelinesPdf(spec, logoSvg)** — LLM generates a brand guidelines HTML page (system prompt: "Generate a brand guidelines document as HTML. Include: brand name, tagline, color palette with hex codes, typography guidance, logo usage rules, spacing guidelines. Inline CSS only. No external resources. Use web-safe fonts."). Render to PDF via Playwright (same pattern as pdf-renderer: resolveBrowserPath, chromium.launch, page.setContent, page.pdf A4). Return Buffer.
|
|
|
|
**buildZip(assets)** — Use archiver v7 to create an in-memory ZIP buffer with folder structure:
|
|
```
|
|
brand-kit/logo/logo.svg
|
|
brand-kit/logo/logo-512.png, logo-256.png, logo-128.png, logo-64.png, logo-32.png
|
|
brand-kit/social/twitter-profile.png, twitter-banner.png, linkedin-profile.png, linkedin-banner.png, instagram-profile.png
|
|
brand-kit/templates/email-signature.html, letterhead.html
|
|
brand-kit/guidelines.pdf
|
|
```
|
|
Use the streaming pattern from RESEARCH.md Pattern 3: Writable sink, archive.pipe(sink), archive.append(buffer, { name }), archive.finalize(), resolve on sink finish.
|
|
|
|
**renderBrandKit(input)** — Main exported function. Extract `prompt` from input. Call extractBrandSpec, generateLogoSvg, rasterizeAvatars, generateSocialImages, generateTemplates, generateGuidelinesPdf, buildZip in sequence. Assemble BrandKitBundle. Return RenderResult.
|
|
|
|
CRITICAL constraints:
|
|
- Open Playwright browser ONCE for the entire brand kit job (guidelines PDF), not per sub-step
|
|
- Use try/finally to close browser
|
|
- Social image SVGs are simple templates (colored rect + embedded logo), NOT LLM-generated — keeps them fast and predictable
|
|
- All base64 encoding via Buffer.from(x).toString("base64")
|
|
|
|
Create server/src/__tests__/brand-renderer.test.ts:
|
|
- vi.mock("playwright-core") — same pattern as pdf-renderer tests
|
|
- vi.mock("../services/puter-inference.js") — mock puterChatComplete to return different responses based on system prompt content (JSON for spec, SVG for logo, HTML for templates/guidelines)
|
|
- vi.mock("../services/renderers/diagram-renderer.js") — resolveBrowserPath returns "/fake/chromium"
|
|
- vi.mock("../services/renderers/icon-renderer.js") — validateAndCleanSvg returns { svg: input, warnings: [] }
|
|
- vi.mock("sharp") — mock sharp() chain: resize().png().toBuffer() returns a small Buffer
|
|
- vi.mock("archiver") — mock archiver() to return an object with append, pipe, finalize, on("error") that triggers the sink finish event
|
|
- Write tests per behavior section
|
|
</action>
|
|
<acceptance_criteria>
|
|
- grep -q "renderBrandKit" server/src/services/renderers/brand-renderer.ts
|
|
- grep -q "archiver" server/src/services/renderers/brand-renderer.ts
|
|
- grep -q "resolveBrowserPath" server/src/services/renderers/brand-renderer.ts
|
|
- grep -q "validateAndCleanSvg" server/src/services/renderers/brand-renderer.ts
|
|
- grep -q "brand-kit-bundle" server/src/services/renderers/brand-renderer.ts
|
|
- grep -q "zipBase64" server/src/services/renderers/brand-renderer.ts
|
|
</acceptance_criteria>
|
|
<verify>
|
|
<automated>pnpm --filter @paperclipai/server test --run src/__tests__/brand-renderer.test.ts</automated>
|
|
</verify>
|
|
<done>Brand renderer produces complete BrandKitBundle with logo SVG, 5 avatar sizes, 5 social images, email signature HTML, letterhead HTML, guidelines PDF, and ZIP package. All tests pass.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Wire brand-kit jobType into content-job-runner</name>
|
|
<files>server/src/services/content-job-runner.ts</files>
|
|
<read_first>
|
|
- server/src/services/content-job-runner.ts (current switch with pdf-document case from Plan 01)
|
|
</read_first>
|
|
<action>
|
|
Add a new case to the `renderContent()` switch in content-job-runner.ts, after the "pdf-document" case and before the `default` case:
|
|
|
|
```typescript
|
|
case "brand-kit": {
|
|
const { renderBrandKit } = await import("./renderers/brand-renderer.js");
|
|
return renderBrandKit(input);
|
|
}
|
|
```
|
|
|
|
Follow exact pattern of existing cases (dynamic import with .js extension, destructured named export).
|
|
</action>
|
|
<acceptance_criteria>
|
|
- grep -q '"brand-kit"' server/src/services/content-job-runner.ts
|
|
- grep -q 'renderBrandKit' server/src/services/content-job-runner.ts
|
|
</acceptance_criteria>
|
|
<verify>
|
|
<automated>pnpm --filter @paperclipai/server exec tsc --noEmit</automated>
|
|
</verify>
|
|
<done>content-job-runner dispatches "brand-kit" jobType to renderBrandKit. TypeScript compiles cleanly.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `pnpm --filter @paperclipai/server test --run src/__tests__/brand-renderer.test.ts` passes
|
|
- `pnpm --filter @paperclipai/server exec tsc --noEmit` compiles without errors
|
|
- `grep -q "brand-kit" server/src/services/content-job-runner.ts` succeeds
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- Brand renderer orchestrates 7 sub-steps: spec extraction, logo SVG, avatar rasterization, social images, templates, guidelines PDF, ZIP
|
|
- All BrandKitBundle fields populated with non-empty values
|
|
- ZIP contains correct folder structure with all assets
|
|
- Job runner dispatches brand-kit jobs correctly
|
|
- All tests pass, TypeScript clean
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/43-documents-branding/43-02-SUMMARY.md`
|
|
</output>
|