Marketing Tools

Dynamic Social Card Generation API: Templates and WordPress

Your content pipeline is already doing the hard work: titles, categories, author, publish date, sometimes a spicy pull-quote. Manually turning that into social cards is the part that feels like punishment. Automated social media image generation fixes that by treating images like build artifacts: deterministic, versionable, and produced the moment a post exists.

The clean architecture is simple: a blog post is created → a renderer API generates an image from a template → WordPress stores it in Media Library → the post’s Featured Image gets updated.

Do it right and you never touch Canva again, except when you want to, not because you must.

Why this works better than “designing” every image

Social cards are mostly a layout problem, not a creativity problem. The creativity is in the template. Automation is in the mapping. Once you separate those two, you can ship consistent cards across LinkedIn, X, Facebook, Slack unfurls, Open Graph, even email headers, without humans copy-pasting titles at 1 a.m.

Also: templates enforce brand discipline. Humans do not.

Using Bannerbear or Placid.app APIs

Both tools follow the same mental model: you design a template once, you fill “layers” (text, images, colors) via API payloads, you get back a rendered image URL (or binary), you download it, you upload it to WordPress.

Where they differ in practice is less about “features” and more about ergonomics: how quickly you can iterate templates, how predictable their render queue is, and how cleanly they handle fonts, long titles, and multi-size variants.

A sane decision rule: if your bottleneck is template iteration and team collaboration, pick the one whose editor your designer doesn’t hate. If your bottleneck is reliability at scale, pick the one whose API status history makes you sleep.

Mapping blog titles to the image canvas

The part most people underestimate is typography collision. Blog titles are chaotic. Your template is not. You need rules that turn chaos into a stable layout.

You’re mapping data fields into a constrained canvas. That means you should define a tiny spec like this: maximum characters per line, maximum lines, fallback shortening, optional subtitle injection, and a font-size strategy (either fixed or responsive).

Here’s a mapping model that behaves like production, not a demo.

Blog fieldTemplate layerTransform ruleFailure mode it prevents
post_titletitle_textsmart wrap to 2–4 lines, then truncate with ellipsistitle spills off-canvas
categorykicker_textuppercase, max 24 charscategory badge overflows
authorbyline_textoptional, hide if missingempty “By ” line
featured_quotesub_textoptional, max 90 charsparagraph pretending to be a quote
brand_coloraccent_bardefault to brand hex if missingrandom colors from chaos

Title wrapping that doesn’t look like a ransom note

This is the unglamorous fix: wrap by words, cap the lines, then scale down if needed.

function wrapTitle(title, maxLineLen = 26, maxLines = 3) {
  const words = title.trim().split(/\s+/);
  const lines = [];
  let cur = "";

  for (const w of words) {
    const next = cur ? `${cur} ${w}` : w;
    if (next.length <= maxLineLen) {
      cur = next;
    } else {
      if (cur) lines.push(cur);
      cur = w;
      if (lines.length === maxLines) break;
    }
  }
  if (lines.length < maxLines && cur) lines.push(cur);

  const usedWords = lines.join(" ").split(/\s+/).length;
  const truncated = usedWords < words.length;
  if (truncated) {
    lines[lines.length - 1] = lines[lines.length - 1].replace(/\.*$/, "") + "…";
  }
  return lines.join("\n");
}

Now your template can render \n line breaks cleanly, and you’ve stopped long titles from destroying your brand identity.

End-to-end flow with an API renderer

A robust workflow typically looks like this:

  1. WordPress publishes (or updates) a post.
  2. A webhook or hook calls your renderer endpoint with post metadata.
  3. The renderer calls Bannerbear/Placid with a template ID and layer modifications.
  4. The renderer downloads the image when ready.
  5. The renderer uploads it to WordPress Media Library.
  6. The renderer sets it as the post’s featured media.

The only “moving part” you need to choose is where the logic runs: inside WordPress (plugin) or outside (serverless). Outside is usually cleaner because you avoid PHP plugin entropy and you can reuse the same service for multiple sites.

Bannerbear example: create image from template

