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

  1. What you can build
  2. Who this is for
  3. Prerequisites
  4. Quick start
  5. Core concepts
  6. User journeys
  7. App UI tour
  8. Project layout on disk
  9. Server API reference
  10. Templates — adding your own
  11. Brand skills — how they plug in
  12. URL-pinned tab state
  13. Live element editing
  14. Box Layout validation
  15. Marketing skills — soft integration
  16. Visual density rules
  17. Troubleshooting

What you can build

OutputFormatTypical use
Social card — single image1:1 / 4:5 / 9:16 / OG 1200×630LinkedIn post, Instagram feed, Twitter card
Carousel — multiple linked cards4:5 or 9:16LinkedIn carousel, Instagram carousel, IG story set
Slide deck — 16:9 presentation1280×720Webinars, pitches, internal updates
Document — A4 pages794×1123Blog post, technical docs, guide, one-pager
Full campaign — one topic, many variantsAll of the aboveLaunch 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

DependencyInstallPurpose
Node.js 18+requiredruns the server
Playwright (optional)npx playwright install chromiumPNG and PDF export
A modern browserrequiredopens 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:

  1. Run scripts/start-server.sh and print the URL
  2. Create a project folder under .codi_output/
  3. Ask a couple of clarifying questions
  4. Write a cover slide + content slides + CTA slide as a single HTML file
  5. 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:

  1. Create a project folder
  2. Ask 1-2 clarifying questions (headline, key points, CTA)
  3. Write content/social.html with one social-card element at 1:1 format
  4. Tell you to open the Preview tab

You:

  1. Open the URL, Preview tab
  2. Review the card at full zoom
  3. Give feedback in chat: “make the headline larger”, “change the background”
  4. Each rewrite reloads the preview in under 200 ms via WebSocket
  5. 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:

  1. Create a project
  2. Ask audience, tone, and which sections to include
  3. Plan the slides (cover → agenda → content → takeaway → CTA)
  4. Write content/slides.html with ten <article class="slide"> elements
  5. Apply visual density rules so no slide ships with blank space

You:

  1. Open the Preview tab, click 16:9 in the format buttons if not already
  2. Click any slide to zoom in
  3. Use left / right arrow keys to navigate slides
  4. Give feedback per slide: “change slide 3 headline”, “add a stat to slide 5”
  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:

  1. Create a project
  2. Ask how many pages, what sections, and whether to include code blocks
  3. Plan each A4 page to fit within the ~950px body budget
  4. Write content/document.html with one <article class="doc-page"> per page
  5. Include page headers, footers, callouts, data tables, and code blocks as needed
  6. Apply DOCX-compatible class conventions so export to Word works cleanly

You:

  1. Open the Preview tab, click A4 in the format buttons
  2. Scroll through the pages
  3. Give feedback: “add a FAQ page”, “move the code block to page 3”
  4. 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:

  1. What is the core topic?
  2. What anchor fits best — blog, docs, or deck? (Agent proposes a default.)
  3. Who is the audience?
  4. What voice? (Inherits from your active brand if any.)
  5. Which platforms do you want to generate for? (Checklist — default is LinkedIn carousel + Instagram feed.)
  6. 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.html
  • 11-linkedin-post.html
  • 20-instagram-feed.html
  • 21-instagram-story.html
  • 30-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:

  1. Reads /api/state and confirms which card is active
  2. Reads the current HTML
  3. Finds the element with the matching data-index
  4. Rewrites only that element
  5. 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:

  1. Call GET /api/brands to list available brand skills
  2. Call POST /api/active-brand with your choice
  3. Read tokens.json (colors, fonts, logo paths, voice tone)
  4. Read tokens.css and inline it into every generated HTML file
  5. Load Google Fonts via <link> or generate @font-face blocks for local fonts (served at /api/brand/<name>/assets/fonts/…)
  6. Fetch the logo SVG via /api/brand/<name>/assets/<logo> and inline it
  7. Open the brand’s references/ directory to learn the brand’s layout and component patterns
  8. Write copy using voice.tone, voice.phrases_use, and avoiding voice.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 typeButtons shownDefault
