We can generate editable PowerPoint infographics from a prompt or a picked layout and let users change text/colors/icons. The trick is: design with native PPT shapes/text (not pictures), so everything is editable once the .pptx opens.

Below is a drop-in backend route that builds three clean, brandable patterns (all fully editable):

Hub & Spoke (6 points) – center circle + 6 nodes around, with connector lines

Horizontal Steps (6) – pill boxes in a row with numbers

Mindmap (6) – center bubble with 3 branches per side

You can call it with labels/colors/icons and it returns a PPTX.

Server: /api/infographics/generate

Create server/api/infographics/generate.ts

import { Router } from "express";
import PPTXGenJS from "pptxgenjs";

const router = Router();

type LayoutType = "hubSpoke" | "stepsHorizontal" | "mindmap";
type LogoPlacement = "none"|"top-left"|"top-right"|"bottom-left"|"bottom-right";

type ReqBody = {
  size?: "16x9"|"4x3";
  layout: LayoutType;
  count?: number; // default 6
  labels?: string[]; // text for each node/step
  notes?: string[];  // optional secondary text lines
  colors?: string[]; // overrides brand accents cyclically
  brand?: {
    name?: string;
    fonts?: { heading?: string; body?: string };
    accents?: string[]; // fallback colors
  };
  iconsSvg?: (string | null)[]; // data URLs like "data:image/svg+xml;utf8,<svg...>" or null
  centerTitle?: string;         // for hub/mindmap center
  assets?: {
    logoDataUrl?: string | null;
    logoPlacement?: LogoPlacement;
    logoScale?: number; // 0.6-2
  };
};