This is the shape you want: a template ID plus “modifications”. The exact property names depend on the template you built in their editor; the principle is constant.

async function createBannerbearImage({ apiKey, templateId, title, category, imageUrl }) {
  const res = await fetch("https://api.bannerbear.com/v2/images", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${apiKey}`
    },
    body: JSON.stringify({
      template: templateId,
      modifications: [
        { name: "title_text", text: title },
        { name: "kicker_text", text: category },
        { name: "bg_image", image_url: imageUrl }
      ]
    })
  });

  if (!res.ok) throw new Error(`Bannerbear create failed: ${res.status}`);
  return await res.json();
}

Rendering is often asynchronous. You create a job, then poll until image_url exists.

async function pollBannerbearImage({ apiKey, imageId, timeoutMs = 30000 }) {
  const started = Date.now();
  while (Date.now() - started < timeoutMs) {
    const res = await fetch(`https://api.bannerbear.com/v2/images/${imageId}`, {
      headers: { "Authorization": `Bearer ${apiKey}` }
    });
    const data = await res.json();
    if (data.image_url && data.status === "completed") return data.image_url;
    await new Promise(r => setTimeout(r, 1200));
  }
  throw new Error("Bannerbear render timed out");
}

Placid example: render with layers

Same concept: a template plus layer values. You’ll typically provide layers (text/image) keyed by IDs from the template.

async function createPlacidImage({ apiKey, templateId, title, category }) {
  const res = await fetch(`https://api.placid.app/api/rest/${templateId}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${apiKey}`
    },
    body: JSON.stringify({
      layers: {
        title_text: title,
        kicker_text: category
      }
    })
  });

  if (!res.ok) throw new Error(`Placid render failed: ${res.status}`);
  const data = await res.json();
  return data.image_url || data.url;
}

Whether you use Bannerbear or Placid, the integration pattern stays boring: build template once, map fields, render, upload.

Auto-updating the WordPress Featured Image

You’re updating featured_media on the post. That requires two API calls: upload to /wp-json/wp/v2/media, then update the post at /wp-json/wp/v2/posts/{id}.

The simplest auth for a single-site automation is WordPress Application Passwords (fast to set up, good enough for internal tooling). If you’re running this across multiple sites or clients, you’ll eventually want OAuth or a signed internal proxy, but start simple.

Upload image to WordPress Media Library

async function wpUploadMedia({ wpBaseUrl, username, appPassword, filename, imageBuffer }) {
  const auth = Buffer.from(`${username}:${appPassword}`).toString("base64");

  const res = await fetch(`${wpBaseUrl}/wp-json/wp/v2/media`, {
    method: "POST",
    headers: {
      "Authorization": `Basic ${auth}`,
      "Content-Disposition": `attachment; filename="${filename}"`,
      "Content-Type": "image/png"
    },
    body: imageBuffer
  });

  if (!res.ok) throw new Error(`WP media upload failed: ${res.status}`);
  return await res.json();
}

Set Featured Image (featured_media)