Social cardExport PNG (current), Export PDF (all)PNG
Slide deckExport PPTX (all), Export PDF (all), Export PNG (current)PPTX
DocumentExport PDF (all), Export DOCX (all), Export PNG (current)PDF

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.

  1. Open the URL → Gallery tab
  2. Click the My Work filter
  3. You see a grid of past project cards — each shows the project date, preset name, and a live thumbnail of the first content file
  4. Click any card to activate that project; the server loads its content files and switches the sidebar file list to that project
  5. Click any file in the sidebar to open it in the Preview tab
  6. Keep iterating — the agent will detect the project via /api/state and 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:

  1. Confirm the template name
  2. Copy the content file to generators/templates/<name>.html in both the installed skill and the source
  3. Update the <meta name="codi:template"> tag with a clean id and name
  4. Broadcast a reload-templates event — the Gallery refreshes within 150ms without a page reload; the new template appears inline under the matching type filter (Social / Slides / Document)
  5. 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.

  1. Open 00-anchor-blog.html (or whichever anchor you have)
  2. Edit it with the agent — “Add a fifth section about cache warming”
  3. The agent rewrites the anchor and silently bumps anchor.revision
  4. Send any next message to the agent — e.g. “change the LinkedIn cover headline”
  5. 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)”

  6. Pick your option; the agent re-distills the selected variants from the new anchor revision, then applies your new feedback
  7. If you pick skip, those variants get marked status: "manual" in brief.json so the same prompt never fires again for that revision

App UI tour