router.post("/", async (req, res) => {
  try {
    const b = req.body as ReqBody;
    const count = Math.max(2, Math.min(b.count ?? 6, 12)); // safety
    const fonts = {
      heading: b.brand?.fonts?.heading || "Inter",
      body: b.brand?.fonts?.body || "Inter",
    };
    const palette = (b.colors && b.colors.length ? b.colors : (b.brand?.accents || ["#0EA5E9","#F59E0B","#10B981","#8B5CF6","#EF4444","#64748B"]));

    const pptx = new PPTXGenJS();
    pptx.layout = (b.size === "4x3") ? "LAYOUT_4x3" : "LAYOUT_16x9";
    const W = pptx.width;  // inches
    const H = pptx.height; // inches

    // helpers ---------------
    const col = (i: number) => hex(palette[i % palette.length]);
    const center = { x: W/2, y: H/2 };
    function addLogo(slide: PPTXGenJS.Slide) {
      const a = b.assets;
      if (!a?.logoDataUrl || !a.logoPlacement || a.logoPlacement==="none") return;
      const base = 1.0, w = base * (a.logoScale || 1), h = w, m = 0.25;
      let x=m, y=m;
      switch (a.logoPlacement) {
        case "top-left": x=m; y=m; break;
        case "top-right": x=W-w-m; y=m; break;
        case "bottom-left": x=m; y=H-h-m; break;
        case "bottom-right": x=W-w-m; y=H-h-m; break;
      }
      slide.addImage({ data: a.logoDataUrl, x, y, w, h });
    }

    // slide
    const s = pptx.addSlide();

    if (b.layout === "hubSpoke") {
      // center bubble
      const R = Math.min(W,H)*0.22;         // radius of the orbit
      const node = Math.min(W,H)*0.11;      // node diameter
      const cx = center.x, cy = center.y;
      // center
      s.addShape(pptx.ShapeType.ellipse, {
        x: cx - node*0.7/2, y: cy - node*0.7/2, w: node*0.7, h: node*0.7,
        fill: { color: hex("#0F172A") }, line: { color: "FFFFFF", width: 1 }
      });
      s.addText(b.centerTitle || "Topic", {
        x: cx - 2.5, y: cy - 0.4, w: 5, h: 0.8,
        align: "center", fontFace: fonts.heading, fontSize: 22, bold: true, color: "FFFFFF"
      });

      // nodes around circle
      for (let i=0;i<count;i++) {
        const angle = (Math.PI*2 * i)/count - Math.PI/2; // start top, go clockwise
        const nx = cx + R * Math.cos(angle);
        const ny = cy + R * Math.sin(angle);

        // connector line
        s.addShape(pptx.ShapeType.line, {
          x: cx, y: cy, w: nx-cx, h: ny-cy,
          line: { color: hex("#CBD5E1"), width: 1.5 }
        });

        // node circle
        s.addShape(pptx.ShapeType.ellipse, {
          x: nx - node/2, y: ny - node/2, w: node, h: node,
          fill: { color: col(i) }, line: { color: "FFFFFF", width: 1 }
        });

        // optional icon
        const icon = b.iconsSvg?.[i];
        if (icon) {
          s.addImage({ data: icon, x: nx - node*0.28, y: ny - node*0.28, w: node*0.56, h: node*0.56 });
        }

        // label box under node
        const lbl = (b.labels?.[i] ?? `Point ${i+1}`);
        const note = (b.notes?.[i] ?? "");
        s.addText(
          [
            { text: `${lbl}\n`, options: { bold: true, fontFace: fonts.heading, fontSize: 14, color: "1F2937" } },
            ...(note ? [{ text: note, options: { fontFace: fonts.body, fontSize: 12, color: "475569" } }] : [])
          ],
          {
            x: nx - 1.6, y: ny + node/2 + 0.1, w: 3.2, h: 1.0, align: "center"
          }
        );
      }
      addLogo(s);
    }

    if (b.layout === "stepsHorizontal") {
      // horizontal pill steps
      const marginX = 0.7, gap = 0.25;
      const availableW = W - marginX*2 - gap*(count-1);
      const boxW = availableW / count;
      const boxH = 1.4;
      const y = H*0.42;

      for (let i=0;i<count;i++) {
        const x = marginX + i*(boxW + gap);
        s.addShape(pptx.ShapeType.roundRect, {
          x, y, w: boxW, h: boxH,
          fill: { color: col(i) },
          line: { color: "FFFFFF", width: 1 },
          rectRadius: 0.15
        });
        // number
        s.addText(String(i+1).padStart(2,"0"), {
          x: x+0.15, y: y+0.18, w: 0.6, h: 0.4,
          fontFace: fonts.heading, fontSize: 14, bold: true, color: "FFFFFF"
        });
        // label
        const lbl = (b.labels?.[i] ?? `Step ${i+1}`);
        const note = (b.notes?.[i] ?? "");
        s.addText(
          [
            { text: `${lbl}\n`, options: { bold: true, fontFace: fonts.heading, fontSize: 14, color: "FFFFFF" } },
            ...(note ? [{ text: note, options: { fontFace: fonts.body, fontSize: 12, color: "F8FAFC" } }] : [])
          ],
          { x: x+0.85, y: y+0.2, w: boxW-1.05, h: boxH-0.4, align: "left" }
        );
      }
      // connector line under pills
      s.addShape(pptx.ShapeType.line, {
        x: marginX, y: y+boxH+0.15, w: W - marginX*2, h: 0,
        line: { color: hex("#CBD5E1"), width: 1 }
      });
      addLogo(s);
    }

    if (b.layout === "mindmap") {
      // center bubble
      const node = 1.3;
      s.addShape(pptx.ShapeType.ellipse, {
        x: center.x - node, y: center.y - node*0.7, w: node*2, h: node*1.4,
        fill: { color: hex("#0F172A") }, line: { color: "FFFFFF", width: 1 }
      });
      s.addText(b.centerTitle || "Topic", {
        x: center.x - 2.5, y: center.y - 0.2, w: 5, h: 0.8,
        align: "center", fontFace: fonts.heading, fontSize: 22, bold: true, color: "FFFFFF"
      });

      // 3 branches per side (total up to count)
      const perSide = Math.ceil(count/2);
      const leftX = 1.0, rightX = W - 4.0;
      const topY = H*0.20, stepY = (H*0.60)/(perSide-1 || 1);

      let idx = 0;
      for (let j=0;j<perSide && idx<count;j++, idx++) {
        const y = topY + j*stepY;
        // connector
        s.addShape(pptx.ShapeType.line, {
          x: center.x - 0.2, y: center.y, w: (leftX+2.2)-(center.x - 0.2), h: y - center.y,
          line: { color: hex("#94A3B8"), width: 1.25 }
        });
        // node bubble
        s.addShape(pptx.ShapeType.roundRect, {
          x: leftX, y, w: 3.2, h: 1.0, rectRadius: 0.12,
          fill: { color: col(idx) }, line: { color: "FFFFFF", width: 1 }
        });
        // text
        const lbl = b.labels?.[idx] ?? `Point ${idx+1}`;
        const note = b.notes?.[idx] ?? "";
        s.addText(
          [
            { text: lbl + "\n", options: { bold: true, fontFace: fonts.heading, fontSize: 14, color: "FFFFFF" } },
            ...(note ? [{ text: note, options: { fontFace: fonts.body, fontSize: 12, color: "F8FAFC" } }] : [])
          ],
          { x: leftX+0.25, y: y+0.12, w: 2.7, h: 0.8, align: "left" }
        );
      }
      for (let j=0;j<perSide && idx<count;j++, idx++) {
        const y = topY + j*stepY;
        s.addShape(pptx.ShapeType.line, {
          x: center.x + 0.2, y: center.y, w: (rightX)-(center.x + 0.2), h: y - center.y,
          line: { color: hex("#94A3B8"), width: 1.25 }
        });
        s.addShape(pptx.ShapeType.roundRect, {
          x: rightX, y, w: 3.2, h: 1.0, rectRadius: 0.12,
          fill: { color: col(idx) }, line: { color: "FFFFFF", width: 1 }
        });
        const lbl = b.labels?.[idx] ?? `Point ${idx+1}`;
        const note = b.notes?.[idx] ?? "";
        s.addText(
          [
            { text: lbl + "\n", options: { bold: true, fontFace: fonts.heading, fontSize: 14, color: "FFFFFF" } },
            ...(note ? [{ text: note, options: { fontFace: fonts.body, fontSize: 12, color: "F8FAFC" } }] : [])
          ],
          { x: rightX+0.25, y: y+0.12, w: 2.7, h: 0.8, align: "left" }
        );
      }
      addLogo(s);
    }

    const buf = await pptx.write("nodebuffer");
    res.setHeader("Content-Disposition", 'attachment; filename="infographic.pptx"');
    res.setHeader("Content-Type", "application/vnd.openxmlformats-officedocument.presentationml.presentation");
    res.send(buf);

  } catch (e) {
    console.error("[infographics/generate] error", e);
    res.status(500).json({ error: "Failed to generate infographic" });
  }
});

