Architecture
The pipeline runs in four phases. The leverage is grounding every phase in the brand’s real site instead of a blank prompt.- Gather crawls the whole marketing site to Markdown (Web Scraping API) and pulls the brand kit (Brand API) and design tokens (Styleguide API).
- Distill runs the product-marketing-context skill over the crawl to capture positioning, audience, and voice, turns the brand kit into a design guide, and generates net-new campaign ideas grounded in that positioning.
- Write runs the copywriting skill on the chosen idea for an on-brand headline, supporting line, and CTA.
- Render lays out the creative from the design guide and copy, then rasterizes it to each platform size.
Prerequisites
- A Context.dev API key. Grab one from the dashboard and export it as
CONTEXT_DEV_API_KEY. - An Anthropic API key for the model calls in Steps 2 through 4. Export it as
ANTHROPIC_API_KEY. - The Context.dev SDK for your backend:
npm install context.dev
pip install context.dev
gem install context.dev
go get github.com/context-dot-dev/context-go-sdk
composer require context-dev/context-dev-php
Step 1. Gather the brand’s site
The first step is always to understand the product deeply. To do this, your AI agent needs context. It needs to know what your product does and who it’s for. You will also need your brand’s logos and styleguides. For this we’ll use:- The Brand API to get brand name, description, slogans, backdrops, and all logo variants.
- The Web Scraping API to crawl reachable marketing-site pages as clean Markdown.
- The Styleguide API to get exact colors, component CSS, spacing, fonts, shadows, etc.
import ContextDev from "context.dev";
const client = new ContextDev({ apiKey: process.env.CONTEXT_DEV_API_KEY! });
export async function gatherBrand(domain: string) {
// The voice (whole-site crawl), the identity (brand), and the type (styleguide).
const [{ results }, { brand }, { styleguide }] = await Promise.all([
client.web.webCrawlMd({ url: `https://${domain}`, maxPages: 100 }),
client.brand.retrieve({ type: "by_domain", domain }),
client.web.extractStyleguide({ domain }),
]);
if (!brand || !styleguide) throw new Error(`No brand or styleguide data for ${domain}`);
const logos = brand.logos ?? [];
const lightLogo =
logos.find((l) => l.type === "logo" && l.mode === "light")?.url ?? logos[0]?.url;
return {
// The design guide: identity plus the look, everything the render step needs.
designGuide: {
name: brand.title,
description: brand.description,
slogan: brand.slogan,
palette: [
styleguide.colors?.accent,
styleguide.colors?.background,
styleguide.colors?.text,
].filter(Boolean),
logos: logos.map((l) => ({ url: l.url, mode: l.mode, type: l.type })), // every variant
lightLogo,
headingFont: styleguide.typography.headings.h1?.fontFamily,
bodyFont: styleguide.typography.p?.fontFamily,
spacing: styleguide.elementSpacing,
shadows: styleguide.shadows,
components: styleguide.components,
},
// The marketing corpus: one entry per page.
pages: results.map((r) => ({ url: r.metadata.url, markdown: r.markdown })),
};
}
import os
from context.dev import ContextDev
client = ContextDev(api_key=os.environ["CONTEXT_DEV_API_KEY"])
def gather_brand(domain: str) -> dict:
# The voice (whole-site crawl), the identity (brand), and the type (styleguide).
crawl = client.web.web_crawl_md(url=f"https://{domain}", max_pages=100)
brand = client.brand.retrieve(type="by_domain", domain=domain).brand
sg = client.web.extract_styleguide(domain=domain).styleguide
tokens = sg.to_dict() # to_dict keys are camelCase
logos = brand.logos or []
light_logo = next(
(l.url for l in logos if l.type == "logo" and l.mode == "light"),
logos[0].url if logos else None,
)
return {
"design_guide": {
"name": brand.title,
"description": brand.description,
"slogan": brand.slogan,
"palette": [
c for c in [
getattr(sg.colors, "accent", None),
getattr(sg.colors, "background", None),
getattr(sg.colors, "text", None),
] if c
],
"logos": [{"url": l.url, "mode": l.mode, "type": l.type} for l in logos], # every variant
"light_logo": light_logo,
"heading_font": sg.typography.headings.h1.font_family if sg.typography.headings.h1 else None,
"body_font": sg.typography.p.font_family,
"spacing": tokens["elementSpacing"],
"shadows": tokens["shadows"],
"components": tokens["components"],
},
"pages": [{"url": r.metadata.url, "markdown": r.markdown} for r in crawl.results],
}
require "context_dev"
CLIENT = ContextDev::Client.new(api_key: ENV.fetch("CONTEXT_DEV_API_KEY"))
def gather_brand(domain)
# The voice (whole-site crawl), the identity (brand), and the type (styleguide).
crawl = CLIENT.web.web_crawl_md(url: "https://#{domain}", max_pages: 100)
brand = CLIENT.brand.retrieve(body: { type: :by_domain, domain: domain }).brand
sg = CLIENT.web.extract_styleguide(domain: domain).styleguide
tokens = sg.to_h # to_h keys are snake_case symbols
logos = brand.logos || []
light_logo = (logos.find { |l| l.type == "logo" && l.mode == "light" } || logos.first)&.url
{
design_guide: {
name: brand.title,
description: brand.description,
slogan: brand.slogan,
palette: [
tokens.dig(:colors, :accent),
tokens.dig(:colors, :background),
tokens.dig(:colors, :text),
].compact,
logos: logos.map { |l| { url: l.url, mode: l.mode, type: l.type } }, # every variant
light_logo: light_logo,
heading_font: sg.typography.headings.h1&.font_family,
# `p` collides with Ruby's Kernel#p, so the SDK exposes it as `p_`.
body_font: sg.typography.p_.font_family,
spacing: tokens[:element_spacing],
shadows: tokens[:shadows],
components: tokens[:components],
},
pages: crawl.results.map { |r| { url: r.metadata.url, markdown: r.markdown } },
}
end
package main
import (
"context"
"fmt"
"os"
contextdev "github.com/context-dot-dev/context-go-sdk"
"github.com/context-dot-dev/context-go-sdk/option"
"github.com/context-dot-dev/context-go-sdk/packages/param"
)
func main() {
client := contextdev.NewClient(option.WithAPIKey(os.Getenv("CONTEXT_DEV_API_KEY")))
ctx := context.TODO()
domain := "stripe.com"
// The voice (whole-site crawl), the identity (brand), and the type (styleguide).
crawl, err := client.Web.WebCrawlMd(ctx, contextdev.WebWebCrawlMdParams{
URL: "https://" + domain,
MaxPages: param.NewOpt(int64(100)),
})
if err != nil {
panic(err)
}
brand, err := client.Brand.Get(ctx, contextdev.BrandGetParams{OfByDomain: &contextdev.BrandGetParamsBodyByDomain{Domain: domain}})
if err != nil {
panic(err)
}
sg, err := client.Web.ExtractStyleguide(ctx, contextdev.WebExtractStyleguideParams{Domain: param.NewOpt(domain)})
if err != nil {
panic(err)
}
b, s := brand.Brand, sg.Styleguide
// Pick the light-background logo for the render step; every variant stays in b.Logos.
lightLogo := ""
for _, l := range b.Logos {
if l.Type == "logo" && l.Mode == "light" {
lightLogo = l.URL
break
}
}
// Identity (name, description, slogan, logos) plus the look (styleguide colors,
// fonts, spacing, shadows, components) make up the campaign design guide.
fmt.Printf("%s: %s\n", b.Title, b.Slogan)
fmt.Printf("%d logo variants, accent color %s, heading font %s, %d pages crawled\n",
len(b.Logos), s.Colors.Accent, s.Typography.Headings.H1.FontFamily, len(crawl.Results))
_ = b.Description
_ = lightLogo
_ = s.ElementSpacing
_ = s.Shadows
_ = s.Components
}
<?php
use ContextDev\Client;
$client = new Client(apiKey: getenv('CONTEXT_DEV_API_KEY'));
function gatherBrand(string $domain): array
{
global $client;
// The voice (whole-site crawl), the identity (brand), and the type (styleguide).
$crawl = $client->web->webCrawlMd(url: "https://{$domain}", maxPages: 100);
$brand = $client->brand->retrieve(type: 'by_domain', domain: $domain)->brand;
$sg = $client->web->extractStyleguide(domain: $domain)->styleguide;
if (!$brand || !$sg) {
throw new RuntimeException("No brand or styleguide data for {$domain}");
}
$logos = $brand->logos ?? [];
$lightLogo = null;
foreach ($logos as $l) {
if ($l->type === 'logo' && $l->mode === 'light') {
$lightLogo = $l->url;
break;
}
}
if (!$lightLogo && $logos) {
$lightLogo = $logos[0]->url;
}
$palette = array_values(array_filter([
$sg->colors->accent ?? null,
$sg->colors->background ?? null,
$sg->colors->text ?? null,
]));
return [
'designGuide' => [
'name' => $brand->title,
'description' => $brand->description,
'slogan' => $brand->slogan,
'palette' => $palette,
'logos' => array_map(fn ($l) => ['url' => $l->url, 'mode' => $l->mode, 'type' => $l->type], $logos),
'lightLogo' => $lightLogo,
'headingFont' => $sg->typography->headings->h1->fontFamily ?? null,
'bodyFont' => $sg->typography->p->fontFamily ?? null,
'spacing' => $sg->elementSpacing,
'shadows' => $sg->shadows,
'components' => $sg->components,
],
'pages' => array_map(
fn ($r) => ['url' => $r->metadata->url, 'markdown' => $r->markdown],
$crawl->results ?? [],
),
];
}
maxPages (default 100, max 500) and urlRegex to crawl fewer, more specific pages.
Step 2. Distill positioning and generate ideas
Next, we need the agent to come up with campaign ideas. Instead of trying to engineer the perfect prompt, we recommend using a skill. Run Corey Haines’ product-marketing-context on your coding agent with access to all the context we gathered in the last step. Make sure to specifically prompt it to generate 10 ideas for social media campaigns.// ideate.ts
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic(); // reads ANTHROPIC_API_KEY
const MODEL = "claude-opus-4-8"; // Opus 4.8 for quality; Sonnet 4.6 for cheaper drafts
// Corey Haines' product-marketing-context skill, loaded straight from its repo.
const SKILL_URL =
"https://raw.githubusercontent.com/coreyhaines31/marketingskills/refs/heads/main/skills/product-marketing/SKILL.md";
type Gathered = {
designGuide: { name: string; slogan?: string; description?: string };
pages: { url: string; markdown: string }[];
};
// Pass in everything gathered in Step 1; no extra Context.dev calls here.
export async function generateIdeas({ designGuide, pages }: Gathered): Promise<string> {
const skill = await fetch(SKILL_URL).then((r) => r.text());
const context = [
`Brand: ${designGuide.name} (${designGuide.slogan})`,
designGuide.description,
...pages.map((p) => `## ${p.url}\n${p.markdown}`),
].join("\n\n");
const message = await anthropic.messages.create({
model: MODEL,
max_tokens: 2000,
system: skill, // run the skill: it tells Claude how to build product-marketing context
messages: [
{
role: "user",
content: `${context}\n\nUsing the product-marketing context above, generate 10 distinct ideas for social media campaigns. One per line: the hook, then the angle.`,
},
],
});
return message.content.find((b) => b.type === "text")?.text ?? "";
}
# ideate.py
import requests
from anthropic import Anthropic
anthropic = Anthropic() # reads ANTHROPIC_API_KEY
MODEL = "claude-opus-4-8" # Opus 4.8 for quality; Sonnet 4.6 for cheaper drafts
# Corey Haines' product-marketing-context skill, loaded straight from its repo.
SKILL_URL = "https://raw.githubusercontent.com/coreyhaines31/marketingskills/refs/heads/main/skills/product-marketing/SKILL.md"
# Pass in everything gathered in Step 1; no extra Context.dev calls here.
def generate_ideas(gathered: dict) -> str:
skill = requests.get(SKILL_URL).text
guide = gathered["design_guide"]
context = "\n\n".join(
[
f"Brand: {guide['name']} ({guide['slogan']})",
guide["description"],
*[f"## {p['url']}\n{p['markdown']}" for p in gathered["pages"]],
]
)
message = anthropic.messages.create(
model=MODEL,
max_tokens=2000,
system=skill, # run the skill: it tells Claude how to build product-marketing context
messages=[{
"role": "user",
"content": f"{context}\n\nUsing the product-marketing context above, generate 10 distinct ideas for social media campaigns. One per line: the hook, then the angle.",
}],
)
return next((b.text for b in message.content if b.type == "text"), "")
# ideate.rb
require "net/http"
require "anthropic"
CLIENT = Anthropic::Client.new # reads ANTHROPIC_API_KEY
MODEL = "claude-opus-4-8" # Opus 4.8 for quality; Sonnet 4.6 for cheaper drafts
# Corey Haines' product-marketing-context skill, loaded straight from its repo.
SKILL_URL = "https://raw.githubusercontent.com/coreyhaines31/marketingskills/refs/heads/main/skills/product-marketing/SKILL.md"
# Pass in everything gathered in Step 1; no extra Context.dev calls here.
def generate_ideas(gathered)
skill = Net::HTTP.get(URI(SKILL_URL))
guide = gathered[:design_guide]
context = [
"Brand: #{guide[:name]} (#{guide[:slogan]})",
guide[:description],
*gathered[:pages].map { |p| "## #{p[:url]}\n#{p[:markdown]}" },
].join("\n\n")
message = CLIENT.messages.create(
model: MODEL,
max_tokens: 2000,
system: skill, # run the skill: it tells Claude how to build product-marketing context
messages: [{
role: "user",
content: "#{context}\n\nUsing the product-marketing context above, generate 10 distinct ideas for social media campaigns. One per line: the hook, then the angle.",
}]
)
message.content.find { |b| b.type.to_s == "text" }&.text.to_s
end
// ideate.go
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/option"
)
const ideateModel = "claude-opus-4-8" // Opus 4.8 for quality; Sonnet 4.6 for cheaper drafts
// Corey Haines' product-marketing-context skill, loaded straight from its repo.
const skillURL = "https://raw.githubusercontent.com/coreyhaines31/marketingskills/refs/heads/main/skills/product-marketing/SKILL.md"
type Page struct{ URL, Markdown string }
// Takes everything gathered in Step 1; no extra Context.dev calls here.
func generateIdeas(guide DesignGuide, pages []Page) (string, error) {
res, err := http.Get(skillURL)
if err != nil {
return "", err
}
defer res.Body.Close()
skill, _ := io.ReadAll(res.Body)
corpus := fmt.Sprintf("Brand: %s (%s)\n\n%s", guide.Name, guide.Slogan, guide.Description)
for _, p := range pages {
corpus += fmt.Sprintf("\n\n## %s\n%s", p.URL, p.Markdown)
}
client := anthropic.NewClient(option.WithAPIKey(os.Getenv("ANTHROPIC_API_KEY")))
msg, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.Model(ideateModel),
MaxTokens: 2000,
System: []anthropic.TextBlockParam{{Text: string(skill)}},
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock(
corpus + "\n\nUsing the product-marketing context above, generate 10 distinct ideas for social media campaigns. One per line: the hook, then the angle.")),
},
})
if err != nil {
return "", err
}
for _, b := range msg.Content {
if b.Type == "text" {
return b.Text, nil
}
}
return "", nil
}
<?php
// ideate.php
const MODEL = 'claude-opus-4-8'; // Opus 4.8 for quality; Sonnet 4.6 for cheaper drafts
// Corey Haines' product-marketing-context skill, loaded straight from its repo.
const SKILL_URL = 'https://raw.githubusercontent.com/coreyhaines31/marketingskills/refs/heads/main/skills/product-marketing/SKILL.md';
// Pass in everything gathered in Step 1; no extra Context.dev calls here.
function generateIdeas(array $gathered): string
{
$skill = file_get_contents(SKILL_URL);
$guide = $gathered['designGuide'];
$parts = [
"Brand: {$guide['name']} ({$guide['slogan']})",
$guide['description'],
];
foreach ($gathered['pages'] as $p) {
$parts[] = "## {$p['url']}\n{$p['markdown']}";
}
$context = implode("\n\n", $parts);
$payload = json_encode([
'model' => MODEL,
'max_tokens' => 2000,
'system' => $skill,
'messages' => [[
'role' => 'user',
'content' => "{$context}\n\nUsing the product-marketing context above, generate 10 distinct ideas for social media campaigns. One per line: the hook, then the angle.",
]],
]);
$ch = curl_init('https://api.anthropic.com/v1/messages');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'x-api-key: ' . getenv('ANTHROPIC_API_KEY'),
'anthropic-version: 2023-06-01',
],
CURLOPT_POSTFIELDS => $payload,
]);
$response = json_decode(curl_exec($ch), true);
foreach ($response['content'] ?? [] as $block) {
if (($block['type'] ?? '') === 'text') {
return $block['text'];
}
}
return '';
}
Step 3. Write the copy
Now you need to pick the ideas you like best. You can also fix or add new ideas. Then run Corey Haines’ copywriting skill on it, with a prompt specifying how you want the output.// copy.ts
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic(); // reads ANTHROPIC_API_KEY
const MODEL = "claude-opus-4-8";
// Corey Haines' copywriting skill, loaded straight from its repo.
const SKILL_URL =
"https://raw.githubusercontent.com/coreyhaines31/marketingskills/refs/heads/main/skills/copywriting/SKILL.md";
export type Copy = { headline: string; subhead: string; cta: string };
type Gathered = {
designGuide: { name: string; slogan?: string; description?: string };
pages: { url: string; markdown: string }[];
};
export async function writeCopy(idea: string, { designGuide, pages }: Gathered): Promise<Copy> {
const skill = await fetch(SKILL_URL).then((r) => r.text());
const context = [
`Brand: ${designGuide.name} (${designGuide.slogan})`,
designGuide.description,
...pages.map((p) => `## ${p.url}\n${p.markdown}`),
].join("\n\n");
const message = await anthropic.messages.create({
model: MODEL,
max_tokens: 1000,
system: skill, // run the skill: it writes on-brand copy from the context
messages: [
{
role: "user",
content: `${context}\n\nWrite a social ad for this campaign idea: "${idea}". Return only a JSON object with keys "headline", "subhead", and "cta".`,
},
],
});
const text = message.content.find((b) => b.type === "text")?.text ?? "{}";
return JSON.parse(text) as Copy;
}
# copy.py
import json
import requests
from anthropic import Anthropic
anthropic = Anthropic() # reads ANTHROPIC_API_KEY
MODEL = "claude-opus-4-8"
# Corey Haines' copywriting skill, loaded straight from its repo.
SKILL_URL = "https://raw.githubusercontent.com/coreyhaines31/marketingskills/refs/heads/main/skills/copywriting/SKILL.md"
# Returns {"headline", "subhead", "cta"} for the render step.
def write_copy(idea: str, gathered: dict) -> dict:
skill = requests.get(SKILL_URL).text
guide = gathered["design_guide"]
context = "\n\n".join(
[
f"Brand: {guide['name']} ({guide['slogan']})",
guide["description"],
*[f"## {p['url']}\n{p['markdown']}" for p in gathered["pages"]],
]
)
message = anthropic.messages.create(
model=MODEL,
max_tokens=1000,
system=skill, # run the skill: it writes on-brand copy from the context
messages=[{
"role": "user",
"content": f'{context}\n\nWrite a social ad for this campaign idea: "{idea}". Return only a JSON object with keys "headline", "subhead", and "cta".',
}],
)
text = next((b.text for b in message.content if b.type == "text"), "{}")
return json.loads(text)
# copy.rb
require "json"
require "net/http"
require "anthropic"
CLIENT = Anthropic::Client.new # reads ANTHROPIC_API_KEY
MODEL = "claude-opus-4-8"
# Corey Haines' copywriting skill, loaded straight from its repo.
SKILL_URL = "https://raw.githubusercontent.com/coreyhaines31/marketingskills/refs/heads/main/skills/copywriting/SKILL.md"
# Returns { headline:, subhead:, cta: } for the render step.
def write_copy(idea, gathered)
skill = Net::HTTP.get(URI(SKILL_URL))
guide = gathered[:design_guide]
context = [
"Brand: #{guide[:name]} (#{guide[:slogan]})",
guide[:description],
*gathered[:pages].map { |p| "## #{p[:url]}\n#{p[:markdown]}" },
].join("\n\n")
message = CLIENT.messages.create(
model: MODEL,
max_tokens: 1000,
system: skill, # run the skill: it writes on-brand copy from the context
messages: [{
role: "user",
content: %(#{context}\n\nWrite a social ad for this campaign idea: "#{idea}". Return only a JSON object with keys "headline", "subhead", and "cta".),
}]
)
text = message.content.find { |b| b.type.to_s == "text" }&.text.to_s
JSON.parse(text, symbolize_names: true)
end
// copy.go
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/option"
)
const copyModel = "claude-opus-4-8"
// Corey Haines' copywriting skill, loaded straight from its repo.
const copySkillURL = "https://raw.githubusercontent.com/coreyhaines31/marketingskills/refs/heads/main/skills/copywriting/SKILL.md"
type Copy struct {
Headline string `json:"headline"`
Subhead string `json:"subhead"`
CTA string `json:"cta"`
}
func writeCopy(idea string, guide DesignGuide, pages []Page) (Copy, error) {
res, err := http.Get(copySkillURL)
if err != nil {
return Copy{}, err
}
defer res.Body.Close()
skill, _ := io.ReadAll(res.Body)
corpus := fmt.Sprintf("Brand: %s (%s)\n\n%s", guide.Name, guide.Slogan, guide.Description)
for _, p := range pages {
corpus += fmt.Sprintf("\n\n## %s\n%s", p.URL, p.Markdown)
}
prompt := corpus + fmt.Sprintf(
"\n\nWrite a social ad for this campaign idea: %q. Return only a JSON object with keys \"headline\", \"subhead\", and \"cta\".", idea)
client := anthropic.NewClient(option.WithAPIKey(os.Getenv("ANTHROPIC_API_KEY")))
msg, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.Model(copyModel),
MaxTokens: 1000,
System: []anthropic.TextBlockParam{{Text: string(skill)}},
Messages: []anthropic.MessageParam{anthropic.NewUserMessage(anthropic.NewTextBlock(prompt))},
})
if err != nil {
return Copy{}, err
}
var out Copy
for _, b := range msg.Content {
if b.Type == "text" {
if err := json.Unmarshal([]byte(b.Text), &out); err != nil {
return Copy{}, err
}
break
}
}
return out, nil
}
<?php
// copy.php
const MODEL = 'claude-opus-4-8';
// Corey Haines' copywriting skill, loaded straight from its repo.
const SKILL_URL = 'https://raw.githubusercontent.com/coreyhaines31/marketingskills/refs/heads/main/skills/copywriting/SKILL.md';
/** @return array{headline: string, subhead: string, cta: string} */
function writeCopy(string $idea, array $gathered): array
{
$skill = file_get_contents(SKILL_URL);
$guide = $gathered['designGuide'];
$parts = [
"Brand: {$guide['name']} ({$guide['slogan']})",
$guide['description'],
];
foreach ($gathered['pages'] as $p) {
$parts[] = "## {$p['url']}\n{$p['markdown']}";
}
$context = implode("\n\n", $parts);
$payload = json_encode([
'model' => MODEL,
'max_tokens' => 1000,
'system' => $skill,
'messages' => [[
'role' => 'user',
'content' => "{$context}\n\nWrite a social ad for this campaign idea: \"{$idea}\". Return only a JSON object with keys \"headline\", \"subhead\", and \"cta\".",
]],
]);
$ch = curl_init('https://api.anthropic.com/v1/messages');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'x-api-key: ' . getenv('ANTHROPIC_API_KEY'),
'anthropic-version: 2023-06-01',
],
CURLOPT_POSTFIELDS => $payload,
]);
$response = json_decode(curl_exec($ch), true);
$text = '{}';
foreach ($response['content'] ?? [] as $block) {
if (($block['type'] ?? '') === 'text') {
$text = $block['text'];
break;
}
}
return json_decode($text, true) ?: ['headline' => '', 'subhead' => '', 'cta' => ''];
}
Step 4. Render the creative across sizes
Before we get an LLM to render the images, we need to specify the sizes. This script contains 6 of the most popular social media post sizes:- Instagram Story
- Instagram Square Post
- Instagram Portrait Post
- X image card
- LinkedIn Banner
- YouTube video thumbnail
// variants.ts
export type Variant = {
id: string;
platform: string;
width: number;
height: number;
layout: "square" | "portrait" | "wide";
};
export const VARIANTS: Variant[] = [
{ id: "ig-square", platform: "Instagram feed", width: 1080, height: 1080, layout: "square" },
{ id: "ig-portrait", platform: "Instagram portrait", width: 1080, height: 1350, layout: "portrait" },
{ id: "ig-story", platform: "Instagram story", width: 1080, height: 1920, layout: "portrait" },
{ id: "x-card", platform: "X card", width: 1200, height: 675, layout: "wide" },
{ id: "li-banner", platform: "LinkedIn banner", width: 1584, height: 396, layout: "wide" },
{ id: "yt-thumbnail", platform: "YouTube thumbnail", width: 1280, height: 720, layout: "wide" },
];
# variants.py
VARIANTS = [
{"id": "ig-square", "platform": "Instagram feed", "width": 1080, "height": 1080, "layout": "square"},
{"id": "ig-portrait", "platform": "Instagram portrait", "width": 1080, "height": 1350, "layout": "portrait"},
{"id": "ig-story", "platform": "Instagram story", "width": 1080, "height": 1920, "layout": "portrait"},
{"id": "x-card", "platform": "X card", "width": 1200, "height": 675, "layout": "wide"},
{"id": "li-banner", "platform": "LinkedIn banner", "width": 1584, "height": 396, "layout": "wide"},
{"id": "yt-thumbnail", "platform": "YouTube thumbnail", "width": 1280, "height": 720, "layout": "wide"},
]
# variants.rb
VARIANTS = [
{ id: "ig-square", platform: "Instagram feed", width: 1080, height: 1080, layout: "square" },
{ id: "ig-portrait", platform: "Instagram portrait", width: 1080, height: 1350, layout: "portrait" },
{ id: "ig-story", platform: "Instagram story", width: 1080, height: 1920, layout: "portrait" },
{ id: "x-card", platform: "X card", width: 1200, height: 675, layout: "wide" },
{ id: "li-banner", platform: "LinkedIn banner", width: 1584, height: 396, layout: "wide" },
{ id: "yt-thumbnail", platform: "YouTube thumbnail", width: 1280, height: 720, layout: "wide" },
].freeze
// variants.go
package main
type Variant struct {
ID string
Platform string
Width int
Height int
Layout string
}
var Variants = []Variant{
{"ig-square", "Instagram feed", 1080, 1080, "square"},
{"ig-portrait", "Instagram portrait", 1080, 1350, "portrait"},
{"ig-story", "Instagram story", 1080, 1920, "portrait"},
{"x-card", "X card", 1200, 675, "wide"},
{"li-banner", "LinkedIn banner", 1584, 396, "wide"},
{"yt-thumbnail", "YouTube thumbnail", 1280, 720, "wide"},
}
<?php
// variants.php
const VARIANTS = [
['id' => 'ig-square', 'platform' => 'Instagram feed', 'width' => 1080, 'height' => 1080, 'layout' => 'square'],
['id' => 'ig-portrait', 'platform' => 'Instagram portrait', 'width' => 1080, 'height' => 1350, 'layout' => 'portrait'],
['id' => 'ig-story', 'platform' => 'Instagram story', 'width' => 1080, 'height' => 1920, 'layout' => 'portrait'],
['id' => 'x-card', 'platform' => 'X card', 'width' => 1200, 'height' => 675, 'layout' => 'wide'],
['id' => 'li-banner', 'platform' => 'LinkedIn banner', 'width' => 1584, 'height' => 396, 'layout' => 'wide'],
['id' => 'yt-thumbnail', 'platform' => 'YouTube thumbnail', 'width' => 1280, 'height' => 720, 'layout' => 'wide'],
];
System prompt for the creative designer
// render.ts
import Anthropic from "@anthropic-ai/sdk";
import { VARIANTS } from "./variants";
const anthropic = new Anthropic(); // reads ANTHROPIC_API_KEY
const MODEL = "claude-opus-4-8";
type Gathered = {
designGuide: {
name: string;
slogan?: string;
description?: string;
palette: string[];
lightLogo?: string;
headingFont: string;
bodyFont: string;
};
};
const SYSTEM_PROMPT = `You are a senior brand designer. Build ONE self-contained HTML document that mocks up a social campaign across every artboard size provided.
- Render one artboard per size at its exact width and height, labeled with the platform name.
- Use ONLY the styleguide palette, fonts, and Brand API logo from the design guide. Inline all CSS and load nothing external except the logo URL and the html-to-image library.
- Write short, on-brand copy for the idea: a headline, a supporting line, and a CTA. Lead with the styleguide accent color and heading font.
- Give each artboard a "Download PNG" button that exports it at its exact size with html-to-image (https://cdn.jsdelivr.net/npm/html-to-image) and triggers a download.
Return only the HTML document, with no commentary or code fences.`;
// One agent per idea: lays out every size as an HTML/CSS/JS mockup.
export async function designMockup(idea: string, { designGuide }: Gathered): Promise<string> {
const message = await anthropic.messages.create({
model: MODEL,
max_tokens: 16000,
system: SYSTEM_PROMPT,
messages: [
{
role: "user",
content: [
`Brand: ${designGuide.name} (${designGuide.slogan}). ${designGuide.description}`,
`Palette (primary first): ${designGuide.palette.join(", ")}`,
`Heading font: ${designGuide.headingFont}. Body font: ${designGuide.bodyFont}. Logo: ${designGuide.lightLogo}`,
`Campaign idea: ${idea}`,
`Artboards (build one of each, at its exact size): ${JSON.stringify(VARIANTS)}`,
].join("\n"),
},
],
});
return message.content.find((b) => b.type === "text")?.text ?? "";
}
// Fan out across every idea from Step 2 in parallel.
export async function renderCampaign(ideas: string[], gathered: Gathered) {
return Promise.all(
ideas.map(async (idea) => ({ idea, html: await designMockup(idea, gathered) })),
);
}
# render.py
import json
from concurrent.futures import ThreadPoolExecutor
from anthropic import Anthropic
from variants import VARIANTS
anthropic = Anthropic() # reads ANTHROPIC_API_KEY
MODEL = "claude-opus-4-8"
SYSTEM_PROMPT = """You are a senior brand designer. Build ONE self-contained HTML document that mocks up a social campaign across every artboard size provided.
- Render one artboard per size at its exact width and height, labeled with the platform name.
- Use ONLY the styleguide palette, fonts, and Brand API logo from the design guide. Inline all CSS and load nothing external except the logo URL and the html-to-image library.
- Write short, on-brand copy for the idea: a headline, a supporting line, and a CTA. Lead with the styleguide accent color and heading font.
- Give each artboard a "Download PNG" button that exports it at its exact size with html-to-image (https://cdn.jsdelivr.net/npm/html-to-image) and triggers a download.
Return only the HTML document, with no commentary or code fences."""
# One agent per idea: lays out every size as an HTML/CSS/JS mockup.
def design_mockup(idea: str, gathered: dict) -> str:
guide = gathered["design_guide"]
prompt = "\n".join([
f"Brand: {guide['name']} ({guide['slogan']}). {guide['description']}",
f"Palette (primary first): {', '.join(guide['palette'])}",
f"Heading font: {guide['heading_font']}. Body font: {guide['body_font']}. Logo: {guide['light_logo']}",
f"Campaign idea: {idea}",
f"Artboards (build one of each, at its exact size): {json.dumps(VARIANTS)}",
])
message = anthropic.messages.create(
model=MODEL,
max_tokens=16000,
system=SYSTEM_PROMPT,
messages=[{"role": "user", "content": prompt}],
)
return next((b.text for b in message.content if b.type == "text"), "")
# Fan out across every idea from Step 2 in parallel.
def render_campaign(ideas: list[str], gathered: dict) -> list[dict]:
with ThreadPoolExecutor(max_workers=max(len(ideas), 1)) as pool:
htmls = list(pool.map(lambda idea: design_mockup(idea, gathered), ideas))
return [{"idea": idea, "html": html} for idea, html in zip(ideas, htmls)]
# render.rb
require "json"
require "anthropic"
require_relative "variants"
CLIENT = Anthropic::Client.new # reads ANTHROPIC_API_KEY
MODEL = "claude-opus-4-8"
SYSTEM_PROMPT = <<~PROMPT
You are a senior brand designer. Build ONE self-contained HTML document that mocks up a social campaign across every artboard size provided.
- Render one artboard per size at its exact width and height, labeled with the platform name.
- Use ONLY the styleguide palette, fonts, and Brand API logo from the design guide. Inline all CSS and load nothing external except the logo URL and the html-to-image library.
- Write short, on-brand copy for the idea: a headline, a supporting line, and a CTA. Lead with the styleguide accent color and heading font.
- Give each artboard a "Download PNG" button that exports it at its exact size with html-to-image (https://cdn.jsdelivr.net/npm/html-to-image) and triggers a download.
Return only the HTML document, with no commentary or code fences.
PROMPT
# One agent per idea: lays out every size as an HTML/CSS/JS mockup.
def design_mockup(idea, gathered)
guide = gathered[:design_guide]
prompt = [
"Brand: #{guide[:name]} (#{guide[:slogan]}). #{guide[:description]}",
"Palette (primary first): #{guide[:palette].join(', ')}",
"Heading font: #{guide[:heading_font]}. Body font: #{guide[:body_font]}. Logo: #{guide[:light_logo]}",
"Campaign idea: #{idea}",
"Artboards (build one of each, at its exact size): #{JSON.generate(VARIANTS)}",
].join("\n")
message = CLIENT.messages.create(
model: MODEL,
max_tokens: 16000,
system: SYSTEM_PROMPT,
messages: [{ role: "user", content: prompt }]
)
message.content.find { |b| b.type.to_s == "text" }&.text.to_s
end
# Fan out across every idea from Step 2 in parallel.
def render_campaign(ideas, gathered)
ideas.map { |idea| Thread.new { { idea: idea, html: design_mockup(idea, gathered) } } }
.map(&:value)
end
// render.go
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/option"
)
const renderModel = "claude-opus-4-8"
type DesignGuide struct {
Name string
Description string
Slogan string
Palette []string
LightLogo string
HeadingFont string
BodyFont string
}
const systemPrompt = `You are a senior brand designer. Build ONE self-contained HTML document that mocks up a social campaign across every artboard size provided.
- Render one artboard per size at its exact width and height, labeled with the platform name.
- Use ONLY the styleguide palette, fonts, and Brand API logo from the design guide. Inline all CSS and load nothing external except the logo URL and the html-to-image library.
- Write short, on-brand copy for the idea: a headline, a supporting line, and a CTA. Lead with the styleguide accent color and heading font.
- Give each artboard a "Download PNG" button that exports it at its exact size with html-to-image (https://cdn.jsdelivr.net/npm/html-to-image) and triggers a download.
Return only the HTML document, with no commentary or code fences.`
type Mockup struct {
Idea string
HTML string
}
// One agent per idea: lays out every size as an HTML/CSS/JS mockup.
func designMockup(idea string, guide DesignGuide) (string, error) {
artboards, _ := json.Marshal(Variants)
prompt := strings.Join([]string{
fmt.Sprintf("Brand: %s (%s). %s", guide.Name, guide.Slogan, guide.Description),
"Palette (primary first): " + strings.Join(guide.Palette, ", "),
fmt.Sprintf("Heading font: %s. Body font: %s. Logo: %s", guide.HeadingFont, guide.BodyFont, guide.LightLogo),
"Campaign idea: " + idea,
"Artboards (build one of each, at its exact size): " + string(artboards),
}, "\n")
client := anthropic.NewClient(option.WithAPIKey(os.Getenv("ANTHROPIC_API_KEY")))
msg, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.Model(renderModel),
MaxTokens: 16000,
System: []anthropic.TextBlockParam{{Text: systemPrompt}},
Messages: []anthropic.MessageParam{anthropic.NewUserMessage(anthropic.NewTextBlock(prompt))},
})
if err != nil {
return "", err
}
for _, b := range msg.Content {
if b.Type == "text" {
return b.Text, nil
}
}
return "", nil
}
// Fan out across every idea from Step 2 in parallel.
func renderCampaign(ideas []string, guide DesignGuide) ([]Mockup, error) {
mockups := make([]Mockup, len(ideas))
errs := make([]error, len(ideas))
var wg sync.WaitGroup
for i, idea := range ideas {
wg.Add(1)
go func(i int, idea string) {
defer wg.Done()
html, err := designMockup(idea, guide)
mockups[i] = Mockup{Idea: idea, HTML: html}
errs[i] = err
}(i, idea)
}
wg.Wait()
for _, err := range errs {
if err != nil {
return nil, err
}
}
return mockups, nil
}
<?php
// render.php
require_once 'variants.php';
const MODEL = 'claude-opus-4-8';
const SYSTEM_PROMPT = <<<'PROMPT'
You are a senior brand designer. Build ONE self-contained HTML document that mocks up a social campaign across every artboard size provided.
- Render one artboard per size at its exact width and height, labeled with the platform name.
- Use ONLY the styleguide palette, fonts, and Brand API logo from the design guide. Inline all CSS and load nothing external except the logo URL and the html-to-image library.
- Write short, on-brand copy for the idea: a headline, a supporting line, and a CTA. Lead with the styleguide accent color and heading font.
- Give each artboard a "Download PNG" button that exports it at its exact size with html-to-image (https://cdn.jsdelivr.net/npm/html-to-image) and triggers a download.
Return only the HTML document, with no commentary or code fences.
PROMPT;
// One agent per idea: lays out every size as an HTML/CSS/JS mockup.
function designMockup(string $idea, array $gathered): string
{
$guide = $gathered['designGuide'];
$prompt = implode("\n", [
"Brand: {$guide['name']} ({$guide['slogan']}). {$guide['description']}",
'Palette (primary first): ' . implode(', ', $guide['palette']),
"Heading font: {$guide['headingFont']}. Body font: {$guide['bodyFont']}. Logo: {$guide['lightLogo']}",
"Campaign idea: {$idea}",
'Artboards (build one of each, at its exact size): ' . json_encode(VARIANTS),
]);
$payload = json_encode([
'model' => MODEL,
'max_tokens' => 16000,
'system' => SYSTEM_PROMPT,
'messages' => [['role' => 'user', 'content' => $prompt]],
]);
$ch = curl_init('https://api.anthropic.com/v1/messages');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'x-api-key: ' . getenv('ANTHROPIC_API_KEY'),
'anthropic-version: 2023-06-01',
],
CURLOPT_POSTFIELDS => $payload,
]);
$response = json_decode(curl_exec($ch), true);
foreach ($response['content'] ?? [] as $block) {
if (($block['type'] ?? '') === 'text') {
return $block['text'];
}
}
return '';
}
// Fan out across every idea from Step 2 in parallel.
function renderCampaign(array $ideas, array $gathered): array
{
return array_map(
fn (string $idea) => ['idea' => $idea, 'html' => designMockup($idea, $gathered)],
$ideas,
);
}
Next steps
Web Scraping API
Crawl a whole site to clean Markdown, the corpus the positioning skill reads.
Styleguide API
Fonts, colors, and component CSS for the design guide.
product-marketing-context skill
Turns the crawl into positioning, audience, and brand voice.
copywriting skill
Writes on-brand copy from that positioning.