ControlWhat it does
FormatSix 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.
Zoom15% - 120% slider. Scales the preview cards. Default 40%.
LogoON/OFF toggle plus size and X/Y sliders. Adds a logo overlay to every card.
Content filesList of HTML files in the active project. Anchor files show an ANCHOR badge. Click to load.
ExportContext-aware export buttons (see Journey G).
Activity logTimestamped 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 type meta is social (cards, carousels, stories)
  • Slides — templates whose type meta is slides (16:9 decks)
  • Document — templates whose type meta is document (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

RouteMethodPurpose
/GETServe the web app HTML shell
/static/*GETServe app.css, app.js
/vendor/*GETServe html2canvas, jszip

Projects and sessions

RouteMethodPurpose
/api/create-projectPOSTCreate 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-projectPOSTActivate an existing project — body {projectDir}
/api/sessionsGETList all projects in the workspace
/api/session-statusPOSTPersist project status — body {sessionDir, status} where status is draft, in-progress, review, or done

Files and content

RouteMethodPurpose
/api/filesGETList HTML files in the active project’s content/
/api/content?file=XGETReturn raw HTML for a content file
/api/session-content?session=&file=GETServe a file from a specific project
/api/content-metadata?kind=&id=GETUnified descriptor for templates and sessions: {kind, id, name, type, format, cardCount, status, createdAt, modifiedAt, readOnly, source}. readOnly=true for built-in templates
/api/content-listGETDebug/utility — every content descriptor the server knows about, templates and sessions merged
/api/clone-template-to-sessionPOSTCopy 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

RouteMethodPurpose
/api/stateGETAggregate 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-fileGET/POSTWhich file is currently loaded
/api/active-cardGETThe card currently highlighted in Preview: {index, total, dataType, dataIdx, file, timestamp}
/api/active-cardPOSTApp-only — the browser posts this when you click a card or use arrow keys
/api/presetGET/POSTWhich Gallery preset was picked — {id, name, type, timestamp}

Live inspection

RouteMethodPurpose
/api/active-elementGETThe 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-elementsGETMulti-select set of Cmd/Ctrl-clicked elements — {count, selections:[...]}
/api/active-elementsDELETEClear the multi-select set
/api/inspect-events?since=<seq>GETRing buffer of preview interactions (clicks, inputs, submits, scrolls). Poll with ?since=<lastSeq> for incremental updates
/api/evalPOSTRun 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

RouteMethodPurpose
/api/persist-stylePOSTPersist 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-styleDELETERevert 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-styleGETList persisted edits for a card — query ?project=<dir>&file=<basename>. Returns {count, rules:[{selector, declarations:[...]}]}

Campaign brief and anchor revisions

RouteMethodPurpose
/api/briefGETReturn the active project’s brief.json or null
/api/briefPOSTWrite the brief — body is an arbitrary JSON object (no schema enforcement). Returns 400 if no project is active
/api/distill-statusGETAnchor 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/revisePOSTBump brief.anchor.revision and mark variants with derivedFromRevision < new revision as status: "stale". Optional body {reason?}
/api/anchor/approvePOSTSet 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

RouteMethodPurpose
/api/validate-cardPOSTValidate 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=GETBatch validate every card in a file — {ok, pass, cards:[...], failingCards:[...]}
/api/validation-config?project=<dir>[&file=<basename>]GETResolved config cascade with source map showing which scope produced each field. Cascade: type-default → user default → session → per-file
/api/validation-configPATCHMerge a partial patch — body `{project
/api/validation-config/togglePOSTFlip a layer on or off — body {project, layer, value}. Layers: all (master), endpoint, badge, agentDiscipline, exportPreflight, statusGate
/api/validation-config/ignore-violationPOSTAdd a per-file exemption — body {project, file, rule, selector?, cardIndex?}
/api/validator-healthGET{degraded, workers, cacheSize, cacheHits, cacheMisses, avgLatencyMs, lastError}. degraded: true means Playwright is missing and all layers default to pass

Templates

RouteMethodPurpose
/api/templatesGETList all stock and brand templates with metadata
/api/template?file=X[&brand=Y]GETServe a single template HTML file

Brands

RouteMethodPurpose
/api/brandsGETList installed brand skills (those with brand/tokens.json)
/api/active-brandPOSTSet or clear the active brand — body {name} or {} to clear
/api/brand/:name/assets/*GETServe a file from a brand skill’s assets/ — use these URLs for logos and fonts in generated HTML

Export

RouteMethodPurpose
/api/export-pngPOSTRender a card to 2× PNG via Playwright — body {html, width, height}
/api/export-pdfPOSTRender 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:

  1. Inlines brand/tokens.css as a <style> block
  2. Loads fonts from tokens.json.fonts.google_fonts_url OR from assets/fonts/ via @font-face blocks pointing at /api/brand/<name>/assets/fonts/<file>
  3. Inlines the logo SVG from assets/<logo-file> based on card background
  4. Reads the brand’s references/ directory for layout guidance
  5. If the brand has a templates/ folder, those templates appear in the Gallery alongside the built-in ones, filtered by each template’s type (Social / Slides / Document). They carry a brand tag on their metadata so the agent knows which skill they come from.
  6. Writes copy using voice.tone, voice.phrases_use, and avoiding voice.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:

  1. <project>/assets/logo.svg — the project’s own logo (wins when present)
  2. <active-brand>/brand/assets/logo.svg — the active brand skill’s logo
  3. Built-in codi mark — 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:

TypeOverflow > 15%Overflow ≤ 15%
documentpaginate (add a new .doc-page sibling)tighten
slidessplit into multiple slides at the next section breaktighten
socialtighten (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%):

FormatCanvasDefault size
Document (A4)794 × 1123159 px
Social (square)1080 × 1080216 px
Slides (16:9)1280 × 720144 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.

ParamMeaning
kindtemplate or session
idStable content id (template id or session dir basename)
fileContent file basename (e.g. social.html)
cardActive 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:

  1. Click the target element in the preview. Cmd/Ctrl-click to build a multi-select set for a batch operation.
  2. Agent reads the selection via GET /api/active-element (or /api/active-elements for multi-select). The response carries a context field with {kind, id, name, file, cardIndex, readOnly}.
  3. If context.readOnly is true, the selection came from a built-in template. The agent calls POST /api/clone-template-to-session with {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.
  4. Persist the edit via POST /api/persist-style with {targetSelector, patches}. The server writes a stable data-cf-id into the HTML and upserts a CSS rule in a bounded /* === cf:user-edits === */ region of the card’s <style> block.
  5. Optional live preview via POST /api/eval for 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:

LayerPurpose
L1Primitive validation via POST /api/validate-card
L2Pass/fail score badges on preview cards
L3Agent discipline — every persist-style write triggers a validate-and-fix loop
L4Export preflight — blocks export of cards below threshold
L5Session-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.

PhaseSkill invoked if present
Intakecontent-strategy — validate topic and audience fit
Anchor (blog / docs)copywritinghumanizer
Anchor (deck)launch-strategy (if present) → humanizer
Distillationsocial-content — one call per platform → humanizer
Optionalad-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> then bash 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, or tighten) in the fix field
  • For documents, either tighten the layout or add a new .doc-page sibling
  • 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 block file:// 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.sh once — it installs Playwright Chromium and the validator’s own dependencies
  • Check GET /api/validator-health — if degraded: 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-wrap use overflow: visibleoverflow: hidden clips content and corrupts the screenshot capture
  • Tables must include table-layout: fixed in 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.json exists at the project root
  • Check that brief.anchor.file matches a file in content/
  • Run curl -s <url>/api/brief to verify the brief is readable