async function wpSetFeaturedImage({ wpBaseUrl, username, appPassword, postId, mediaId }) {
  const auth = Buffer.from(`${username}:${appPassword}`).toString("base64");

  const res = await fetch(`${wpBaseUrl}/wp-json/wp/v2/posts/${postId}`, {
    method: "POST",
    headers: {
      "Authorization": `Basic ${auth}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({ featured_media: mediaId })
  });

  if (!res.ok) throw new Error(`WP post update failed: ${res.status}`);
  return await res.json();
}

Putting it together

async function generateAndAttachSocialCard({
  postId,
  postTitle,
  category,
  wpBaseUrl,
  wpUser,
  wpAppPassword,
  bannerbearKey,
  bannerbearTemplateId
}) {
  const title = wrapTitle(postTitle, 26, 3);

  const job = await createBannerbearImage({
    apiKey: bannerbearKey,
    templateId: bannerbearTemplateId,
    title,
    category,
    imageUrl: "https://example.com/static/bg.png"
  });

  const imageUrl = await pollBannerbearImage({ apiKey: bannerbearKey, imageId: job.id });
  const imgRes = await fetch(imageUrl);
  const imgBuf = Buffer.from(await imgRes.arrayBuffer());

  const media = await wpUploadMedia({
    wpBaseUrl,
    username: wpUser,
    appPassword: wpAppPassword,
    filename: `social-card-${postId}.png`,
    imageBuffer: imgBuf
  });

  return await wpSetFeaturedImage({
    wpBaseUrl,
    username: wpUser,
    appPassword: wpAppPassword,
    postId,
    mediaId: media.id
  });
}

This is the core. Everything “advanced” is just production hardening: retries, idempotency, caching, and handling updates without spamming your media library.

The production problems you should solve early

Template drift is real. Someone edits the template, renames a layer, your automation silently starts producing cards missing the title. The fix is to store a “template contract” in code: required layer names and a health check endpoint that renders a test card daily.

Duplicate media bloat is also real. If you regenerate a card on every minor edit, WordPress fills up with dead images. The fix is to store the last generated media ID in post meta and replace it, or delete prior media when you regenerate.

Title entropy will keep humiliating your layout. Add a second strategy: if wrap+truncate still looks bad, fall back to “short title + subtitle” using the first clause before a colon, or the first sentence.

What I’d do for Triumphoid, specifically?

I’d standardize on one template for Open Graph and one for “feed card”, and I’d generate both every publish. Even if you only use one today, you’ll eventually care when LinkedIn crops differently from X.

I’d also stop letting the social card be “just the title.” Add a short category kicker and a subtle site mark. The whole point is recognizability at scroll speed.

Practitioner take: social-card automation earns its keep when it removes production variance. The API call is easy; the durable work is making a template survive long titles, missing images, retries, and platform crops.

Social card API workflow: what to automate

StepDecision ruleQuality check
Template selectionMap content type to a fixed visual systemDo not let categories invent a new layout
Text fittingSet a character budget and overflow fallbackRender the longest expected title
Asset lookupUse a source image or a deliberate fallbackReject a missing or tiny image
PublishingUpload, inspect, then set featured mediaKeep the prior image until the new one is valid

Use a render queue, not a blind publish hook

Store the post ID, template version, input values, render URL, and validation result. That makes a failed image reproducible instead of a one-off visual mystery. For the WordPress-specific handoff, this featured-image automation guide covers the operational considerations.

FAQ

Should one template serve every social network?

Usually no. Keep a core visual system, but generate platform-specific crops when the subject, logo, or title needs protected space.

The WordPress media REST API reference is the right baseline for a safe upload and attachment workflow.

Elizabeth Sramek

Elizabeth Sramek is an independent advisor on search visibility and demand architecture for B2B companies operating in high-competition markets. Based in Prague and working globally, she specializes in designing search presence for AI-mediated discovery and building category visibility that survives algorithmic shifts.

Recent Posts

The Infinite OAuth Loop: Automating Token Refreshes in Custom Webhooks

TL;DR — Automating OAuth Token Refreshes in Webhooks The loop happens when multiple concurrent webhook…

8 hours ago

Firecrawl vs. Jina AI: Which LLM Web Scraper Actually Bypasses Cloudflare?

TL;DR Neither tool "beats" Cloudflare outright, but Firecrawl gets meaningfully further into Cloudflare-protected JavaScript-heavy sites…

2 days ago

PhantomJS is Dead: Using ScrapingBee API for Javascript Rendering

TL;DR — PhantomJS → ScrapingBee Migration 2026 PhantomJS is dead. No updates since 2018, WebKit…

2 days ago

ETL Process Optimization: How to Make Data Pipelines Faster, Cleaner and More Reliable

Direct answer: ETL process optimization means improving how data is extracted, transformed and loaded so…

3 days ago

Workflow Automation in 2026: Our Examples, Tools and Implementation Guide

Direct answer: Workflow automation is the use of software to move tasks, data and approvals…

4 days ago

How to A/B Test Your Website with AI: What Actually Changes (and What Doesn’t)

A/B testing with AI means using a language model to generate, critique, and prioritize test…

6 days ago