> ## Documentation Index
> Fetch the complete documentation index at: https://docs.context.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Generate Branded Campaign Assets at Scale

> Run an automated marketing pipeline that turns a single domain into on-brand social media posts, ad creative, and campaign assets at scale.

export const CampaignAssetsDemo = () => {
  const TRY = ["airbnb.com", "spotify.com", "duolingo.com", "figma.com"];
  const DEFAULT_DOMAIN = "airbnb.com";
  const DEFAULT_HTML = '<div style="display: flex; flex-direction: column; gap: 12px; max-width: 560px; margin: 0 auto; font-family: \'Circular\', -apple-system, BlinkMacSystemFont, \'Segoe UI\', Helvetica, Arial, sans-serif;">\n\n  <div style="aspect-ratio: 1/1; position: relative; background: radial-gradient(circle at 80% 10%, #fc9cb4 0%, #fc3c5c 45%, #c91e3f 100%); border-radius: 20px; overflow: hidden; padding: 28px; box-sizing: border-box; display: flex; flex-direction: column; justify-content: space-between;">\n    <div style="position: absolute; top: 14px; left: 14px; background: rgba(0,0,0,0.3); color: #fff; font-size: 10px; letter-spacing: 1.5px; font-family: ui-monospace, Menlo, monospace; padding: 5px 9px; border-radius: 999px; z-index: 5;">1:1 · INSTAGRAM POST</div>\n    <div style="position: absolute; bottom: -180px; right: -180px; width: 480px; height: 480px; border-radius: 50%; border: 1px solid rgba(255,255,255,0.18);"></div>\n    <div style="position: absolute; bottom: -120px; right: -120px; width: 360px; height: 360px; border-radius: 50%; border: 1px solid rgba(255,255,255,0.22);"></div>\n    <div style="position: absolute; bottom: -60px; right: -60px; width: 240px; height: 240px; border-radius: 50%; border: 1px solid rgba(255,255,255,0.28);"></div>\n    <div style="display: flex; justify-content: flex-end; align-items: flex-start; margin-top: 18px;">\n      <img src="https://media.brand.dev/4ae9a10b-ffde-45c8-b5c1-c1e7cef5b716.png" alt="Airbnb" style="width: 44px; height: 44px; display: block;" />\n    </div>\n    <div style="position: relative; z-index: 2;">\n      <div style="color: rgba(255,255,255,0.75); font-size: 12px; letter-spacing: 3px; text-transform: uppercase; margin-bottom: 14px;">— A place for every you</div>\n      <div style="color: #fff; font-size: 64px; line-height: 0.95; font-weight: 800; letter-spacing: -2.5px;">Belong<br/>Anywhere.</div>\n      <div style="margin-top: 22px; display: inline-flex; align-items: center; gap: 8px; background: #fff; color: #fc3c5c; padding: 10px 18px; border-radius: 999px; font-size: 13px; font-weight: 700;">Open the door →</div>\n    </div>\n  </div>\n\n  <div style="aspect-ratio: 16/9; position: relative; background: #fc9cb4; border-radius: 20px; overflow: hidden; padding: 28px; box-sizing: border-box; display: flex; flex-direction: column; justify-content: space-between;">\n    <div style="position: absolute; top: 14px; left: 14px; background: rgba(0,0,0,0.3); color: #fff; font-size: 10px; letter-spacing: 1.5px; font-family: ui-monospace, Menlo, monospace; padding: 5px 9px; border-radius: 999px; z-index: 5;">16:9 · X CARD</div>\n    <div style="position: absolute; top: -80px; right: -80px; width: 280px; height: 280px; border-radius: 50%; background: radial-gradient(circle, #fff 0%, rgba(255,255,255,0) 65%); opacity: 0.55;"></div>\n    <div style="display: flex; align-items: center; gap: 10px; margin-top: 18px; position: relative; z-index: 2;">\n      <img src="https://media.brand.dev/4ae9a10b-ffde-45c8-b5c1-c1e7cef5b716.png" alt="Airbnb" style="width: 36px; height: 36px; display: block;" />\n      <div style="display: flex; flex-direction: column;">\n        <span style="color: #5a1029; font-weight: 700; font-size: 14px; letter-spacing: -0.3px;">Airbnb</span>\n        <span style="color: rgba(90,16,41,0.65); font-size: 12px;">@airbnb</span>\n      </div>\n    </div>\n    <div style="position: relative; z-index: 2; max-width: 80%;">\n      <div style="color: #4a0c22; font-size: 44px; line-height: 1; font-weight: 800; letter-spacing: -1.8px;">Unpack your<br/><em style="font-style: italic; font-weight: 500; color: #fc3c5c;">someday.</em></div>\n      <div style="margin-top: 14px; color: rgba(74,12,34,0.7); font-size: 13px; letter-spacing: 0.2px;">7M+ homes. 220 countries. One key.</div>\n    </div>\n  </div>\n\n  <div style="aspect-ratio: 16/5; position: relative; background: linear-gradient(95deg, #fb5c74 0%, #fc3c5c 60%, #fc9cb4 100%); border-radius: 20px; overflow: hidden; padding: 24px 28px; box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: 16px;">\n    <div style="position: absolute; top: 12px; left: 12px; background: rgba(0,0,0,0.3); color: #fff; font-size: 10px; letter-spacing: 1.5px; font-family: ui-monospace, Menlo, monospace; padding: 5px 9px; border-radius: 999px; z-index: 5;">16:5 · HERO</div>\n    <div style="position: absolute; inset: 0; background-image: repeating-linear-gradient(115deg, rgba(255,255,255,0.06) 0 1px, transparent 1px 32px); pointer-events: none;"></div>\n    <div style="display: flex; align-items: center; gap: 12px; position: relative; z-index: 2; flex-shrink: 0;">\n      <img src="https://media.brand.dev/4ae9a10b-ffde-45c8-b5c1-c1e7cef5b716.png" alt="Airbnb" style="width: 38px; height: 38px; display: block;" />\n      <div style="color: #fff; font-size: 18px; font-weight: 700; letter-spacing: -0.5px;">Airbnb</div>\n    </div>\n    <div style="position: relative; z-index: 2; text-align: center; flex: 1;">\n      <div style="color: #fff; font-size: 28px; font-weight: 800; line-height: 1; letter-spacing: -1px;">Stay somewhere<br/><span style="font-style: italic; font-weight: 500;">that stays with you.</span></div>\n    </div>\n    <a href="https://airbnb.com" style="position: relative; z-index: 2; flex-shrink: 0; display: inline-flex; align-items: center; gap: 6px; background: #fff; color: #fc3c5c; padding: 12px 20px; border-radius: 999px; font-size: 13px; font-weight: 700; text-decoration: none; letter-spacing: -0.2px; box-shadow: 0 6px 18px rgba(0,0,0,0.15);">Learn more →</a>\n  </div>\n\n</div>';
  const DEFAULT_BRAND = {
    title: "Airbnb",
    domain: "airbnb.com",
    colors: [{
      hex: "#fc3c5c"
    }, {
      hex: "#fc9cb4"
    }, {
      hex: "#fb5c74"
    }]
  };
  const STATIC = {
    "airbnb.com": {
      html: DEFAULT_HTML,
      brand: DEFAULT_BRAND
    },
    "spotify.com": {
      html: '<div style="display: flex; flex-direction: column; gap: 12px; max-width: 560px; margin: 0 auto; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Helvetica, Arial, sans-serif;">\n\n  <!-- 1. STORY 1:1 -->\n  <div style="position: relative; width: 100%; aspect-ratio: 1/1; background: linear-gradient(135deg, #1cd462 0%, #17b854 50%, #207f3f 100%); border-radius: 16px; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; padding: 32px; box-sizing: border-box;">\n    <!-- Aspect label -->\n    <div style="position: absolute; top: 12px; left: 12px; background: rgba(0,0,0,0.3); color: #fff; font-size: 10px; font-family: \'Courier New\', Courier, monospace; letter-spacing: 0.08em; padding: 4px 10px; border-radius: 20px; text-transform: uppercase; z-index: 10;">1:1 · INSTAGRAM POST</div>\n\n    <!-- Decorative circles -->\n    <div style="position: absolute; bottom: -80px; right: -80px; width: 320px; height: 320px; border-radius: 50%; background: rgba(0,0,0,0.12);"></div>\n    <div style="position: absolute; bottom: 40px; right: 40px; width: 180px; height: 180px; border-radius: 50%; background: rgba(0,0,0,0.08);"></div>\n    <div style="position: absolute; top: -60px; left: -60px; width: 220px; height: 220px; border-radius: 50%; background: rgba(255,255,255,0.07);"></div>\n\n    <!-- Top: icon -->\n    <div style="display: flex; align-items: center; gap: 10px; z-index: 1;">\n      <img src="https://media.brand.dev/b3a3572c-6834-4800-b17c-6a3ee0c063d8.png" alt="Spotify" style="width: 48px; height: 48px; border-radius: 50%; object-fit: contain;" />\n      <span style="color: #fff; font-size: 18px; font-weight: 700; letter-spacing: -0.02em;">Spotify</span>\n    </div>\n\n    <!-- Center headline -->\n    <div style="z-index: 1;">\n      <div style="color: #fff; font-size: 48px; font-weight: 800; line-height: 1.1; letter-spacing: -0.03em; margin-bottom: 16px;">Your world.<br/>Soundtrack<br/>included.</div>\n      <div style="color: rgba(255,255,255,0.85); font-size: 15px; font-weight: 400; line-height: 1.5; max-width: 280px;">Music, podcasts, and audiobooks for every moment.</div>\n    </div>\n\n    <!-- Bottom CTA -->\n    <div style="z-index: 1;">\n      <div style="display: inline-block; background: #fff; color: #1a1a1a; font-size: 14px; font-weight: 700; padding: 12px 28px; border-radius: 999px; letter-spacing: 0.02em;">Listen Free</div>\n    </div>\n  </div>\n\n  <!-- 2. X CARD 16:9 -->\n  <div style="position: relative; width: 100%; aspect-ratio: 16/9; background: linear-gradient(120deg, #207f3f 0%, #185e2e 100%); border-radius: 16px; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; padding: 28px 32px; box-sizing: border-box;">\n    <!-- Aspect label -->\n    <div style="position: absolute; top: 12px; left: 12px; background: rgba(0,0,0,0.3); color: #fff; font-size: 10px; font-family: \'Courier New\', Courier, monospace; letter-spacing: 0.08em; padding: 4px 10px; border-radius: 20px; text-transform: uppercase; z-index: 10;">16:9 · X CARD</div>\n\n    <!-- Decorative -->\n    <div style="position: absolute; top: -60px; right: -60px; width: 280px; height: 280px; border-radius: 50%; background: rgba(28,212,98,0.15);"></div>\n    <div style="position: absolute; bottom: -40px; right: 80px; width: 160px; height: 160px; border-radius: 50%; background: rgba(28,212,98,0.1);"></div>\n    <div style="position: absolute; bottom: -80px; left: -40px; width: 220px; height: 220px; border-radius: 50%; background: rgba(255,255,255,0.04);"></div>\n\n    <!-- Top: icon + handle -->\n    <div style="display: flex; align-items: center; gap: 10px; z-index: 1; margin-top: 16px;">\n      <img src="https://media.brand.dev/b3a3572c-6834-4800-b17c-6a3ee0c063d8.png" alt="Spotify" style="width: 36px; height: 36px; border-radius: 50%; object-fit: contain;" />\n      <span style="color: rgba(255,255,255,0.75); font-size: 14px; font-weight: 600; letter-spacing: 0.01em;">@Spotify</span>\n    </div>\n\n    <!-- Main copy -->\n    <div style="z-index: 1; flex: 1; display: flex; flex-direction: column; justify-content: center;">\n      <div style="color: #1cd462; font-size: 13px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: 10px;">Now Streaming</div>\n      <div style="color: #fff; font-size: 36px; font-weight: 800; line-height: 1.1; letter-spacing: -0.03em; max-width: 380px;">Every beat.<br/>Every story.<br/>Every moment.</div>\n    </div>\n\n    <!-- Bottom tagline -->\n    <div style="z-index: 1; display: flex; align-items: center; justify-content: space-between;">\n      <div style="color: rgba(255,255,255,0.6); font-size: 13px;">spotify.com</div>\n      <div style="background: #1cd462; color: #0d3d1e; font-size: 13px; font-weight: 700; padding: 8px 20px; border-radius: 999px;">Stream Now</div>\n    </div>\n  </div>\n\n  <!-- 3. HERO BANNER 16:5 -->\n  <div style="position: relative; width: 100%; aspect-ratio: 16/5; background: linear-gradient(90deg, #1cd462 0%, #18c058 40%, #207f3f 100%); border-radius: 16px; overflow: hidden; display: flex; align-items: center; justify-content: space-between; padding: 0 32px; box-sizing: border-box;">\n    <!-- Aspect label -->\n    <div style="position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.3); color: #fff; font-size: 10px; font-family: \'Courier New\', Courier, monospace; letter-spacing: 0.08em; padding: 4px 10px; border-radius: 20px; text-transform: uppercase; z-index: 10;">16:5 · HERO BANNER</div>\n\n    <!-- Decorative -->\n    <div style="position: absolute; top: -50px; right: 200px; width: 160px; height: 160px; border-radius: 50%; background: rgba(0,0,0,0.08);"></div>\n    <div style="position: absolute; bottom: -60px; left: 50%; width: 180px; height: 180px; border-radius: 50%; background: rgba(255,255,255,0.07);"></div>\n\n    <!-- Left: icon + name -->\n    <div style="display: flex; align-items: center; gap: 10px; z-index: 1; flex-shrink: 0;">\n      <img src="https://media.brand.dev/b3a3572c-6834-4800-b17c-6a3ee0c063d8.png" alt="Spotify" style="width: 38px; height: 38px; border-radius: 50%; object-fit: contain;" />\n      <span style="color: #fff; font-size: 18px; font-weight: 800; letter-spacing: -0.02em;">Spotify</span>\n    </div>\n\n    <!-- Center: headline -->\n    <div style="z-index: 1; text-align: center; flex: 1; padding: 0 20px;">\n      <div style="color: #fff; font-size: 22px; font-weight: 800; letter-spacing: -0.03em; line-height: 1.15; white-space: nowrap;">Sound for every single moment.</div>\n    </div>\n\n    <!-- Right: CTA pill -->\n    <div style="z-index: 1; flex-shrink: 0;">\n      <div style="display: inline-flex; align-items: center; gap: 6px; background: #fff; color: #0d3d1e; font-size: 14px; font-weight: 700; padding: 10px 22px; border-radius: 999px; white-space: nowrap; letter-spacing: 0.01em;">Learn more →</div>\n    </div>\n  </div>\n\n</div>',
      brand: {
        title: "Spotify",
        domain: "spotify.com",
        colors: [{
          hex: "#1cd462"
        }, {
          hex: "#207f3f"
        }]
      }
    },
    "duolingo.com": {
      html: '<div style="display: flex; flex-direction: column; gap: 12px; max-width: 560px; margin: 0 auto; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Helvetica, Arial, sans-serif;">\n\n  <!-- 1. STORY 1:1 -->\n  <div style="position: relative; aspect-ratio: 1/1; background: #60ca10; border-radius: 16px; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; padding: 28px;">\n    <!-- corner label -->\n    <div style="position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.3); color: #fff; font-size: 9px; font-family: \'Courier New\', Courier, monospace; letter-spacing: 0.08em; text-transform: uppercase; padding: 3px 8px; border-radius: 20px; z-index: 10;">1:1 · INSTAGRAM POST</div>\n    <!-- top: icon -->\n    <div style="display: flex; align-items: center; gap: 10px; margin-top: 10px;">\n      <img src="https://media.brand.dev/4d9e47cc-413c-4036-a271-5c8abfd56c76.jpg" alt="Duolingo icon" style="width: 52px; height: 52px; border-radius: 14px; object-fit: cover; box-shadow: 0 2px 8px rgba(0,0,0,0.18);" />\n    </div>\n    <!-- center: headline -->\n    <div style="flex: 1; display: flex; flex-direction: column; justify-content: center; padding: 12px 0;">\n      <div style="font-size: 46px; font-weight: 900; color: #182d24; line-height: 1.05; letter-spacing: -1.5px;">Learn any<br/>language.<br/><span style="color: #fff;">Free.</span></div>\n    </div>\n    <!-- bottom: slogan + decorative bar -->\n    <div>\n      <div style="width: 48px; height: 4px; background: #182d24; border-radius: 4px; margin-bottom: 12px;"></div>\n      <div style="font-size: 14px; color: #182d24; font-weight: 600; letter-spacing: 0.02em; opacity: 0.85;">Free language education for the world</div>\n      <div style="margin-top: 10px;">\n        <img src="https://media.brand.dev/391dfb37-c3cb-488d-b137-e1f240e6a4c6.svg" alt="Duolingo wordmark" style="height: 22px; filter: brightness(0) saturate(100%) invert(11%) sepia(26%) saturate(800%) hue-rotate(100deg) brightness(92%);" />\n      </div>\n    </div>\n    <!-- decorative circle -->\n    <div style="position: absolute; bottom: -60px; right: -60px; width: 200px; height: 200px; background: rgba(24,45,36,0.12); border-radius: 50%;"></div>\n    <div style="position: absolute; top: 60px; right: -30px; width: 120px; height: 120px; background: rgba(255,255,255,0.1); border-radius: 50%;"></div>\n  </div>\n\n  <!-- 2. X CARD 16:9 -->\n  <div style="position: relative; aspect-ratio: 16/9; background: #e8cc8a; border-radius: 16px; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; padding: 24px 28px;">\n    <!-- corner label -->\n    <div style="position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.3); color: #fff; font-size: 9px; font-family: \'Courier New\', Courier, monospace; letter-spacing: 0.08em; text-transform: uppercase; padding: 3px 8px; border-radius: 20px; z-index: 10;">16:9 · X CARD</div>\n    <!-- top row: icon + handle -->\n    <div style="display: flex; align-items: center; gap: 10px; margin-top: 10px;">\n      <img src="https://media.brand.dev/4d9e47cc-413c-4036-a271-5c8abfd56c76.jpg" alt="Duolingo icon" style="width: 40px; height: 40px; border-radius: 10px; object-fit: cover; box-shadow: 0 2px 6px rgba(0,0,0,0.15);" />\n      <div>\n        <div style="font-size: 13px; font-weight: 800; color: #182d24; letter-spacing: -0.2px;">Duolingo</div>\n        <div style="font-size: 11px; color: #182d24; opacity: 0.6; font-weight: 500;">@duolingo</div>\n      </div>\n    </div>\n    <!-- center: headline -->\n    <div style="flex: 1; display: flex; align-items: center;">\n      <div style="font-size: 36px; font-weight: 900; color: #182d24; line-height: 1.1; letter-spacing: -1px; max-width: 75%;">The world learns<br/>with <span style="color: #60ca10; -webkit-text-stroke: 1px #182d24;">Duolingo.</span></div>\n    </div>\n    <!-- bottom: wordmark -->\n    <div style="display: flex; align-items: center; justify-content: space-between;">\n      <img src="https://media.brand.dev/391dfb37-c3cb-488d-b137-e1f240e6a4c6.svg" alt="Duolingo wordmark" style="height: 18px; filter: brightness(0) saturate(100%) invert(11%) sepia(26%) saturate(800%) hue-rotate(100deg) brightness(92%);" />\n      <div style="font-size: 11px; color: #182d24; opacity: 0.55; font-weight: 500;">duolingo.com</div>\n    </div>\n    <!-- decorative shapes -->\n    <div style="position: absolute; top: -40px; right: -40px; width: 160px; height: 160px; background: rgba(96,202,16,0.18); border-radius: 50%;"></div>\n    <div style="position: absolute; bottom: -30px; right: 80px; width: 90px; height: 90px; background: rgba(24,45,36,0.08); border-radius: 50%;"></div>\n  </div>\n\n  <!-- 3. HERO BANNER 16:5 -->\n  <div style="position: relative; aspect-ratio: 16/5; background: #182d24; border-radius: 16px; overflow: hidden; display: flex; align-items: center; justify-content: space-between; padding: 0 28px;">\n    <!-- corner label -->\n    <div style="position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.3); color: #fff; font-size: 9px; font-family: \'Courier New\', Courier, monospace; letter-spacing: 0.08em; text-transform: uppercase; padding: 3px 8px; border-radius: 20px; z-index: 10;">16:5 · HERO BANNER</div>\n    <!-- left: icon + name -->\n    <div style="display: flex; align-items: center; gap: 10px; flex-shrink: 0;">\n      <img src="https://media.brand.dev/4d9e47cc-413c-4036-a271-5c8abfd56c76.jpg" alt="Duolingo icon" style="width: 38px; height: 38px; border-radius: 10px; object-fit: cover; box-shadow: 0 2px 8px rgba(0,0,0,0.3);" />\n      <img src="https://media.brand.dev/391dfb37-c3cb-488d-b137-e1f240e6a4c6.svg" alt="Duolingo wordmark" style="height: 16px; filter: brightness(0) invert(1);" />\n    </div>\n    <!-- center: headline -->\n    <div style="flex: 1; text-align: center; padding: 0 16px;">\n      <div style="font-size: 20px; font-weight: 900; color: #fff; letter-spacing: -0.5px; line-height: 1.15;">Free language education<br/><span style="color: #60ca10;">for the world.</span></div>\n    </div>\n    <!-- right: CTA pill -->\n    <div style="flex-shrink: 0;">\n      <div style="background: #60ca10; color: #182d24; font-size: 13px; font-weight: 800; padding: 10px 20px; border-radius: 50px; letter-spacing: 0.01em; white-space: nowrap; box-shadow: 0 2px 10px rgba(96,202,16,0.35);">Learn more →</div>\n    </div>\n    <!-- decorative elements -->\n    <div style="position: absolute; left: -30px; top: 50%; transform: translateY(-50%); width: 100px; height: 100px; background: rgba(96,202,16,0.08); border-radius: 50%;"></div>\n    <div style="position: absolute; right: 120px; bottom: -30px; width: 80px; height: 80px; background: rgba(232,204,138,0.07); border-radius: 50%;"></div>\n  </div>\n\n</div>',
      brand: {
        title: "Duolingo",
        domain: "duolingo.com",
        colors: [{
          hex: "#60ca10"
        }, {
          hex: "#e8cc8a"
        }, {
          hex: "#182d24"
        }]
      }
    },
    "figma.com": {
      html: '<div style="display: flex; flex-direction: column; gap: 12px; max-width: 560px; margin: 0 auto; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Helvetica, Arial, sans-serif;">\n\n  <!-- 1. STORY 1:1 -->\n  <div style="position: relative; aspect-ratio: 1/1; background: #f76040; border-radius: 16px; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; padding: 28px;">\n    <!-- Aspect label -->\n    <div style="position: absolute; top: 12px; left: 12px; background: rgba(0,0,0,0.3); color: #fff; font-size: 10px; font-family: \'Courier New\', Courier, monospace; font-weight: 700; letter-spacing: 0.08em; padding: 4px 10px; border-radius: 20px; text-transform: uppercase; z-index: 10;">1:1 · INSTAGRAM POST</div>\n    <!-- Brand icon top-left (below label) -->\n    <div style="margin-top: 36px; display: flex; align-items: center; gap: 10px;">\n      <img src="https://media.brand.dev/c219dc98-9ece-432d-a19a-173b64c97c8f.jpg" alt="Figma icon" style="width: 48px; height: 48px; border-radius: 10px; object-fit: cover;" />\n    </div>\n    <!-- Center decorative blob -->\n    <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 260px; height: 260px; background: rgba(255,255,255,0.07); border-radius: 50%; pointer-events: none;"></div>\n    <div style="position: absolute; top: 42%; left: 44%; transform: translate(-50%, -50%); width: 160px; height: 160px; background: rgba(163,92,251,0.18); border-radius: 50%; pointer-events: none;"></div>\n    <!-- Main copy -->\n    <div style="z-index: 2;">\n      <div style="color: #fff; font-size: 38px; font-weight: 800; line-height: 1.1; letter-spacing: -0.03em; margin-bottom: 14px;">Design.<br/>Collaborate.<br/>Ship faster.</div>\n      <div style="color: rgba(255,255,255,0.85); font-size: 14px; font-weight: 400; line-height: 1.5; max-width: 280px;">Design, prototype, and build faster with Figma\'s collaborative platform.</div>\n      <!-- CTA -->\n      <div style="margin-top: 20px; display: inline-block; background: #fff; color: #f76040; font-size: 13px; font-weight: 700; padding: 9px 20px; border-radius: 30px; letter-spacing: 0.02em;">Try Figma Free →</div>\n    </div>\n  </div>\n\n  <!-- 2. X CARD 16:9 -->\n  <div style="position: relative; aspect-ratio: 16/9; background: #13c5be; border-radius: 16px; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; padding: 24px 28px;">\n    <!-- Aspect label -->\n    <div style="position: absolute; top: 12px; left: 12px; background: rgba(0,0,0,0.3); color: #fff; font-size: 10px; font-family: \'Courier New\', Courier, monospace; font-weight: 700; letter-spacing: 0.08em; padding: 4px 10px; border-radius: 20px; text-transform: uppercase; z-index: 10;">16:9 · X CARD</div>\n    <!-- Decorative circles -->\n    <div style="position: absolute; right: -40px; bottom: -60px; width: 280px; height: 280px; background: rgba(255,255,255,0.08); border-radius: 50%; pointer-events: none;"></div>\n    <div style="position: absolute; right: 60px; bottom: -100px; width: 200px; height: 200px; background: rgba(163,92,251,0.15); border-radius: 50%; pointer-events: none;"></div>\n    <!-- Icon + handle top-left -->\n    <div style="display: flex; align-items: center; gap: 10px; margin-top: 28px; z-index: 2;">\n      <img src="https://media.brand.dev/c219dc98-9ece-432d-a19a-173b64c97c8f.jpg" alt="Figma icon" style="width: 36px; height: 36px; border-radius: 8px; object-fit: cover;" />\n      <span style="color: rgba(255,255,255,0.9); font-size: 13px; font-weight: 600; letter-spacing: 0.01em;">@figma</span>\n    </div>\n    <!-- Headline -->\n    <div style="z-index: 2; padding-bottom: 8px;">\n      <div style="color: #fff; font-size: 30px; font-weight: 800; line-height: 1.15; letter-spacing: -0.03em; max-width: 380px;">One platform.<br/>Infinite possibilities.</div>\n      <div style="margin-top: 12px; color: rgba(255,255,255,0.8); font-size: 13px; font-weight: 400; max-width: 340px; line-height: 1.5;">Design, prototype, and build — all in one collaborative workspace.</div>\n    </div>\n  </div>\n\n  <!-- 3. HERO BANNER 16:5 -->\n  <div style="position: relative; aspect-ratio: 16/5; background: #a35cfb; border-radius: 16px; overflow: hidden; display: flex; align-items: center; justify-content: space-between; padding: 0 28px;">\n    <!-- Aspect label -->\n    <div style="position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.3); color: #fff; font-size: 10px; font-family: \'Courier New\', Courier, monospace; font-weight: 700; letter-spacing: 0.08em; padding: 4px 10px; border-radius: 20px; text-transform: uppercase; z-index: 10;">16:5 · HERO BANNER</div>\n    <!-- Decorative shapes -->\n    <div style="position: absolute; left: -30px; top: -40px; width: 180px; height: 180px; background: rgba(247,96,64,0.2); border-radius: 50%; pointer-events: none;"></div>\n    <div style="position: absolute; right: 120px; bottom: -50px; width: 140px; height: 140px; background: rgba(19,197,190,0.15); border-radius: 50%; pointer-events: none;"></div>\n    <!-- Left: icon + wordmark -->\n    <div style="display: flex; align-items: center; gap: 10px; z-index: 2; flex-shrink: 0;">\n      <img src="https://media.brand.dev/c219dc98-9ece-432d-a19a-173b64c97c8f.jpg" alt="Figma icon" style="width: 34px; height: 34px; border-radius: 7px; object-fit: cover;" />\n      <img src="https://media.brand.dev/e90d762b-bcf9-479a-a6bd-1a2a00badab7.svg" alt="Figma" style="height: 20px; object-fit: contain;" />\n    </div>\n    <!-- Center: headline -->\n    <div style="z-index: 2; text-align: center; flex: 1; padding: 0 20px;">\n      <div style="color: #fff; font-size: 20px; font-weight: 800; letter-spacing: -0.02em; line-height: 1.2; white-space: nowrap;">Build together. Ship faster.</div>\n    </div>\n    <!-- Right: CTA pill -->\n    <div style="z-index: 2; flex-shrink: 0;">\n      <div style="background: #fff; color: #a35cfb; font-size: 13px; font-weight: 700; padding: 10px 20px; border-radius: 30px; white-space: nowrap; letter-spacing: 0.01em;">Learn more →</div>\n    </div>\n  </div>\n\n</div>',
      brand: {
        title: "Figma",
        domain: "figma.com",
        colors: [{
          hex: "#f76040"
        }, {
          hex: "#13c5be"
        }, {
          hex: "#a35cfb"
        }]
      }
    }
  };
  const [domain, setDomain] = useState(DEFAULT_DOMAIN);
  const [html, setHtml] = useState(DEFAULT_HTML);
  const [brand, setBrand] = useState(DEFAULT_BRAND);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const iframeRef = useRef(null);
  const isDefault = html === DEFAULT_HTML && domain === DEFAULT_DOMAIN;
  async function selectDomain(d) {
    if (d === domain || loading) return;
    setDomain(d);
    setError(null);
    setLoading(true);
    await new Promise(r => setTimeout(r, 900));
    const hit = STATIC[d];
    setHtml(hit.html);
    setBrand(hit.brand);
    setLoading(false);
  }
  function reset() {
    setDomain(DEFAULT_DOMAIN);
    setHtml(DEFAULT_HTML);
    setBrand(DEFAULT_BRAND);
    setError(null);
  }
  function onIframeLoad() {
    const iframe = iframeRef.current;
    if (!iframe) return;
    try {
      const doc = iframe.contentDocument;
      if (!doc) return;
      const h = Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight);
      iframe.style.height = `${h + 8}px`;
    } catch {}
  }
  const colors = brand && brand.colors || [];
  const name = brand && brand.title || domain;
  const cleanDomain = brand && brand.domain || domain;
  const srcDoc = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><style>html,body{margin:0;padding:0;background:#fafafa;overflow:hidden;}body{padding:20px;}</style></head><body>${html}</body></html>`;
  return <div className="ca-demo not-prose">
      <style>{`
        .ca-demo {
          --ca-blue: #1373e8;
          --ca-blue-press: #0e5cc0;
          --ca-pill-bd: #cfd9e8;
          --ca-line: #ececec;
          --ca-ink: #0a0a0a;
          --ca-ink-2: #3f3f46;
          --ca-ink-3: #919191;
          --ca-bg: #ffffff;
          --ca-bg-soft: #fbfcfe;
          --ca-sans: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
          --ca-mono: "Geist Mono", "IBM Plex Mono", ui-monospace, Menlo, monospace;
          position: relative;
          margin: 28px 0; padding: 22px 22px 20px;
          background: var(--ca-bg);
          border: 1px solid var(--ca-line);
          border-radius: 6px;
          font-family: var(--ca-sans);
          color: var(--ca-ink);
          overflow: hidden;
        }
        .ca-demo::before {
          content: ""; position: absolute; inset: 0; pointer-events: none;
          background: radial-gradient(800px 280px at 50% 100%, rgba(96, 165, 250, 0.10), transparent 70%);
        }
        .ca-demo > * { position: relative; }
        .ca-demo *, .ca-demo *::before, .ca-demo *::after { box-sizing: border-box; }

        .dark .ca-demo {
          --ca-blue: #3b82f6;
          --ca-blue-press: #2563eb;
          --ca-pill-bd: #2f3a4d;
          --ca-line: #26262d;
          --ca-ink: #f5f5f7;
          --ca-ink-2: #c2c2cb;
          --ca-ink-3: #8b8b93;
          --ca-bg: #0a0a0c;
          --ca-bg-soft: #141418;
        }
        .dark .ca-error { color: #fca5a5; background: #2a1416; border-color: #5b2526; }
        .dark .ca-overlay { background: rgba(10, 10, 12, 0.85); }

        .ca-chrome {
          display: flex; align-items: center; justify-content: space-between;
          margin-bottom: 18px; min-height: 24px;
        }
        .ca-kicker {
          display: inline-flex; align-items: center; gap: 9px;
          font-family: var(--ca-mono);
          font-size: 11px; font-weight: 500;
          letter-spacing: 0.04em; text-transform: uppercase;
          color: var(--ca-ink-3);
        }
        .ca-kicker::before { content: ""; width: 5px; height: 5px; background: var(--ca-blue); border-radius: 1px; }
        .ca-logo-light, .ca-logo-dark { height: 22px; display: block; }
        .ca-logo-dark { display: none; }
        .dark .ca-logo-light { display: none; }
        .dark .ca-logo-dark { display: block; }
        .ca-reset {
          font-family: var(--ca-mono);
          font-size: 10.5px; letter-spacing: 0.10em;
          text-transform: uppercase; color: var(--ca-ink-2);
          background: var(--ca-bg);
          border: 1px solid var(--ca-line);
          border-radius: 0;
          padding: 5px 10px;
          cursor: pointer;
        }
        .ca-reset:hover { color: var(--ca-blue); border-color: var(--ca-blue); }

        .ca-form { display: flex; flex-direction: column; gap: 14px; margin-bottom: 14px; }
        .ca-field { display: flex; flex-direction: column; }
        .ca-label {
          font-family: var(--ca-mono);
          font-size: 10.5px; letter-spacing: 0.10em;
          text-transform: uppercase; color: var(--ca-ink-3);
          margin-bottom: 8px;
        }
        .ca-row { display: flex; gap: 8px; }
        .ca-input {
          flex: 1;
          padding: 11px 13px;
          background: var(--ca-bg);
          border: 1px solid var(--ca-line);
          border-radius: 0;
          font-family: var(--ca-mono);
          font-size: 14px; color: var(--ca-ink);
          outline: none; min-width: 0;
        }
        .ca-input:focus { border-color: var(--ca-blue); }
        .ca-btn {
          padding: 11px 18px;
          background: var(--ca-blue); color: #fff;
          border: 1px solid var(--ca-blue);
          border-radius: 0;
          font-family: var(--ca-mono);
          font-size: 13px; font-weight: 500;
          cursor: pointer; white-space: nowrap;
        }
        .ca-btn:hover { background: var(--ca-blue-press); }
        .ca-btn:disabled { opacity: 0.55; cursor: progress; }
        .ca-trys {
          display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap;
          font-family: var(--ca-mono);
          font-size: 11px; color: var(--ca-ink-3);
        }
        .ca-try {
          font-family: var(--ca-mono);
          font-size: 11px;
          padding: 2px 7px;
          background: var(--ca-bg);
          border: 1px solid var(--ca-line);
          color: var(--ca-ink-2);
          cursor: pointer;
        }
        .ca-try:hover { color: var(--ca-blue); border-color: var(--ca-blue); }
        .ca-error {
          padding: 8px 11px; margin-bottom: 10px;
          font-family: var(--ca-mono);
          font-size: 11.5px;
          color: #9b1c1c; background: #fbf0f0;
          border: 1px solid #e6c5c5;
        }
        .ca-divider { height: 1px; margin: 22px 0; background: var(--ca-line); border: 0; }
        .ca-result {
          position: relative;
          background: var(--ca-bg-soft);
          border: 1px solid var(--ca-line);
          overflow: hidden;
        }
        .ca-result-meta {
          padding: 11px 14px;
          border-bottom: 1px solid var(--ca-line);
          display: flex; align-items: center; justify-content: space-between;
          flex-wrap: wrap; gap: 8px;
        }
        .ca-who {
          font-family: var(--ca-mono);
          font-size: 11.5px; color: var(--ca-ink);
        }
        .ca-who strong { font-weight: 500; }
        .ca-who .sub { color: var(--ca-ink-3); }
        .ca-swatches { display: flex; align-items: center; gap: 4px; }
        .ca-sw { width: 14px; height: 14px; border: 1px solid rgba(0,0,0,0.08); }
        .ca-frame-wrap { position: relative; }
        .ca-frame {
          width: 100%;
          height: 720px;
          border: 0;
          display: block;
          background: #fafafa;
        }
        .ca-overlay {
          position: absolute; inset: 0;
          background: rgba(255, 255, 255, 0.85);
          backdrop-filter: blur(2px);
          display: flex; flex-direction: column;
          align-items: center; justify-content: center;
          gap: 12px; z-index: 5;
        }
        .ca-spin {
          width: 22px; height: 22px; border-radius: 50%;
          border: 1.5px solid var(--ca-line);
          border-top-color: var(--ca-blue);
          animation: ca-spin 0.7s linear infinite;
        }
        @keyframes ca-spin { to { transform: rotate(360deg); } }
        .ca-line {
          font-family: var(--ca-mono);
          font-size: 10.5px; letter-spacing: 0.14em;
          text-transform: uppercase; color: var(--ca-ink-3);
        }
      `}</style>

      <div className="ca-chrome">
        <span className="ca-kicker">Live demo</span>
        {isDefault ? <span className="ca-logo">
            <img className="ca-logo-light" src="/logo/light.svg" alt="context.dev" />
            <img className="ca-logo-dark" src="/logo/dark.svg" alt="context.dev" />
          </span> : <button className="ca-reset" type="button" onClick={reset}>
            Reset
          </button>}
      </div>

      <form className="ca-form" onSubmit={e => e.preventDefault()}>
        <div className="ca-field">
          <label className="ca-label" htmlFor="ca-domain-input">
            Enter a domain
          </label>
          <div className="ca-row">
            <input id="ca-domain-input" className="ca-input" type="text" value={domain} readOnly placeholder="example.com" spellCheck={false} required />
          </div>
          <div className="ca-trys" style={{
    marginTop: 10
  }}>
            <span>Change demo</span>
            {TRY.map(d => <button key={d} type="button" className="ca-try" onClick={() => selectDomain(d)}>
                {d}
              </button>)}
          </div>
        </div>
      </form>

      {error && <div className="ca-error">{error}</div>}

      <div className="ca-divider" />

      <div className="ca-result">
        <div className="ca-result-meta">
          <span className="ca-who">
            <strong>{name}</strong>
            <span className="sub"> · {cleanDomain}</span>
          </span>
          <div className="ca-swatches">
            {colors.slice(0, 4).map((c, i) => <span key={i} className="ca-sw" style={{
    background: c.hex
  }} title={c.hex} />)}
          </div>
        </div>
        <div className="ca-frame-wrap">
          <iframe ref={iframeRef} className="ca-frame" sandbox="allow-same-origin" scrolling="no" srcDoc={srcDoc} onLoad={onIframeLoad} title="Generated campaign preview" />
          {loading && <div className="ca-overlay">
              <div className="ca-spin" />
              <div className="ca-line">Brand · Generate variants</div>
            </div>}
        </div>
      </div>
    </div>;
};