The agent keeps asking to re-distill variants after I said “skip”

  • The skip answer marks variants as status: "manual" in brief.json for 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.json and remove the variant from variants[]

What’s next

  • Campaign export bundle — single ZIP with manifest listing each variant
  • Gallery “Campaigns” filter — group projects with brief.json and show mini-thumbnails per platform
  • Codi-native marketing skills — first-party replacement for the external marketing-skills plugin
  • Multi-card selection in the preview strip — right now activeCard tracks one card at a time

SKILL.md

Non-negotiable rules

  • The brand logo lives in <brand-skill>/assets/. Preferred name is logo.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 OUTSIDE assets/, 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 .slide deck ships as one self-contained HTML file with all CSS, @keyframes, fonts, and per-slide inline <script> bundled in. No sibling deck.css / deck.js. Motion is deliberate: staggered entry animations, compositor-only transform / 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-presentation which 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 returned violations[] until the report is clean (valid: true). Canvas overflow (rule: R11, “Canvas Fit”) is a standard validation violation alongside the box-layout rules — its fix field prescribes paginate/split/tighten with 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 .pptx directly (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.

TermMeaning
Project / SessionSame 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 fileOne 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)
CardOne rendered page inside a content file. Three element types: .social-card, .slide, .doc-page. The app scanner only recognizes these three class names
Preset / TemplateSame 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
AnchorThe 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
VariantA platform-specific content file distilled from an anchor. Carries a <meta name="codi:variant"> tag linking back to the anchor’s revision
BriefA project’s brief.json — arbitrary JSON (no schema enforcement) capturing intake, anchor revision, and variant registry

Skill Assets