export default router;

// utils
function hex(h?: string) {
  const v = (h || "").replace("#","");
  return v.length===6 ? v.toUpperCase() : "000000";
}


Mount it:

// server/server.ts
import infographicsRoute from "./api/infographics/generate";
app.use("/api/infographics/generate", infographicsRoute);

How to call it (frontend example)
const payload = {
  size: "16x9",
  layout: "hubSpoke",           // "hubSpoke" | "stepsHorizontal" | "mindmap"
  count: 6,
  centerTitle: "5W1H METHOD",
  labels: ["What","Why","Who","Where","When","How"],
  notes: ["Add your copy","Add your copy","Add your copy","Add your copy","Add your copy","Add your copy"],
  colors: [],                   // or let brand accents cycle
  brand: { fonts: { heading: "Inter", body: "Inter" }, accents: ["#0EA5E9","#F59E0B","#10B981","#8B5CF6","#EF4444","#64748B"] },
  iconsSvg: [null,null,null,null,null,null], // or data:image/svg+xml;utf8,<svg...>
  assets: { logoDataUrl: null, logoPlacement: "none", logoScale: 1 }
};

const res = await fetch("/api/infographics/generate", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(payload),
});

const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = "infographic.pptx"; a.click();
URL.revokeObjectURL(url);

Notes (so it matches your vision)

Everything is editable (text boxes, shapes, lines). Users can change colors, fonts, text in PPT.

Icons: pass sanitized SVG data URLs and they’ll drop in as vector art (modern Office supports SVG).

Brand binding: if you don’t pass colors, it cycles your 6 brand accents automatically.

Extensible: Add more layouts by copy-pasting a case and using basic shapes math.