export const PromptBanner = ({prompt, message = "Use this pre-built prompt to get started faster."}) => {
  const [copied, setCopied] = useState(false);
  const timeoutRef = useRef(null);
  useEffect(() => {
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, []);
  const handleCopy = useCallback(async () => {
    try {
      await navigator.clipboard.writeText(prompt);
      setCopied(true);
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
      timeoutRef.current = setTimeout(() => {
        setCopied(false);
        timeoutRef.current = null;
      }, 2000);
    } catch (err) {
      console.error("Failed to copy:", err);
    }
  }, [prompt]);
  const handleOpenInCursor = useCallback(() => {
    const url = new URL('cursor://anysphere.cursor-deeplink/prompt');
    url.searchParams.set('text', prompt);
    window.location.href = url.toString();
  }, [prompt]);
  return <div className="not-prose my-6 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 overflow-hidden">
      <div className="p-4">
        <div className="mb-3">
          <div className="text-xs font-medium text-zinc-500 dark:text-zinc-400 mb-2">
            {message}
          </div>
          <div className="relative">
            <textarea readOnly value={prompt} className="w-full px-3 py-2 text-sm font-mono text-zinc-700 dark:text-zinc-300 bg-zinc-50 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-md resize-none focus:outline-none focus:ring-2 focus:ring-zinc-400 dark:focus:ring-zinc-600" rows={Math.min(prompt.split('\n').length, 6)} />
          </div>
        </div>

        <div className="flex items-center gap-2">
          <button onClick={handleOpenInCursor} className="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-zinc-950 dark:bg-white text-sm font-medium text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-200 transition-colors" aria-label="Try in Cursor">
            <img src="/images/cursor-light.svg" alt="Cursor logo" className="size-4 block dark:hidden" loading="lazy" width="16" height="16" />
            <img src="/images/cursor-dark.svg" alt="Cursor logo" className="size-4 hidden dark:block" loading="lazy" width="16" height="16" />
            Try in Cursor
          </button>

          <button onClick={handleCopy} className="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-zinc-100 dark:bg-zinc-800 text-sm font-medium text-zinc-950 dark:text-white hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors" aria-label={copied ? "Copied" : "Copy prompt"} aria-live="polite">
            {copied ? <>
                <svg className="size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
                </svg>
                Copied
              </> : <>
                <svg className="size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
                </svg>
                Copy prompt
              </>}
          </button>
        </div>
      </div>
    </div>;
};

Designing eye-catching social media creatives using an AI agent requires it to have a lot of context: your product features, voice, target audience, and design tokens.

Your agent needs to crawl your marketing site, distill its positioning and design system, then generate on-brand campaign ideas, copy, and social creatives for every platform.

In this guide, we build a pipeline that does this at scale using Context.dev's APIs and some popular agent skills.

Try it: drop a domain in and render three example artboards — square post, landscape, and hero banner — using the brand's website styleguide palette.

<CampaignAssetsDemo />

<PromptBanner
  message="Drop this prompt into your AI coding tool to build the campaign pipeline."
  prompt="Build a branded social-campaign generator that grounds everything in the brand's real site, using Context.dev plus the product-marketing-context and copywriting skills.

1. Read CONTEXT_DEV_API_KEY and ANTHROPIC_API_KEY from env. Never call Context.dev from the browser.

2. Gather: crawl the whole marketing site with /web/crawl (1 credit per page) for the brand's words, plus /brand/retrieve (10 credits) for logos and metadata and /web/styleguide (10 credits) for colors, fonts, and component styles. Write the crawled pages to a folder and the brand kit to a design guide.

3. Distill: run the product-marketing-context skill on the crawled folder to draft .agents/product-marketing.md (positioning, audience, voice, proof, customer language), then with an LLM (Claude Opus 4.8) generate a handful of campaign ideas grounded in that positioning.

4. Copy: run the copywriting skill on the chosen idea; it reads .agents/product-marketing.md and returns headline, supporting line, and CTA in the brand voice.

5. Render: lay out the creative from the design guide and the copy as a self-contained HTML document, then rasterize it at each platform size (Instagram square/portrait/story, X card, LinkedIn banner, YouTube thumbnail). No extra Context.dev calls past the gather.

Full guide: https://docs.context.dev/use-cases/branded-campaign-assets. Never expose CONTEXT_DEV_API_KEY to the browser."
/>

## Architecture

The pipeline runs in four phases. The leverage is grounding every phase in the brand's real site instead of a blank prompt.

1. **Gather** crawls the whole marketing site to Markdown ([Web Scraping API](/guides/scrape-websites-to-markdown#crawl-a-whole-site)) and pulls the brand kit ([Brand API](/guides/get-brand-data)) and design tokens ([Styleguide API](/guides/extract-design-system-from-website#extract-the-full-styleguide)).
2. **Distill** runs the [product-marketing-context](https://www.skills.sh/coreyhaines31/marketingskills/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.
3. **Write** runs the [copywriting](https://www.skills.sh/coreyhaines31/marketingskills/copywriting) skill on the chosen idea for an on-brand headline, supporting line, and CTA.
4. **Render** lays out the creative from the design guide and copy, then rasterizes it to each platform size.

Phases 2 and 3 run in your coding agent through the two skills; the rest is code. Gather once per brand and reuse the corpus, positioning, and design guide across every idea, creative, and size.

## Prerequisites

* **A Context.dev API key.** Grab one from the [dashboard](https://context.dev/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:

<CodeGroup>
  ```bash TypeScript theme={null}
  npm install context.dev
  ```

  ```bash Python theme={null}
  pip install context.dev
  ```

  ```bash Ruby theme={null}
  gem install context.dev
  ```

  ```bash Go theme={null}
  go get github.com/context-dot-dev/context-go-sdk
  ```

  ```bash PHP theme={null}
  composer require context-dev/context-dev-php
  ```
</CodeGroup>

## 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:

1. The [**Brand API**](/guides/get-brand-data) to get brand name, description, slogans, backdrops, and all logo variants.
2. The [**Web Scraping API**](/guides/scrape-websites-to-markdown#crawl-a-whole-site) to crawl reachable marketing-site pages as clean Markdown.
3. The [**Styleguide API**](/guides/extract-design-system-from-website#extract-the-full-styleguide) to get exact colors, component CSS, spacing, fonts, shadows, etc.

<CodeGroup>
  ```typescript TypeScript theme={null}
  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 })),
    };
  }
  ```

  ```python Python theme={null}
  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],
      }
  ```

  ```ruby Ruby theme={null}
  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
  ```

  ```go Go theme={null}
  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 PHP theme={null}
  <?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 ?? [],
          ),
      ];
  }
  ```
</CodeGroup>

<Badge color="blue">Brand · 10 credits</Badge> <Badge color="blue">Styleguide · 10 credits</Badge> <Badge color="blue">Crawl · 1 credit per page</Badge>

The crawl bills 1 credit per page, so cost depends on how big the marketing site is. Use [`maxPages`](/guides/scrape-websites-to-markdown#crawl-a-whole-site) (default 100, max 500) and [`urlRegex`](/guides/scrape-websites-to-markdown#crawl-a-whole-site) 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](https://www.skills.sh/coreyhaines31/marketingskills/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.

<CodeGroup>
  ```typescript TypeScript theme={null}
  // 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 ?? "";
  }
  ```

  ```python Python theme={null}
  # 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"), "")
  ```

  ```ruby Ruby theme={null}
  # 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
  ```

  ```go Go theme={null}
  // 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 PHP theme={null}
  <?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 '';
  }
  ```
</CodeGroup>

## 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](https://www.skills.sh/coreyhaines31/marketingskills/copywriting) skill on it, with a prompt specifying how you want the output.

<CodeGroup>
  ```typescript TypeScript theme={null}
  // 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;
  }
  ```

  ```python Python theme={null}
  # 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)
  ```

  ```ruby Ruby theme={null}
  # 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
  ```

  ```go Go theme={null}
  // 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 PHP theme={null}
  <?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' => ''];
  }
  ```
</CodeGroup>

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

Edit this to include all the variants you want.

<CodeGroup>
  ```typescript TypeScript theme={null}
  // 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" },
  ];
  ```

  ```python Python theme={null}
  # 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"},
  ]
  ```

  ```ruby Ruby theme={null}
  # 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
  ```

  ```go Go theme={null}
  // 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 PHP theme={null}
  <?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'],
  ];
  ```
</CodeGroup>

This pipeline designs the creatives in HTML/CSS/JS instead of using an image model because this way is cheaper, leads to fewer hallucinations, can be customized easily, and the text stays crisp and editable.

So we will fan out one agent per idea, in parallel, handing each the design guide, the full set of sizes, and its idea. Each returns a single self-contained mockup that lays out every platform size, with a Download PNG button under each artboard that exports it to an image in the browser.

<Prompt description="System prompt for the creative designer" actions={["copy"]}>
  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](https://cdn.jsdelivr.net/npm/html-to-image)) and triggers a download.
    Return only the HTML document, with no commentary or code fences.
</Prompt>

<CodeGroup>
  ```typescript TypeScript theme={null}
  // 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) })),
    );
  }
  ```

  ```python Python theme={null}
  # 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)]
  ```

  ```ruby Ruby theme={null}
  # 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
  ```

  ```go Go theme={null}
  // 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 PHP theme={null}
  <?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,
      );
  }
  ```
</CodeGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Web Scraping API" icon="spider" href="/guides/scrape-websites-to-markdown#crawl-a-whole-site">
    Crawl a whole site to clean Markdown, the corpus the positioning skill reads.
  </Card>

  <Card title="Styleguide API" icon="paintbrush" href="/guides/extract-design-system-from-website#extract-the-full-styleguide">
    Fonts, colors, and component CSS for the design guide.
  </Card>

  <Card title="product-marketing-context skill" icon="bullseye" href="https://www.skills.sh/coreyhaines31/marketingskills/product-marketing-context">
    Turns the crawl into positioning, audience, and brand voice.
  </Card>

  <Card title="copywriting skill" icon="pen-nib" href="https://www.skills.sh/coreyhaines31/marketingskills/copywriting">
    Writes on-brand copy from that positioning.
  </Card>
</CardGroup>