AssetPurpose
${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:

RoutePurpose
GET /api/stateOrient before every action — returns mode, contentId, activeFilePath, activeCard, brief, activeBrand
POST /api/create-projectStart a new project — body {name, type}, type required (social|slides|document)
POST /api/active-brandSet or clear the active brand
POST /api/validate-cardBox Layout validator (Layer 1 primitive)
GET /api/active-elementRead what the user most recently clicked in the preview
POST /api/persist-stylePersist a style edit to the card source file
POST /api/anchor/reviseBump anchor revision, mark stale variants
POST /api/anchor/approveRecord 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:

  1. Read the selection — check multi first, then single. Call GET /api/active-elements first. If count > 0, the user is in multi-selection mode (orange overlays) — work off selections[]. Only if count === 0 fall back to GET /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 a context field carrying {kind, id, name, file, sessionDir?, templateId?, cardIndex, readOnly} captured by the iframe that hosted the click.

    If GET /api/active-element returns null AND /api/active-elements returns count: 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 with GET /api/inspect-events — if that is also empty, the inspector client has not received any clicks in this session yet.

  2. If context.readOnly is true, the selection came from a built-in template. Call POST /api/clone-template-to-session with {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 retry persist-style. The 409 response from persist-style also includes a ready-to-use cloneSuggestion payload.

  3. Persist to source. Call POST /api/persist-style with {targetSelector, patches}. The server reads context from the selection (no project/file needed in the body). It writes a stable data-cf-id into the card HTML and upserts a CSS rule inside a bounded /* === cf:user-edits === */ region of the card’s <style> block. Returns 409 with {templateId, suggestion} if context is read-only.

  4. (Optional) Live preview. Call POST /api/eval for 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-card primitive
  • 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.log first — 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 connect against localhost while the log shows a healthy start means your workspace-write sandbox is blocking outbound loopback. The project .codex/config.toml grants sandbox_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 next codi generate. If a fix is genuinely needed, report via codi contribute so the change lands in src/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:

  1. 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).

  2. 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 (default standard). Persist to brief.json via POST /api/brief.

  3. 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; call POST /api/anchor/approve.

  4. 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.

  5. HARD GATE — user approves each plan before any HTML renders. Present each plan. Iterate on the prose. Render the matching .html ONLY after the user explicitly says yes / approved / render it for that specific plan. Silence is not approval. Partial edits are continued iteration, not approval.

  6. Render approved plans into HTML. Once a plan is approved, generate content/<platform>/<variant>.html from the same-basename .md. Tag the HTML with <meta name="codi:variant" content='{"derivedFromRevision":N,"sourceAnchor":"00-anchor.md","planSource":"<platform>/<variant>.md",...}'>.

  7. 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.

  8. 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):

PathContent
content/00-anchor.mdMarkdown 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.

modeMeaningWhat to do
"template"User opened a Gallery templateStep 1b + Step 3: create a project, generate content styled after that template
"mywork"User opened a My Work projectStep 4b: edit activeFilePath in place
nullNothing selected yetTell 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>}}
  • type must match: social for cards, slides for decks, document for A4 pages
  • format must match the active format button dimensions
  • name describes 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) and data-index (zero-padded: 01, 02, …)
  • Dimensions come from the active format button, not the HTML itself
  • Replace @handle with 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: hidden on 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 use overflow: hidden on .code-block, pre, or table
  • Tables inside .doc-page: the flex-column wrapper MUST have width: 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-page sibling inside .doc-container, move overflow content into it, preserve header/footer on every page
  • remediation: "split" (slides, overflow > 15%) — cut the offending slide at the next h2/hr boundary
  • remediation: "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 (h1h3, p, ul/ol, strong, em, code) map automatically to DOCX paragraph styles
  • Use .page-header, .page-footer, .callout, .eyebrow for page chrome
  • Use <table class="data-table"> with table-layout: fixed in CSS
  • Use <div class="code-block"> for syntax-highlighted code — exported as Playwright screenshot PNG; overflow: visible required
  • Use <div class="diagram-wrap"><svg ...> — SVG must be a direct child of the wrapper
  • .doc-page is display: flex; flex-direction: column — all direct children need min-width: 0
  • Remote https:// image URLs not fetched during DOCX export — use data URIs or file:// 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:

  1. User opens the Preview tab and reviews the rendered cards
  2. User gives feedback in chat: “change the headline on slide 2”, “make the background darker”
  3. Agent reads /api/state to confirm what the user has open
  4. Agent fetches the current HTML, applies the changes, rewrites the file
  5. App reloads in under 200ms — user sees the update immediately
  6. 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:

  1. Read /api/state, confirm activeCard matches what the user is looking at
  2. Read the file at activeFilePath, locate the element whose data-index matches activeCard.dataIdx
  3. Edit only that element. Preserve data-type and data-index unless asked to change them
  4. Rewrite the whole file (the watcher needs a file write to trigger reload)
  5. 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:

  1. Read state. activeFilePath is the absolute path to edit
  2. Read the current HTML from disk
  3. Edit and rewrite activeFilePath directly
  4. 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:

  1. Read state. activeFilePath is the absolute path to the template file

  2. Edit activeFilePath in place — change CSS, copy, colors, structure. The server’s template watcher fires within 150ms and broadcasts reload-templates

  3. 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.