README
A local browser-based content production tool. Create branded social cards, slide decks, documents, and full cross-platform campaigns through an interactive preview with live reload and one-click export.
The skill runs a Node.js HTTP + WebSocket server. The coding agent starts the server, tells you the URL, writes HTML files to a project folder, and the browser app renders them as cards you can preview, iterate on, and export.
Table of contents
- What you can build
- Who this is for
- Prerequisites
- Quick start
- Core concepts
- User journeys
- Journey A — Make a quick single card
- Journey B — Make a slide deck
- Journey C — Make a document / blog post
- Journey D — Run a full cross-platform campaign
- Journey E — Edit a specific card you are looking at
- Journey F — Apply a brand
- Journey G — Export your content
- Journey H — Reopen past work
- Journey I — Promote a project to a reusable template
- Journey J — Handle anchor edits in a campaign
- App UI tour
- Project layout on disk
- Server API reference
- Templates — adding your own
- Brand skills — how they plug in
- URL-pinned tab state
- Live element editing
- Box Layout validation
- Marketing skills — soft integration
- Visual density rules
- Troubleshooting
What you can build
| Output | Format | Typical use |
|---|---|---|
| Social card — single image | 1:1 / 4:5 / 9:16 / OG 1200×630 | LinkedIn post, Instagram feed, Twitter card |
| Carousel — multiple linked cards | 4:5 or 9:16 | LinkedIn carousel, Instagram carousel, IG story set |
| Slide deck — 16:9 presentation | 1280×720 | Webinars, pitches, internal updates |
| Document — A4 pages | 794×1123 | Blog post, technical docs, guide, one-pager |
| Full campaign — one topic, many variants | All of the above | Launch post distilled into blog + LinkedIn + Instagram + TikTok + Twitter |
Every output is a self-contained HTML file you can also open in any browser.
Who this is for
- Solo creators who want branded social content without a design tool
- Founders shipping a launch across many platforms in one sitting
- Engineers who write technical docs and want shareable summaries for social
- Marketing teams iterating on carousels with an AI agent in the loop
If you only need a single image, use one of the quick-start journeys below. If you are publishing one topic across many platforms, use the campaign pipeline.
Prerequisites
| Dependency | Install | Purpose |
|---|---|---|
| Node.js 18+ | required | runs the server |
| Playwright (optional) | npx playwright install chromium | PNG and PDF export |
| A modern browser | required | opens the app |
The server has zero npm dependencies at runtime — scripts/server.cjs is a
self-contained bundle.
Quick start
Open your coding agent in any project directory and say:
“Start content factory and make me a LinkedIn carousel about my edge caching results.”
The agent will:
- Run
scripts/start-server.shand print the URL - Create a project folder under
.codi_output/ - Ask a couple of clarifying questions
- Write a cover slide + content slides + CTA slide as a single HTML file
- Tell you to open the URL and review the preview
Open the URL in your browser, go to the Preview tab, and iterate with the agent in chat. When you are happy, click Export PDF or Export PPTX in the sidebar.
To stop:
bash scripts/stop-server.sh <workspace_dir>
The agent will do this automatically if you say “done” or close the session.
Core concepts
Card
One rendered “page” inside a content file. Three HTML element types:
<article class="social-card">— social media card<article class="slide">— slide deck page<article class="doc-page">— A4 document page
Each card has data-type (cover / content / stat / quote / cta / closing) and
data-index (zero-padded: 01, 02, …). The app scans these attributes to build
the preview strip.
Content file
One HTML file that contains one or more cards plus a <meta name="codi:template">
tag describing the content (id, name, type, format). One file = one logical
unit of content (a carousel, a deck, or a document).
Project
A named folder under .codi_output/. Contains a content/ directory with one
or more content files, a state/ directory with session metadata, and an
exports/ directory for output. You can switch between projects via the
Gallery → My Work tab.
Brief (campaign mode only)
A brief.json file at the project root. Captures the campaign intake: topic,
audience, voice, brand, goal, CTA, anchor type, and the list of variants to
distill. Only created when you run the campaign pipeline.
Anchor (campaign mode only)
The long-form master file in a campaign. Always named 00-anchor-<type>.html
where <type> is blog, docs, or deck. Single source of truth — all
variants derive from it. Shown in the file list with an ANCHOR badge.
Variant (campaign mode only)
A platform-specific content file distilled from the anchor. Lives in the same
project with a numeric prefix (10-linkedin-carousel.html,
21-instagram-story.html, 30-tiktok-cover.html, …). Each variant carries a
<meta name="codi:variant"> tag that links it back to a specific anchor
revision.
Format
The canvas dimensions. Six presets: 1:1 (1080×1080), 4:5 (1080×1350), 9:16 (1080×1920), OG (1200×630), 16:9 (1280×720), A4 (794×1123). Social cards and slides adapt to the format you pick in the sidebar; documents always render at A4.
Preset / template
A stock HTML file in generators/templates/ that the app shows in the Gallery.
Click one to load it into the preview strip as a starting point for your own
content.
Brand
A separate Codi skill (any skill whose folder contains brand/tokens.json)
that ships colors, fonts, logo, voice, and optional Gallery templates.
Content Factory detects installed brand skills and lets you activate one per
project. The agent then applies the brand to every generated file.
User journeys
Each journey lists what you say to the agent, what the agent does, and what you do in the browser. All journeys assume the agent has already started the server and given you the URL.
Journey A — Make a quick single card
You want a one-off social card for an announcement. No brief, no distillation, just one image.
Say:
“Make me an Instagram post about our 2.0 release.”
The agent will:
- Create a project folder
- Ask 1-2 clarifying questions (headline, key points, CTA)
- Write
content/social.htmlwith onesocial-cardelement at 1:1 format - Tell you to open the Preview tab
You:
- Open the URL, Preview tab
- Review the card at full zoom
- Give feedback in chat: “make the headline larger”, “change the background”
- Each rewrite reloads the preview in under 200 ms via WebSocket
- When happy, click Export PNG in the sidebar
Journey B — Make a slide deck
You want a 16:9 slide deck for a talk or webinar.
Say:
“Build me a 10-slide deck about API caching strategies.”
The agent will:
- Create a project
- Ask audience, tone, and which sections to include
- Plan the slides (cover → agenda → content → takeaway → CTA)
- Write
content/slides.htmlwith ten<article class="slide">elements - Apply visual density rules so no slide ships with blank space
You:
- Open the Preview tab, click 16:9 in the format buttons if not already
- Click any slide to zoom in
- Use left / right arrow keys to navigate slides
- Give feedback per slide: “change slide 3 headline”, “add a stat to slide 5”
- Export: click Export PPTX for PowerPoint, Export PDF for a single PDF, or Export PNG for the current slide only
Journey C — Make a document / blog post
You want a multi-page document — a technical guide, a blog post, or a product one-pager.
Say:
“Write a technical guide about setting up edge caching with Cloudflare.”
The agent will:
- Create a project
- Ask how many pages, what sections, and whether to include code blocks
- Plan each A4 page to fit within the ~950px body budget
- Write
content/document.htmlwith one<article class="doc-page">per page - Include page headers, footers, callouts, data tables, and code blocks as needed
- Apply DOCX-compatible class conventions so export to Word works cleanly
You:
- Open the Preview tab, click A4 in the format buttons
- Scroll through the pages
- Give feedback: “add a FAQ page”, “move the code block to page 3”
- Export: Export PDF for a print-ready file, Export DOCX for Word/Google Docs (uses Playwright screenshots for code blocks and SVG diagrams)
Journey D — Run a full cross-platform campaign
You want to publish one topic across many platforms in a coordinated set. This is the most powerful journey — it runs the full anchor → distill pipeline.
Say one of:
“Create a campaign about how we cut API latency 80% with edge caching.”
“I want a blog post plus a LinkedIn carousel, Instagram story, and TikTok cover about our 2.0 launch.”
“Turn this topic into content for all the main social platforms.”
The agent will run five phases.
Phase 1 — Intake. The agent asks you six questions, one or two at a time:
- What is the core topic?
- What anchor fits best — blog, docs, or deck? (Agent proposes a default.)
- Who is the audience?
- What voice? (Inherits from your active brand if any.)
- Which platforms do you want to generate for? (Checklist — default is LinkedIn carousel + Instagram feed.)
- What is the CTA or goal?
The agent writes brief.json, shows you a summary, and waits for you to reply
“go” before doing anything else.
Phase 2 — Anchor generation. The agent writes only the anchor file first
(content/00-anchor-blog.html, or 00-anchor-docs.html, or
00-anchor-deck.html). You iterate with the agent until the master is good.
Say “approve” or “looks good” to mark the anchor as approved — this unlocks
distillation.
Phase 3 — Distillation. The agent loops over your chosen platforms and writes one file per platform, serially:
10-linkedin-carousel.html11-linkedin-post.html20-instagram-feed.html21-instagram-story.html30-tiktok-cover.html(plus a video script block)40-twitter-card.html(plus a thread block)50-summary-deck.html
Each variant follows the per-platform rules in
references/platform-rules.md — hook length, slide count, safe areas,
hashtag strategy, CTA placement. Each file also carries a codi:variant meta
tag linking back to the anchor revision it was distilled from.
Phase 4 — Per-file iteration. Open any file in the Preview tab, give feedback, see updates in real time. Works the same as the single-file journeys.
Phase 5 — Edit propagation. When you edit the anchor, the agent bumps
anchor.revision in brief.json. The next time you message the agent, it
checks whether any variants are now out of date and asks:
“The anchor changed since I distilled these variants:
- 10-linkedin-carousel.html (was rev 1, now rev 3)
- 20-instagram-feed.html (was rev 1, now rev 3)
Should I re-distill them? (all / name a file / skip)”
Choose all to re-distill everything, name a single file, or skip to keep
the variants as-is. A “skip” is sticky — the same prompt will not fire again
for the same anchor revision.
When the whole campaign is ready, export each file individually from the
sidebar, or ask the agent to export everything into exports/.
Journey E — Edit a specific card you are looking at
You want to change “this slide” without naming its number.
The app automatically tracks which card you clicked (or navigated to with
arrow keys) and reports it to the server. When you say “change this”, the
agent reads /api/state, finds the activeCard entry, and edits only the
matching element by its data-index.
Say, while looking at a specific card:
“Make this headline shorter and bolder.” “Change the background color on this page.” “Add a bullet to the card I am viewing.”
The agent:
- Reads
/api/stateand confirms which card is active - Reads the current HTML
- Finds the element with the matching
data-index - Rewrites only that element
- Confirms with “Updated slide 3 of 7 (stat card) — new value is …”
If you name a slide explicitly (“change slide 5”), the agent uses your number. The “active card” mechanism only kicks in for deictic language (this, here, the one I am on).
Journey F — Apply a brand
You want your content to use your company’s colors, fonts, logo, and voice.
Prerequisite: a brand skill is installed in your project (any skill folder
with brand/tokens.json). Use the codi-brand-creator skill to build one if
you do not have one yet.
Say, at the start of a session:
“Use the Codi brand for this content.”
The agent will:
- Call
GET /api/brandsto list available brand skills - Call
POST /api/active-brandwith your choice - Read
tokens.json(colors, fonts, logo paths, voice tone) - Read
tokens.cssand inline it into every generated HTML file - Load Google Fonts via
<link>or generate@font-faceblocks for local fonts (served at/api/brand/<name>/assets/fonts/…) - Fetch the logo SVG via
/api/brand/<name>/assets/<logo>and inline it - Open the brand’s
references/directory to learn the brand’s layout and component patterns - Write copy using
voice.tone,voice.phrases_use, and avoidingvoice.phrases_avoid
The brand stays active for the rest of the session. Every file the agent writes after activation inherits the brand automatically.
Journey G — Export your content
Exports are context-aware — the sidebar button set changes based on the type of content you have open:
| Content type | Buttons shown | Default |
|---|---|---|
| Social card | Export PNG (current), Export PDF (all) | PNG |
| Slide deck | Export PPTX (all), Export PDF (all), Export PNG (current) | PPTX |
| Document | Export PDF (all), Export DOCX (all), Export PNG (current) |
PNG uses Playwright at 2× resolution for crisp output. PDF renders slides to a multi-page PDF server-side via Playwright. PPTX embeds PNG screenshots of each slide via PptxGenJS — preserves fonts and layout exactly as you see them. DOCX captures text with Pandoc and renders code blocks / SVG diagrams as Playwright screenshots so they survive the Word export.
All exports land in <projectDir>/exports/.
Journey H — Reopen past work
You want to keep iterating on a project you started yesterday.
- Open the URL → Gallery tab
- Click the My Work filter
- You see a grid of past project cards — each shows the project date, preset name, and a live thumbnail of the first content file
- Click any card to activate that project; the server loads its content files and switches the sidebar file list to that project
- Click any file in the sidebar to open it in the Preview tab
- Keep iterating — the agent will detect the project via
/api/stateand edit the existing file in place (not create a new one)
Journey I — Promote a project to a reusable template
You want to save a project as a template so future sessions can start from it.
Say:
“Save this project as a new template called ‘edge-caching-report’.”
The agent will:
- Confirm the template name
- Copy the content file to
generators/templates/<name>.htmlin both the installed skill and the source - Update the
<meta name="codi:template">tag with a clean id and name - Broadcast a
reload-templatesevent — the Gallery refreshes within 150ms without a page reload; the new template appears inline under the matching type filter (Social / Slides / Document) - Confirm that the template is now selectable from the Gallery for all future sessions
If you want to share the template with others, ask the agent to “contribute this template upstream” and it will package it for a PR or ZIP export.
Journey J — Handle anchor edits in a campaign
You want to make a change to the long-form anchor and keep the variants in sync.
- Open
00-anchor-blog.html(or whichever anchor you have) - Edit it with the agent — “Add a fifth section about cache warming”
- The agent rewrites the anchor and silently bumps
anchor.revision - Send any next message to the agent — e.g. “change the LinkedIn cover headline”
- Before applying the new change, the agent detects stale variants and
asks:
“The anchor changed since I distilled 10-linkedin-carousel.html and 21-instagram-story.html — should I re-distill? (all / name a file / skip)”
- Pick your option; the agent re-distills the selected variants from the new anchor revision, then applies your new feedback
- If you pick
skip, those variants get markedstatus: "manual"inbrief.jsonso the same prompt never fires again for that revision
App UI tour
Sidebar (left, scrollable)
| Control | What it does |
|---|---|
| Format | Six buttons: 1:1, 4:5, 9:16, OG, 16:9, A4. Switches the canvas dimensions for all cards. Documents always render at A4. |
| Handle | @username placeholder. The agent replaces @handle in generated content with your value. |
| Zoom | 15% - 120% slider. Scales the preview cards. Default 40%. |
| Logo | ON/OFF toggle plus size and X/Y sliders. Adds a logo overlay to every card. |
| Content files | List of HTML files in the active project. Anchor files show an ANCHOR badge. Click to load. |
| Export | Context-aware export buttons (see Journey G). |
| Activity log | Timestamped server events and WebSocket status. Green dot = connected. |
Main area
Preview tab — horizontal card strip. Click any card to select it. Arrow
keys navigate. The active card is highlighted and reported to the server via
/api/active-card so the agent knows which card you are looking at. A
metadata bar above the canvas shows the content name, type, format, and slide
count.
Gallery tab — preset browser. Five filters:
- All — every built-in template and every installed brand skill template, side by side. Past projects are hidden from this view.
- Social — templates whose
typemeta issocial(cards, carousels, stories) - Slides — templates whose
typemeta isslides(16:9 decks) - Document — templates whose
typemeta isdocument(A4 pages) - My Work — past projects from
.codi_output/, most recent first. When active, a secondary status row appears (All / Draft / In Progress / Review / Done) for filtering by project status.
Stock templates (from generators/templates/) and brand templates (from any installed brand skill’s templates/ folder) land in the same flat list — the Gallery does not separate them into their own tab. Each card’s type decides which filter it appears under. Click any card to load it into Preview.
Project layout on disk
A single-file project:
.codi_output/
my-quick-post/
content/
social.html
state/
active.json # which file is open
active-card.json # which card is selected
preset.json # which template was picked
manifest.json # project metadata + status
exports/ # PNG / PDF / PPTX / DOCX output lands here
A campaign project:
.codi_output/
edge-caching-launch/
brief.json # intake answers + variants + pipeline state
content/
00-anchor-blog.html # master (document, A4)
10-linkedin-carousel.html
11-linkedin-post.html
20-instagram-feed.html
21-instagram-story.html
30-tiktok-cover.html
40-twitter-card.html
50-summary-deck.html
state/
exports/
Numeric prefixes give natural sort order in the app’s file list:
00- anchor first, then 10-19 LinkedIn, 20-29 Instagram, 30-39 TikTok,
40-49 Twitter, 50-59 decks, 60-69 email/ads/other.
Server API reference
All endpoints run on the same port as the web app. Routes are grouped by concern.
App assets
| Route | Method | Purpose |
|---|---|---|
/ | GET | Serve the web app HTML shell |
/static/* | GET | Serve app.css, app.js |
/vendor/* | GET | Serve html2canvas, jszip |
Projects and sessions
| Route | Method | Purpose |
|---|---|---|
/api/create-project | POST | Create and activate a new project — body {name, type}. type is required and must be one of social, slides, document. Returns {projectDir, contentDir, stateDir, exportsDir} |
/api/open-project | POST | Activate an existing project — body {projectDir} |
/api/sessions | GET | List all projects in the workspace |
/api/session-status | POST | Persist project status — body {sessionDir, status} where status is draft, in-progress, review, or done |
Files and content
| Route | Method | Purpose |
|---|---|---|
/api/files | GET | List HTML files in the active project’s content/ |
/api/content?file=X | GET | Return raw HTML for a content file |
/api/session-content?session=&file= | GET | Serve a file from a specific project |
/api/content-metadata?kind=&id= | GET | Unified descriptor for templates and sessions: {kind, id, name, type, format, cardCount, status, createdAt, modifiedAt, readOnly, source}. readOnly=true for built-in templates |
/api/content-list | GET | Debug/utility — every content descriptor the server knows about, templates and sessions merged |
/api/clone-template-to-session | POST | Copy a built-in template into a new editable session — body {templateId, name?}. Use before applying any persist-style edit when the content is a template |
State and selection
| Route | Method | Purpose |
|---|---|---|
/api/state | GET | Aggregate state: {mode, contentId, activeFile, activeFilePath, activePreset, activeSessionDir, status, activeCard, brief, activeBrand}. mode is template, mywork, or null. Use contentId and activeFilePath as the authoritative identifiers — never reconstruct paths from name fragments |
/api/active-file | GET/POST | Which file is currently loaded |
/api/active-card | GET | The card currently highlighted in Preview: {index, total, dataType, dataIdx, file, timestamp} |
/api/active-card | POST | App-only — the browser posts this when you click a card or use arrow keys |
/api/preset | GET/POST | Which Gallery preset was picked — {id, name, type, timestamp} |
Live inspection
| Route | Method | Purpose |
|---|---|---|
/api/active-element | GET | The DOM element the user most recently clicked in the preview — full context (selector, tag, id, classes, attributes, text, outerHTML snippet, bounding rect, computed styles, parent chain, and a context field carrying {kind, id, name, file, cardIndex, readOnly}). null if no click yet |
/api/active-elements | GET | Multi-select set of Cmd/Ctrl-clicked elements — {count, selections:[...]} |
/api/active-elements | DELETE | Clear the multi-select set |
/api/inspect-events?since=<seq> | GET | Ring buffer of preview interactions (clicks, inputs, submits, scrolls). Poll with ?since=<lastSeq> for incremental updates |
/api/eval | POST | Run JavaScript inside the currently-previewed HTML page — body {js, timeoutMs?}. Returns {ok, result, error}. Ephemeral — changes revert on reload. Disable with env CONTENT_FACTORY_ALLOW_EVAL=0 |
Style persistence
| Route | Method | Purpose |
|---|---|---|
/api/persist-style | POST | Persist a style edit to the card source file — body {targetSelector, patches}. The server assigns a stable data-cf-id, writes it into the HTML, and upserts a CSS rule in a bounded /* === cf:user-edits === */ region. Returns 409 with a cloneSuggestion payload when the target is read-only (template). Idempotent: re-applying the same edit is a no-op |
/api/persist-style | DELETE | Revert a persisted edit — query ?cfId=<id>&project=<dir>&file=<basename>. Removes the rule and strips the data-cf-id attribute if no other rule references it |
/api/persist-style | GET | List persisted edits for a card — query ?project=<dir>&file=<basename>. Returns {count, rules:[{selector, declarations:[...]}]} |
Campaign brief and anchor revisions
| Route | Method | Purpose |
|---|---|---|
/api/brief | GET | Return the active project’s brief.json or null |
/api/brief | POST | Write the brief — body is an arbitrary JSON object (no schema enforcement). Returns 400 if no project is active |
/api/distill-status | GET | Anchor revision and per-variant staleness: {anchor:{file,revision,status}, variants:[{file,format,derivedFromRevision,status,staleBy}], stale:[files]}. Use at the start of every iteration turn to detect stale variants |
/api/anchor/revise | POST | Bump brief.anchor.revision and mark variants with derivedFromRevision < new revision as status: "stale". Optional body {reason?} |
/api/anchor/approve | POST | Set brief.anchor.status = "approved", record approvedAt. Idempotent. Call only when the user explicitly approves the anchor |
Variant metadata uses camelCase throughout — derivedFromRevision, derivedFrom, createdAt. Older references may show snake_case; camelCase is authoritative.
Box Layout validation
| Route | Method | Purpose |
|---|---|---|
/api/validate-card | POST | Validate one card — body {project, file, cardIndex, force?}. Returns {ok, pass, score, violations:[{rule, severity, path, message, fix}], summary, fixInstructions}. Cached by SHA-1 of HTML + dimensions + preset |
/api/validate-cards?project=&file= | GET | Batch validate every card in a file — {ok, pass, cards:[...], failingCards:[...]} |
/api/validation-config?project=<dir>[&file=<basename>] | GET | Resolved config cascade with source map showing which scope produced each field. Cascade: type-default → user default → session → per-file |
/api/validation-config | PATCH | Merge a partial patch — body `{project |
/api/validation-config/toggle | POST | Flip a layer on or off — body {project, layer, value}. Layers: all (master), endpoint, badge, agentDiscipline, exportPreflight, statusGate |
/api/validation-config/ignore-violation | POST | Add a per-file exemption — body {project, file, rule, selector?, cardIndex?} |
/api/validator-health | GET | {degraded, workers, cacheSize, cacheHits, cacheMisses, avgLatencyMs, lastError}. degraded: true means Playwright is missing and all layers default to pass |
Templates
| Route | Method | Purpose |
|---|---|---|
/api/templates | GET | List all stock and brand templates with metadata |
/api/template?file=X[&brand=Y] | GET | Serve a single template HTML file |
Brands
| Route | Method | Purpose |
|---|---|---|
/api/brands | GET | List installed brand skills (those with brand/tokens.json) |
/api/active-brand | POST | Set or clear the active brand — body {name} or {} to clear |
/api/brand/:name/assets/* | GET | Serve a file from a brand skill’s assets/ — use these URLs for logos and fonts in generated HTML |
Export
| Route | Method | Purpose |
|---|---|---|
/api/export-png | POST | Render a card to 2× PNG via Playwright — body {html, width, height} |
/api/export-pdf | POST | Render slides to a multi-page PDF — body {slides:[{html,width,height}]} |
PPTX and DOCX export run in the browser via PptxGenJS and client-side Pandoc — no dedicated server endpoints. PNG screenshots for PPTX slides and DOCX figures still route through /api/export-png.
WebSocket
The server runs a WebSocket endpoint at the same port. The browser app connects automatically and receives:
{type: "reload"}whenever a content file changes — triggers a live update{type: "reload-templates"}whenever a template file changes — refreshes the Gallery without a page reload
Templates — adding your own
Drop a .html file in generators/templates/. It must include a
<meta name="codi:template"> tag in <head>:
<meta name="codi:template" content='{"id":"my-template","name":"My Template","type":"social","format":{"w":1080,"h":1080}}'>
Required fields: id (kebab-case), name (human-readable), type
(social / slides / document), format (width + height in pixels).
The template watcher picks up the new file within 150 ms and pushes a
reload-templates event — your template appears in the Gallery without
restarting the server.
See references/visual-density.md and the main skill workflow for the card
structure rules.
Brand skills — how they plug in
Any skill folder that contains brand/tokens.json is detected as a brand.
The agent discovers brands via GET /api/brands, activates one via
POST /api/active-brand, and applies it to every generated file:
- Inlines
brand/tokens.cssas a<style>block - Loads fonts from
tokens.json.fonts.google_fonts_urlOR fromassets/fonts/via@font-faceblocks pointing at/api/brand/<name>/assets/fonts/<file> - Inlines the logo SVG from
assets/<logo-file>based on card background - Reads the brand’s
references/directory for layout guidance - If the brand has a
templates/folder, those templates appear in the Gallery alongside the built-in ones, filtered by each template’stype(Social / Slides / Document). They carry abrandtag on their metadata so the agent knows which skill they come from. - Writes copy using
voice.tone,voice.phrases_use, and avoidingvoice.phrases_avoid
Use the codi-brand-creator skill to build a brand package.
Logo discovery
Every content project loads its overlay logo from the canonical path:
.codi_output/<project>/assets/logo.svg
The browser app requests it via GET /api/project/logo. The server runs a
three-step fallback chain:
<project>/assets/logo.svg— the project’s own logo (wins when present)<active-brand>/brand/assets/logo.svg— the active brand skill’s logo- Built-in
codimark — last resort
The first time the factory needs a logo for a project that has none, it copies the active brand’s logo to the canonical project path. From that moment the project owns the file — subsequent edits to the brand skill do not retroactively flow into existing projects. This keeps projects portable (zipping one ships its identity with it) and predictable (the convention path is always where the logo lives).
Both the in-page preview overlay and exported HTML inline the resolved
SVG, so exports are self-contained (no external <img src>). The overlay
size tracks the inspector’s size slider; see Logo defaults below for the
format-derived starting value.
Content fit
Canvas overflow is emitted as rule R11 “Canvas Fit” by the standard
box-layout validator. The agent catches it in the same
/api/validate-cards loop that runs R1–R10 — no separate report file, no
parallel notification channel.
{
"rule": "R11",
"severity": "error",
"path": "body > div.doc-container > section.doc-page[0]",
"message": "Canvas overflow on .doc-page — content is 287px larger than 794x1123 (25.6%)",
"fix": "paginate: Page exceeds 794x1123 by 287px (25.6%). Add a new .doc-page sibling after this one and move overflow content into it. Preserve the existing header and footer on every page.",
"remediation": "paginate",
"overflowPx": 287,
"overflowPct": 25.6,
"canvasType": "document"
}
The remediation is content-type aware:
| Type | Overflow > 15% | Overflow ≤ 15% |
|---|---|---|
document | paginate (add a new .doc-page sibling) | tighten |
slides | split into multiple slides at the next section break | tighten |
social | tighten (single canvas, no pagination) | tighten |
Pagination contract — a multi-page document is a sequence of sibling
.doc-page elements inside .doc-container. Each .doc-page is its own
canvas (e.g. 794×1123 for A4) and ships its own header and footer. The
validator measures per page, not the whole document; adding pages
legitimately resolves overflow only when every page fits.
The canvas-root overflow: hidden that templates ship for export is
overridden in preview by an injected stylesheet
(scripts/lib/injector.cjs), so authors see overflow while editing
instead of silent clipping. Exports still clip per the template’s own CSS.
Rule source: scripts/lib/box-layout/rules/r11-canvas-fit.cjs.
Logo defaults
The overlay logo size defaults to 20% of the active canvas’s shortest side, positioned at top-right (x=85%, y=15%):
| Format | Canvas | Default size |
|---|---|---|
| Document (A4) | 794 × 1123 | 159 px |
| Social (square) | 1080 × 1080 | 216 px |
| Slides (16:9) | 1280 × 720 | 144 px |
Switching the active format recomputes the size automatically — until the
user moves the size slider, at which point the flag logo.userOverridden
flips to true and the user’s value sticks across future format changes.
URL-pinned tab state
Every preview tab is addressable by URL. Reloads land on exactly the same project, file, and card. Two tabs with different URLs show independent states. The agent can construct a URL directly and send it to the user for deep-linking.
| Param | Meaning |
|---|---|
kind | template or session |
id | Stable content id (template id or session dir basename) |
file | Content file basename (e.g. social.html) |
card | Active card index, 0-based (default 0) |
Example (template):
http://localhost:PORT/?kind=template&id=linkedin-carousel-concept-story&file=social.html&card=2
Example (session):
http://localhost:PORT/?kind=session&id=my-campaign-oct&file=social.html&card=0
The URL is the single source of truth for a tab. Legacy ?project= and
?preset= parameters are honored for one release for existing bookmarks.
Live element editing
Click any element in the preview. The agent can read what you clicked, propose a change, and persist it to the card source file. Edits survive reloads, regeneration, and exports.
The edit flow:
- Click the target element in the preview.
Cmd/Ctrl-click to build a multi-select set for a batch operation. - Agent reads the selection via
GET /api/active-element(or/api/active-elementsfor multi-select). The response carries acontextfield with{kind, id, name, file, cardIndex, readOnly}. - If
context.readOnlyistrue, the selection came from a built-in template. The agent callsPOST /api/clone-template-to-sessionwith{templateId: context.templateId}to create an editable copy in My Work, then loads it via?kind=session&id=<newId>&file=<basename>and asks you to re-click the element. - Persist the edit via
POST /api/persist-stylewith{targetSelector, patches}. The server writes a stabledata-cf-idinto the HTML and upserts a CSS rule in a bounded/* === cf:user-edits === */region of the card’s<style>block. - Optional live preview via
POST /api/evalfor instant visual feedback before the next render cycle.
Revert an edit with DELETE /api/persist-style?cfId=<id>&project=<dir>&file=<basename>.
The rule and the data-cf-id attribute both go away cleanly.
List all persisted edits on a card with GET /api/persist-style?project=<dir>&file=<basename>.
Edits are byte-additive: everything outside the user-edits region is untouched. Idempotent: re-applying the same edit is a no-op.
Box Layout validation
Every generated card passes through the vendored Box Layout validator before the agent ships it. Five layers enforce spacing, hierarchy, and structural consistency:
| Layer | Purpose |
|---|---|
| L1 | Primitive validation via POST /api/validate-card |
| L2 | Pass/fail score badges on preview cards |
| L3 | Agent discipline — every persist-style write triggers a validate-and-fix loop |
| L4 | Export preflight — blocks export of cards below threshold |
| L5 | Session-status gate — blocks done status when any card fails |
Default thresholds:
- Slides and documents: strict, score ≥ 0.9
- Social cards: lenient, score ≥ 0.8
Install the validator once per machine:
bash scripts/setup-validation.sh
If Playwright is not installed, GET /api/validator-health reports
degraded: true and all layers silently default to pass. The skill still
works, but without validation feedback.
Toggle any layer on or off with POST /api/validation-config/toggle.
Override the threshold per session or globally with
PATCH /api/validation-config. Exempt a specific violation with
POST /api/validation-config/ignore-violation.
Marketing skills — soft integration
Content Factory softly uses the external marketing-skills plugin if it is
installed. If not, the workflow and outputs are identical — the only
difference is whether extra LLM passes refine the copy.
| Phase | Skill invoked if present |
|---|---|
| Intake | content-strategy — validate topic and audience fit |
| Anchor (blog / docs) | copywriting → humanizer |
| Anchor (deck) | launch-strategy (if present) → humanizer |
| Distillation | social-content — one call per platform → humanizer |
| Optional | ad-creative, email-sequence for paid or email variants |
If you want to force inline generation even when the plugin is installed, say “do not use marketing skills for this campaign” — the agent will skip detection.
Codi will eventually ship its own first-party marketing skills to replace this external dependency. When that lands, the soft-detect pattern means you get the benefit automatically with no workflow change.
Visual density rules
Every generated card must visibly occupy ≥85% of its canvas with
purposeful content. No single centered headlines floating on blank space.
Full rules live in references/visual-density.md, but the key ideas are:
- Mentally draw a 3×3 grid over the card; at least 7 of the 9 cells must contain content or a purposeful decorative element
- Use two or more of these fill techniques per card: grid/multi-column layouts, supporting visual elements (eyebrow, stat row, meta strip), decorative accents (gradients, shapes, large outlined numerals), code/data mockups for technical content, edge-anchored chrome (brand mark, page number, handle, CTA arrow), oversized typography, background imagery or gradient fills
- Per card type, minimum elements are enforced — e.g. a social cover needs headline + sub-headline + eyebrow + brand mark + handle + one accent
If the agent ever generates a card with too much blank space, say “this is too empty” and it will rewrite the card applying the density rules.
Troubleshooting
The browser app cannot reach the server
- Check the activity log in the sidebar — the WebSocket dot should be green
- Restart the server:
bash scripts/stop-server.sh <workspace>thenbash scripts/start-server.sh --project-dir . - Check that the port from the startup JSON is not blocked by a firewall
Cards look clipped or have content falling off the edge
- The preview shows overflow (it used to clip silently) — content visibly extends past the canvas when it doesn’t fit
- Run
GET /api/validate-cards?project=<dir>&file=<file>— any R11 “Canvas Fit” violation names the overflowing page and prescribes the remediation (paginate,split, ortighten) in thefixfield - For documents, either tighten the layout or add a new
.doc-pagesibling - For slides, split at the next natural section break
- Exports still clip per each template’s own CSS — the relaxation applies to the in-app preview only, so overflow is visible at authoring time
Fonts look wrong
- If a brand is active and uses Google Fonts, make sure your machine has internet access — Google Fonts are loaded from the web inside iframes
- If the brand ships local fonts, check that
/api/brand/<name>/assets/fonts/<file>returns 200 in your browser - Never use
<link href="file://…">— iframes blockfile://URLs
PNG export produces a blank image
- Install Playwright Chromium:
npx playwright install chromium - Check that the card has non-zero dimensions in the preview
- Try exporting from the sidebar “Export PNG” button; it uses Playwright server-side at 2× resolution
Validator says degraded: true or scores never appear
- Run
bash scripts/setup-validation.shonce — it installs Playwright Chromium and the validator’s own dependencies - Check
GET /api/validator-health— ifdegraded: true, Playwright is missing and every validation layer silently passes - If install succeeds but scores still don’t render, check the sidebar activity log for worker crashes and restart the server
DOCX export is missing code blocks or diagrams
- Make sure
.code-block,pre, and.diagram-wrapuseoverflow: visible—overflow: hiddenclips content and corrupts the screenshot capture - Tables must include
table-layout: fixedin CSS to render correctly in both the preview and the exported DOCX
The file list shows an anchor badge but clicking does nothing
- Check that
brief.jsonexists at the project root - Check that
brief.anchor.filematches a file incontent/ - Run
curl -s <url>/api/briefto verify the brief is readable
The agent keeps asking to re-distill variants after I said “skip”
- The
skipanswer marks variants asstatus: "manual"inbrief.jsonfor the current anchor revision. If the anchor changes again, the check fires again for the new revision — this is intentional - To permanently stop propagation for a variant, edit
brief.jsonand remove the variant fromvariants[]
What’s next
- Campaign export bundle — single ZIP with manifest listing each variant
- Gallery “Campaigns” filter — group projects with
brief.jsonand show mini-thumbnails per platform - Codi-native marketing skills — first-party replacement for the external
marketing-skillsplugin - Multi-card selection in the preview strip — right now
activeCardtracks one card at a time
SKILL.md
Non-negotiable rules
- The brand logo lives in
<brand-skill>/assets/. Preferred name islogo.svg/logo.png, but the resolver also accepts themed variants (logo-light.svg,logo-dark.svg,logo-black.svg) and any file whose basename contains “logo” (e.g.bbva-logo.svg). All conform — no auto-fix needed. If the brand ships the logo OUTSIDEassets/, fix the brand or ask the user — do not build workarounds in content code. Full convention + pre-flight decision tree:${CLAUDE_SKILL_DIR}[[/references/logo-convention.md]] - Never embed the brand logo inside content HTML (
<img>, background-image, inline SVG). Content Factory renders the brand logo as an overlay on every card, automatically sized to the canvas (≈20% of the shortest side, top-right by default) and positioned/scaled via the inspector. Embedding a second logo in the HTML duplicates the mark on export and desyncs when the brand changes. Author content with chrome only (title bars, eyebrows, accent bars) — the factory adds the logo. - Slide decks are animated, single-file HTML. Always. Every
.slidedeck ships as one self-contained HTML file with all CSS,@keyframes, fonts, and per-slide inline<script>bundled in. No siblingdeck.css/deck.js. Motion is deliberate: staggered entry animations, compositor-onlytransform/opacity,@media (prefers-reduced-motion: reduce)honored, final state always visible. Quality floor: premium, modern, brand-aligned. HTML export is byte-for-byte — what you author is exactly what downloads. Full brief:${CLAUDE_SKILL_DIR}[[/references/slide-deck-engine.md]]. Read it before writing any deck. - Slide decks MUST bundle dual-mode presentation. Every deck must open
standalone (double-click the downloaded
.html) as a Google-Slides-style fullscreen presentation with keyboard (← → Space PageUp PageDown Home End) and click navigation, viewport-fit scaling, animation replay on every slide change, and a bottom-right page counter. Vertical scrolling through stacked slides is a defect in standalone mode. The pattern is dual-mode — base CSS stacks (Content Factory preview / thumbnails / Playwright export see stacked), a head<script>adds.js-presentationwhich switches CSS into fullscreen horizontal. Content Factory drops the top-level script during extraction, so preview stays stacked. All four pieces (head hook, presentation CSS, end-of-body driver, page counter element) are required. Reference implementation:${CLAUDE_SKILL_DIR}[[/references/slide-deck-engine.md]]§ 2.7 and § 5.2. - Run validation after every content write; fix every violation before declaring done.
Call
GET /api/validate-cards?project=<dir>&file=<file>and iterate on the returnedviolations[]until the report is clean (valid: true). Canvas overflow (rule: R11, “Canvas Fit”) is a standard validation violation alongside the box-layout rules — itsfixfield prescribespaginate/split/tightenwith the exact overflow numbers. Full protocol, including the content-fit remediation decision tree, lives at${CLAUDE_SKILL_DIR}[[/references/content-fit.md]].
Skip When
- User wants a single static poster, album cover, or museum-quality art piece — use codi-canvas-design
- User wants a generative / interactive p5.js sketch — use codi-algorithmic-art
- User wants to edit a
.pptxdirectly (binary format) — use codi-pptx - User wants a Word document with tracked changes — use codi-docx
- User wants a multi-component React artifact — use codi-claude-artifacts-builder
Overview
Content Factory is a standalone browser-based production tool for creating social media carousels, slide decks, and documents. It runs as a local web server that the agent starts, then the user opens in their browser to interact with a live preview and export interface.
The tool has two sides:
- Gallery — a library of built-in style presets, each with a full deck of rendered slides. Click any preset to load all its slides into Preview.
- Preview — a scrollable card strip showing all slides at the selected zoom level, with sidebar controls for format, handle, zoom, and logo overlay. Export any slide as PNG or all slides as a ZIP.
The agent’s job is to start the server, tell the user the URL, then generate content HTML files that the app picks up automatically via WebSocket.
Terminology
These terms appear throughout the skill and references. They are stable — use them consistently.
| Term | Meaning |
|---|---|
| Project / Session | Same thing. A named directory under .codi_output/ containing one or more content files plus state. The HTTP API uses session in URL params and path names (e.g. sessionDir, /api/sessions); the workflow prose uses project. When reading the code, both refer to the same on-disk folder |
| Content file | One HTML file in a project’s content/ directory. Carries a <meta name="codi:template"> tag and one or more cards. One file = one logical content unit (a carousel, a deck, a document) |
| Card | One rendered page inside a content file. Three element types: .social-card, .slide, .doc-page. The app scanner only recognizes these three class names |
| Preset / Template | Same thing at the Gallery level. A “preset” is what the user picks from the Gallery. A “template” is the underlying HTML file in generators/templates/ (built-in) or a brand skill’s templates/ directory |
| Anchor | The long-form master file in an anchor-first flow. Always Markdown, always at content/00-anchor.md. Rendered in the preview as a styled A4 document; distillation reads the Markdown sections (H1/H2/blockquotes/lists) to produce variants |
| Variant | A platform-specific content file distilled from an anchor. Carries a <meta name="codi:variant"> tag linking back to the anchor’s revision |
| Brief | A project’s brief.json — arbitrary JSON (no schema enforcement) capturing intake, anchor revision, and variant registry |
Skill Assets
| Asset | Purpose |
|---|---|
${CLAUDE_SKILL_DIR}[[/scripts/server.cjs]] | Node.js HTTP + WebSocket server. Minimal deps: docx (DOCX export), optional playwright (box-layout validation). See scripts/package.json. |
${CLAUDE_SKILL_DIR}[[/scripts/start-server.sh]] | Start the server, outputs JSON with URL and paths |
${CLAUDE_SKILL_DIR}[[/scripts/stop-server.sh]] | Stop the server gracefully |
${CLAUDE_SKILL_DIR}[[/scripts/setup-validation.sh]] | Install Playwright and validator deps (first-time) |
${CLAUDE_SKILL_DIR}[[/generators/app.html]] | App shell — always served at / |
${CLAUDE_SKILL_DIR}[[/generators/app.css]] | App styles — served at /static/app.css |
${CLAUDE_SKILL_DIR}[[/generators/app.js]] | App logic — served at /static/app.js |
${CLAUDE_SKILL_DIR}[[/generators/social-base.html]] | HTML template for agent-generated social cards |
${CLAUDE_SKILL_DIR}[[/generators/document-base.html]] | HTML template for agent-generated documents |
${CLAUDE_SKILL_DIR}[[/generators/templates/]] | Stock template HTML files — appear in the Gallery under All / Social / Slides / Document filters |
${CLAUDE_SKILL_DIR}[[/references/operating-system.md]] | MUST-READ first. The Content Factory operating philosophy — the six-phase validation-first workflow (Discovery → Master → Validation → Planning → Validation → Generation). Describes WHO the skill is and HOW it operates before any mechanics. Every other reference is the HOW behind this WHY |
${CLAUDE_SKILL_DIR}[[/references/server-api.md]] | Full Server API reference — every endpoint, grouped by concern. Read on demand when you need a specific route |
${CLAUDE_SKILL_DIR}[[/references/url-pinning.md]] | URL-pinned tab state — how to construct deep-link URLs |
${CLAUDE_SKILL_DIR}[[/references/app-ui.md]] | Browser app UI layout — sidebar, tabs, filters. Rarely needed by agents |
${CLAUDE_SKILL_DIR}[[/references/methodology.md]] | Content methodology — anchor-first flow, fast-path, quality gates, principles. Read this before any non-trivial content request |
${CLAUDE_SKILL_DIR}[[/references/intent-detection.md]] | How to read user requests and decide anchor-first vs. fast-path |
${CLAUDE_SKILL_DIR}[[/references/anchor-authoring.md]] | How to write a great anchor — shapes, length classes, semantic tagging, worked examples |
${CLAUDE_SKILL_DIR}[[/references/distillation-principles.md]] | How to compress an anchor into any target format — platform norms, five compression moves, thesis + CTA preservation |
${CLAUDE_SKILL_DIR}[[/references/slide-deck-engine.md]] | Creative brief for slide decks — structural contract, motion principles, anti-patterns |
${CLAUDE_SKILL_DIR}[[/references/design-system.md]] | The 13 design rules. Read before authoring any slide |
${CLAUDE_SKILL_DIR}[[/references/visual-density.md]] | 85% canvas fill rule and per-type element minimums |
${CLAUDE_SKILL_DIR}[[/references/html-clipping.md]] | Overflow rules per card type |
${CLAUDE_SKILL_DIR}[[/references/content-fit.md]] | R11 Canvas Fit rule. Canvas overflow is emitted by /api/validate-cards as a standard box-layout violation (rule: R11). The fix field prescribes paginate/split/tighten. Handled by the same validate-before-done loop as R1–R10 |
${CLAUDE_SKILL_DIR}[[/references/logo-convention.md]] | Logo standard + pre-flight. Brand logos live in <brand>/assets/: logo.svg/.png, logo-*.{svg,png}, or any *logo*.{svg,png} all conform. Call GET /api/brand/<name>/conformance at project creation; auto-fix or ask the user only when the logo sits OUTSIDE assets/ |
${CLAUDE_SKILL_DIR}[[/references/docx-export.md]] | Document page discipline + DOCX class conventions |
${CLAUDE_SKILL_DIR}[[/references/business-documents.md]] | Branded business deliverables — report, proposal, one-pager, case study, executive summary. Use for report/proposal/case-study requests |
${CLAUDE_SKILL_DIR}[[/references/brand-integration.md]] | Apply an installed brand skill end-to-end |
${CLAUDE_SKILL_DIR}[[/references/platform-rules.md]] | Convenience appendix: per-platform distillation recipes |
${CLAUDE_SKILL_DIR}[[/references/plan-authoring.md]] | The plan-first pipeline contract — how to author a Markdown plan per variant, status lifecycle (planned → approved → rendered → stale), revision handling. Read BEFORE writing any variant file |
${CLAUDE_SKILL_DIR}[[/references/copywriting-formulas.md]] | Seven battle-tested formulas (AIDA, PAS, BAB, 4Ps, 1-2-3-4, 4Us, FAB) with structure, worked examples, default formula-per-variant mapping |
${CLAUDE_SKILL_DIR}[[/references/hooks-and-retention.md]] | Hook archetypes (10), anti-patterns, per-platform length budgets, retention tactics (open loops, scan beats, pattern interrupts) |
${CLAUDE_SKILL_DIR}[[/references/humanized-writing.md]] | Anti-AI-sounding techniques — the five tells, banned words, humanization checklist, per-platform AI tolerance. MANDATORY pass before any .html renders |
${CLAUDE_SKILL_DIR}[[/references/research-audit-2026.md]] | The reference-catalog audit that produced the three craft docs above; tracks what’s covered, what’s delegated to external skills, and what’s still missing |
${CLAUDE_SKILL_DIR}[[/references/platforms/]] | Per-platform playbooks — read the ones you’re distilling into. Each playbook’s “Plan shape” section shows the exact Markdown structure for that platform’s plan. Files: linkedin.md, instagram.md, facebook.md, tiktok.md, x.md, blog.md, deck.md |
${CLAUDE_SKILL_DIR}[[/references/external-skills.md]] | Soft-dependency integration: marketingskills, claude-blog, claude-seo, banana-claude — when to use, install instructions, detection patterns |
${CLAUDE_SKILL_DIR}[[/references/promote-template.md]] | Promote a My Work project into a built-in Gallery template |
Server API — overview
The full reference lives in ${CLAUDE_SKILL_DIR}[[/references/server-api.md]]. Read it on
demand. The endpoints agents use most frequently:
| Route | Purpose |
|---|---|
GET /api/state | Orient before every action — returns mode, contentId, activeFilePath, activeCard, brief, activeBrand |
POST /api/create-project | Start a new project — body {name, type}, type required (social|slides|document) |
POST /api/active-brand | Set or clear the active brand |
POST /api/validate-card | Box Layout validator (Layer 1 primitive) |
GET /api/active-element | Read what the user most recently clicked in the preview |
POST /api/persist-style | Persist a style edit to the card source file |
POST /api/anchor/revise | Bump anchor revision, mark stale variants |
POST /api/anchor/approve | Record anchor approval timestamp |
Field names use camelCase throughout (activeFilePath, derivedFromRevision,
activeSessionDir). Older references may show snake_case — camelCase is authoritative.
URL-pinned tab state — overview
Tab state is fully addressable by URL. Every reload lands on the same project, file, and
card. The agent can construct a URL and send it to the user for deep-linking. Full schema
in ${CLAUDE_SKILL_DIR}[[/references/url-pinning.md]].
Query params: kind (template|session), id (stable content id), file
(content file basename), card (index, 0-based).
Example: http://localhost:PORT/?kind=session&id=my-campaign-oct&file=social.html&card=0
App UI — overview
Sidebar controls format / handle / zoom / logo / files / export. Main area has Preview
(scrollable card strip, arrow-key navigation) and Gallery (5 filters: All / Social /
Slides / Document / My Work). Full layout in ${CLAUDE_SKILL_DIR}[[/references/app-ui.md]].
Persisting element-level edits
The robust edit flow for element styling:
-
Read the selection — check multi first, then single. Call
GET /api/active-elementsfirst. Ifcount > 0, the user is in multi-selection mode (orange overlays) — work offselections[]. Only ifcount === 0fall back toGET /api/active-element(blue overlay). The two modes are mutually exclusive at the client: a plain click clears the orange set, a Cmd/Ctrl+click clears the blue single selection. Each response includes acontextfield carrying{kind, id, name, file, sessionDir?, templateId?, cardIndex, readOnly}captured by the iframe that hosted the click.If
GET /api/active-elementreturnsnullAND/api/active-elementsreturnscount: 0, no element has been clicked — the user may have navigated to the card (updating/api/state.activeCard) but not clicked inside it. Ask the user to click inside the card preview iframe (not the sidebar, not the card border, not the preview-strip thumbnail) on the exact element they want to target. Cross-check withGET /api/inspect-events— if that is also empty, the inspector client has not received any clicks in this session yet. -
If
context.readOnlyistrue, the selection came from a built-in template. CallPOST /api/clone-template-to-sessionwith{templateId: context.templateId}to create an editable copy in My Work. Load the new session via?kind=session&id=<newId>&file=<basename>, ask the user to re-click the element, then retrypersist-style. The 409 response frompersist-stylealso includes a ready-to-usecloneSuggestionpayload. -
Persist to source. Call
POST /api/persist-stylewith{targetSelector, patches}. The server reads context from the selection (no project/file needed in the body). It writes a stabledata-cf-idinto the card HTML and upserts a CSS rule inside a bounded/* === cf:user-edits === */region of the card’s<style>block. Returns409with{templateId, suggestion}if context is read-only. -
(Optional) Live preview. Call
POST /api/evalfor instant visual feedback without waiting for the iframe to rebuild. The persisted source still takes effect on the next render cycle.
Persisted edits survive reloads, card regeneration, and exports. They are byte-additive:
everything outside the user-edits region is untouched. Revert with
DELETE /api/persist-style?cfId=<id>&project=<sessionDir>&file=<basename>.
Validation — Box Layout Theory enforcement
Content Factory vendors the box-validator rule engine. Five layers are all on by default,
each toggleable via POST /api/validation-config/toggle:
- L1
/api/validate-cardprimitive - L2 score badges on preview cards
- L3 agent discipline (validate after each
persist-style) - L4 export preflight
- L5 session-status gate
Defaults: slides and documents strict at ≥ 0.9; social cards lenient at ≥ 0.8.
If Playwright is not installed, GET /api/validator-health reports degraded: true and
all layers silently pass. Install with:
bash ${CLAUDE_SKILL_DIR}[[/scripts/setup-validation.sh]]
Agent workflow (Layer 3): after every persist-style write, check
GET /api/validation-config — if enabled === false or layers.agentDiscipline === false,
skip. Otherwise call POST /api/validate-card {project, file, cardIndex}. If pass: false,
read violations[].fix, apply fixes via more persist-style calls, re-validate, iterate
up to config.iterateLimit (default 3), then report remaining violations and stop.
Template Library
Built-in templates are standalone HTML files in generators/templates/. Each file must
include a <meta name="codi:template"> tag:
{"id":"<kebab-id>","name":"<Human Name>","type":"<social|slides|document>","format":{"w":<w>,"h":<h>}}
When the user clicks a template in the Gallery, the app fetches and parses the HTML, loads
all social-card / slide / doc-page elements into Preview, and saves the selection
to state_dir/preset.json.
Agent Workflow
Step 1 — Start the server
bash ${CLAUDE_SKILL_DIR}[[/scripts/start-server.sh]] --project-dir .
Save the JSON output:
{
"type": "server-started",
"url": "http://localhost:PORT",
"workspace_dir": "/path/to/.codi_output"
}
Tell the user:
“Content factory is ready at
<url>— open it in your browser. Go to the Gallery tab, pick a preset, then describe the content you want.”
If curl <url>/api/state fails but the log shows server-started:
- Read
<workspace_dir>/_server.logfirst — a real crash has a stack trace. No stack trace = the server is fine and the failure is on your side. - Codex agents:
curl: (7) Failed to connectagainst localhost while the log shows a healthy start means your workspace-write sandbox is blocking outbound loopback. The project.codex/config.tomlgrantssandbox_workspace_write.network_access = true, but the setting only takes effect on a fresh Codex session. Ask the user to restart Codex — do NOT patch the server, the watchers, or any file under.agents/skills/. - Scripts under
[[/scripts/]]are generated. Editing.agents/skills/,.claude/skills/,.cursor/skills/, or any other installed copy is lost on the nextcodi generate. If a fix is genuinely needed, report viacodi contributeso the change lands insrc/templates/.
Step 1b — Create a project (when generating new content)
Before writing any files, create a named project. The type field is required. Valid
types: social, slides, document. The server rejects anything else with HTTP 400.
curl -s -X POST <url>/api/create-project \
-H "Content-Type: application/json" \
-d '{"name": "acme-social-campaign", "type": "social"}'
Response: {ok, projectDir, contentDir, stateDir, exportsDir}. Save contentDir — this
is where you write HTML files. The project is now active.
Skip Step 1b when the user opens an existing My Work project from the gallery — the server activates the project automatically when the user clicks it.
Step 1c — Content methodology
Read ${CLAUDE_SKILL_DIR}[[/references/methodology.md]] before any non-trivial content
request. It covers the anchor-first flow, the fast-path for one-off requests, quality gates,
and the principles you apply with judgment. You are framed as a senior content strategist +
designer — the methodology gives you principles and tools, not a script.
The agent never decides the workflow path silently. The default is the full anchor-first workflow (Discovery → Master Anchor → Plans → Render). Fast-path runs only when the user explicitly authorizes it in Step 1 below. Intent signals inform the conversation with the user — they do not authorize the agent to skip steps on its own.
High-level shape:
-
Read the request, then present the workflow choice to the user. Never pick anchor-first vs. fast-path unilaterally, even when the request looks trivially one-off. Present these three options verbatim at the start of every new content request:
“I can run this two ways: (A) Default — full workflow. Discovery intake → master anchor document → per-variant plans → render. Best for multi-format campaigns, substantive topics, or anything you’ll iterate on. (B) Fast-path — single artifact. Skip intake and anchor; go straight to one rendered file. Best for one-off quick requests. (C) You choose for me. I’ll read the signals in your request and pick A or B, then confirm with you before proceeding. Which? (Default is A.)”
Only proceed past this prompt after the user picks A, B, or C. If the user picks C, classify per
${CLAUDE_SKILL_DIR}[[/references/intent-detection.md]], state your pick and the signals behind it, and wait for explicit user confirmation before running Step 2 (for A) or Step 8 (for B). -
Campaign intake — always ask which platforms. Runs only after the user has confirmed option A (directly or via option C resolved to A). Present the platform checklist before authoring: LinkedIn (carousel, post) · Instagram (feed, story, reel cover) · Facebook (post, story, reel) · TikTok (cover) · X/Twitter (card/thread) · blog · slide deck. Also ask: topic, audience, voice, CTA, anchor
length_class(defaultstandard). Persist tobrief.jsonviaPOST /api/brief. -
Author the anchor in Markdown. Write
content/00-anchor.md. The anchor is Markdown — not HTML. Read${CLAUDE_SKILL_DIR}[[/references/anchor-authoring.md]]for frontmatter, length classes, and structure. Iterate until approved; callPOST /api/anchor/approve. -
Plan each requested variant in Markdown. For every platform the user selected in step 2, write a plan file (Markdown, NOT HTML) at
content/<platform>/<variant>.md. The plan is prose — slide-by-slide breakdown, copy drafts, caption, hashtags, visual direction. Read${CLAUDE_SKILL_DIR}[[/references/plan-authoring.md]]for the contract + the platform playbook for per-platform quirks. No HTML exists yet at this stage. -
HARD GATE — user approves each plan before any HTML renders. Present each plan. Iterate on the prose. Render the matching
.htmlONLY after the user explicitly says yes / approved / render it for that specific plan. Silence is not approval. Partial edits are continued iteration, not approval. -
Render approved plans into HTML. Once a plan is approved, generate
content/<platform>/<variant>.htmlfrom the same-basename.md. Tag the HTML with<meta name="codi:variant" content='{"derivedFromRevision":N,"sourceAnchor":"00-anchor.md","planSource":"<platform>/<variant>.md",...}'>. -
Revisions. When the anchor changes substantively, call
POST /api/anchor/revise. Plans AND rendered HTML both become stale. At the start of the next iteration, surface staleness; let the user choose what to re-plan and re-render. Never auto-propagate. -
Fast-path. Runs only when the user explicitly selected option B in Step 1 (directly, or via option C resolved to B with user confirmation). Never enter fast-path based on inferred signals alone — phrases like “quick”, “just”, “one tweet” hint at fast-path but do not authorize the skip. Once the user has authorized fast-path, plan the single variant in Markdown, get plan approval, then render. No
brief.json.
Folder contract (enforced by the scanner + workspace scaffolder):
| Path | Content |
|---|---|
content/00-anchor.md | Markdown anchor — always at content/ root, always .md |
content/linkedin/ | LinkedIn variants (carousel.html, post.html) |
content/instagram/ | Instagram variants (feed.html, story.html, reel-cover.html) |
content/facebook/ | Facebook variants (post.html, story.html, reel.html) |
content/tiktok/ | TikTok (cover.html) |
content/x/ | X/Twitter (card.html) |
content/blog/ | Blog post (post.html) |
content/deck/ | Slide deck (slides.html) |
External skills — soft dependencies. See
${CLAUDE_SKILL_DIR}[[/references/external-skills.md]]. Probe for
marketingskills, claude-blog, claude-seo, and banana-claude at the start of
every anchor-first session. If present, invoke relevant slash commands during intake
(/content-strategy, /marketing-psychology), anchor authoring (/blog outline,
/blog write, /blog factcheck, /seo content), and distillation (/social-content,
/copy-editing, /banana generate for hero images). If absent, inline-generate with
lower fidelity and tell the user once which installs would upgrade quality — never
auto-install, never block the workflow.
Step 1d — Detect and apply a brand (optional)
Full reference: ${CLAUDE_SKILL_DIR}[[/references/brand-integration.md]]
After creating a project, query GET /api/brands to discover installed brand skills. If
any exist and the user has not specified one, ask whether to apply it. If the user confirms,
POST /api/active-brand with {name}, then apply the brand end-to-end (tokens, fonts,
logo, voice) using the full procedure in the reference file.
Skip this step if the user explicitly provides a template or says “no brand”.
Step 2 — Read state and determine mode
Before doing anything else, read the current state to know what the user has open:
curl -s <url>/api/state
Example responses:
// Built-in template open
{ "mode": "template", "contentId": "b2e4a9f3", "activePreset": "earthy-bold",
"activeFilePath": "/abs/path/generators/templates/earthy-bold.html",
"activeFile": null, "activeSessionDir": null, "preset": {...} }
// My Work project open
{ "mode": "mywork", "contentId": "a3f9c2d1", "activePreset": null,
"activeFilePath": "/abs/path/.codi_output/acme-campaign/content/social.html",
"activeFile": "social.html", "activeSessionDir": "/abs/path/.codi_output/acme-campaign",
"status": "in-progress", "preset": null }
// Nothing selected
{ "mode": null, "contentId": null, "activeFilePath": null, ... }
Use contentId as the anchor — not the template name. contentId is a hash of
activeFilePath and is always unique. If unsure which item is open, re-read /api/state
and confirm contentId matches what the user is looking at.
Use activeFilePath for all file edits. Never reconstruct the path from name fragments.
mode | Meaning | What to do |
|---|---|---|
"template" | User opened a Gallery template | Step 1b + Step 3: create a project, generate content styled after that template |
"mywork" | User opened a My Work project | Step 4b: edit activeFilePath in place |
null | Nothing selected yet | Tell the user to pick a template or a My Work project |
Template mode means the user is looking at a read-only built-in template as a style
reference. Your job is to create a new project (Step 1b) and generate a new HTML file in
contentDir that follows that template’s visual identity — colors, typography, layout —
but with the user’s content.
My Work mode means the user is looking at content they already created. Edit the
existing HTML file at activeFilePath in place.
Step 3 — Generate content (Template mode)
After creating a project (Step 1b), write <contentDir>/social.html (or slides.html /
document.html). The app detects the new file via WebSocket and adds it to the Content
Files list.
Required HTML structure
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- REQUIRED: content identity — powers the preview metadata bar -->
<meta name="codi:template" content='{"id":"my-content","name":"My Content Title","type":"social","format":{"w":1080,"h":1080}}'>
<title>My Content Title</title>
<!-- FONTS: if a brand is active and tokens.json.fonts.google_fonts_url is set, use that URL.
If the brand has local fonts, generate @font-face blocks using
http://localhost:PORT/api/brand/<name>/assets/fonts/<file>.woff2 URLs.
If no brand is active, use the default: -->
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800;900&family=Geist+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<style>
/* If brand is active: paste full content of brand/tokens.css here (inline — not <link>) */
/* Then @font-face blocks for local fonts */
/* Then card-specific styles */
</style>
</head>
<body>
<article class="social-card" data-type="cover" data-index="01"><!-- slide HTML --></article>
<article class="social-card" data-type="content" data-index="02"><!-- slide HTML --></article>
<article class="social-card" data-type="cta" data-index="03"><!-- slide HTML --></article>
</body>
</html>
Content identity — REQUIRED
Every generated HTML file MUST include a <meta name="codi:template"> tag and a <title>
in <head>:
{"id":"<kebab-case-id>","name":"<Human Readable Name>","type":"<social|slides|document>","format":{"w":<w>,"h":<h>}}
typemust match:socialfor cards,slidesfor decks,documentfor A4 pagesformatmust match the active format button dimensionsnamedescribes the content topic, not the template (e.g. “AI Agents Series”, not “Dark Editorial”)
Card rules
- Element selectors scanned by the app:
class="social-card"— social media cards (1:1, 4:5, 9:16, OG)class="slide"— slide deck pages (16:9)class="doc-page"— document pages (A4)
- Required attributes:
data-type(cover / content / stat / quote / cta / title / closing) anddata-index(zero-padded: 01, 02, …) - Dimensions come from the active format button, not the HTML itself
- Replace
@handlewith the user’s actual handle - All CSS goes in
<style>— the app extracts styles per card and renders each in its own iframe - Rewrite the whole file to update — the WebSocket watcher broadcasts a reload on every change
Clipping rules — MANDATORY
Full rules: ${CLAUDE_SKILL_DIR}[[/references/html-clipping.md]]
- Social cards and slides:
overflow: hiddenon export — content beyond the canvas is clipped in the final PNG/PDF/PPTX. In preview the factory relaxes this so you can see overflow visually (and the validator can measure it). - Document pages (
.doc-page):overflow: visible— content grows vertically. Never useoverflow: hiddenon.code-block,pre, ortable - Tables inside
.doc-page: the flex-column wrapper MUST havewidth: 100%— without it,width: 100%on a child table resolves against an indefinite width and columns collapse
Canvas-fit validation — part of the standard validate-before-done loop
Full protocol: ${CLAUDE_SKILL_DIR}[[/references/content-fit.md]]
Canvas overflow is emitted as R11 “Canvas Fit” by the same
/api/validate-cards endpoint that runs R1–R10. There is no separate
state file to read, no parallel notification channel. The agent’s normal
validate-before-done loop catches overflow automatically.
For every R11 violation in violations[]:
remediation: "paginate"(documents, overflow > 15%) — add a new.doc-pagesibling inside.doc-container, move overflow content into it, preserve header/footer on every pageremediation: "split"(slides, overflow > 15%) — cut the offending slide at the nexth2/hrboundaryremediation: "tighten"(small overflow, or any social card) — reduce padding, condense copy, or drop one line-height / font-size notch
The fix field is prefixed with the remediation name and includes the
exact overflow numbers — patch the HTML and re-validate until valid: true.
Document template conventions — DOCX export
Full reference: ${CLAUDE_SKILL_DIR}[[/references/docx-export.md]]
- Standard HTML tags (
h1–h3,p,ul/ol,strong,em,code) map automatically to DOCX paragraph styles - Use
.page-header,.page-footer,.callout,.eyebrowfor page chrome - Use
<table class="data-table">withtable-layout: fixedin CSS - Use
<div class="code-block">for syntax-highlighted code — exported as Playwright screenshot PNG;overflow: visiblerequired - Use
<div class="diagram-wrap"><svg ...>— SVG must be a direct child of the wrapper .doc-pageisdisplay: flex; flex-direction: column— all direct children needmin-width: 0- Remote
https://image URLs not fetched during DOCX export — use data URIs orfile://paths
Visual density — MANDATORY for all card types
Full reference: ${CLAUDE_SKILL_DIR}[[/references/visual-density.md]]
Every card must visibly occupy ≥85% of its canvas with purposeful content. Mentally draw a 3×3 grid over each card — at least 7 of 9 cells must contain content or a purposeful decorative element. If 3+ cells are empty, add content from the fill techniques list in the reference.
Design system — MANDATORY for all slides
Read ${CLAUDE_SKILL_DIR}[[/references/design-system.md]] before authoring any slide.
The reference holds the 13 design rules, full CSS patterns, before/after examples, and
verification scripts. Every slide must pass every rule before shipping.
Slide deck generation — single-file authoring (MANDATORY)
Read ${CLAUDE_SKILL_DIR}[[/references/slide-deck-engine.md]] before authoring any deck.
The brief is the single source of truth for the structural contract (per-slide isolation,
canvas size, document shape), motion principles, and anti-patterns.
Every deck is one self-contained HTML file authored from scratch. No sibling deck.css
/ deck.js. No external refs except Google Fonts. The HTML export downloads the source
byte-for-byte.
Document page discipline — MANDATORY
Read ${CLAUDE_SKILL_DIR}[[/references/docx-export.md]] for the full rules on
.doc-page canvas (794×1123 fixed), per-page structure
(.page-header + .page-body + .page-footer), content height budget (~950px usable),
page-split checklist, and DOCX mapping.
Step 3b — Validate layout structure
After writing any HTML file, validate every card via the vendored Box Layout validator before showing anything to the user:
curl -s -X POST <url>/api/validate-card \
-H "Content-Type: application/json" \
-d '{"project":"<sessionDir>","file":"<basename>","cardIndex":0}'
If pass: false, read violations[].fix, patch the HTML, and re-validate. Iterate up
to 3 times (config.iterateLimit), then report remaining violations and stop. Only show
the user the final validated result.
For batch validation across all cards in a file, use
GET /api/validate-cards?project=<sessionDir>&file=<basename>.
Step 4 — Iterate (loop until done)
Content creation is a back-and-forth process. This step repeats until the user is satisfied.
Campaign propagation check — run FIRST when a brief exists
Before applying any new feedback, if the project has a brief, read it and check whether the anchor has drifted ahead of any variants:
curl -s <url>/api/distill-status
If stale is non-empty, ask the user before processing new feedback — which variants
to re-distill, which to mark manual. See methodology.md §6 for the full re-distill
semantics. If no brief exists, skip this check entirely.
When the current edit IS an anchor edit, call POST /api/anchor/revise after writing
the file. This is what makes the next invocation’s propagation check fire.
The loop:
- User opens the Preview tab and reviews the rendered cards
- User gives feedback in chat: “change the headline on slide 2”, “make the background darker”
- Agent reads
/api/stateto confirm what the user has open - Agent fetches the current HTML, applies the changes, rewrites the file
- App reloads in under 200ms — user sees the update immediately
- Repeat from step 1
Do not ask the user to reload the page — the WebSocket handles it.
Targeted card edits — “this slide”, “this page”, “update this”
When the user says “change this slide”, “fix this page”, or gives feedback without
naming a specific slide number, they are referring to the card currently highlighted in
Preview. The app posts the selected card to /api/active-card automatically.
Resolution rule — use dataIdx first, then dataType, then index as fallback.
dataIdx is the stable identifier baked into the HTML; index shifts when cards are
added or removed.
Workflow:
- Read
/api/state, confirmactiveCardmatches what the user is looking at - Read the file at
activeFilePath, locate the element whosedata-indexmatchesactiveCard.dataIdx - Edit only that element. Preserve
data-typeanddata-indexunless asked to change them - Rewrite the whole file (the watcher needs a file write to trigger reload)
- Confirm to the user which card changed: “Updated slide 3 of 7 (stat card) — new value is 42%”
If activeCard is null, ask the user to click the card they want to change, then
re-read /api/state.
Ambiguity guard: if the user names a slide explicitly (“change slide 5”), trust the
number — do NOT silently remap it to activeCard. Only use activeCard for deictic
language (“this”, “here”, “the one I’m on”).
Step 4b — Edit a My Work project
When mode is "mywork" in /api/state:
- Read state.
activeFilePathis the absolute path to edit - Read the current HTML from disk
- Edit and rewrite
activeFilePathdirectly - Do not create a new file — this is an edit of the user’s existing work. Preserve all slides they did not ask to change
Never reconstruct the path from name fragments — always use activeFilePath verbatim.
Step 4c — Modify a built-in template
When mode is "template" in /api/state:
-
Read state.
activeFilePathis the absolute path to the template file -
Edit
activeFilePathin place — change CSS, copy, colors, structure. The server’s template watcher fires within 150ms and broadcastsreload-templates -
Also edit the source so the change persists across
codi generate:${CLAUDE_SKILL_DIR}/generators/templates/<name>.html (installed copy) src/templates/skills/content-factory/generators/templates/<name>.html (self-dev only — Codi repo)The
src/templates/path only exists when working on the Codi source repo itself; skip it in consumer projects.
Editing a template changes it for all future sessions. If the user only wants a one-off variation without touching the original, generate a new content file (Step 3) and use the template as a style reference only.
Step 4d — Promote a My Work project to a built-in template
Full reference: ${CLAUDE_SKILL_DIR}[[/references/promote-template.md]]
Trigger phrases: “save this as a template”, “add this to my presets”, “make this reusable”, “add my project from .codi_output as a new template”.
Read the reference for the full 8-step workflow (read state → verify doc conventions → confirm name → copy to both installed and source paths → update meta → verify in Gallery → log feedback → optional upstream contribution).
Do not run codi generate unless the user explicitly asks — copying the source file
is sufficient.
Step 5 — Export and stop
Export happens in the browser via the sidebar buttons. When done:
bash ${CLAUDE_SKILL_DIR}[[/scripts/stop-server.sh]] <workspace_dir>
Summarize: workspace path, project name, number of slides, format, and where exports were saved.