3) Backend builds with the handoff (if present)

Edit server/api/ppt/build-template.ts — accept optional coverDividers and use those masters; otherwise fall back to your simple defaults.

Replace the existing file with:

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

const router = Router();

type LogoPlacement = "none"|"top-left"|"top-right"|"bottom-left"|"bottom-right";

type CoverPayload = {
  size: "16x9"|"4x3";
  style: "centered"|"band"|"left"|"wash";
  brand: { name?: string; fonts: { heading: string; body: string }; accents: string[] };
  content: { title: string; subtitle?: string };
  assets: { logoDataUrl: string|null; logoPlacement: LogoPlacement; logoScale: number };
};

type BuildBody = {
  size: "16x9" | "4x3";
  brand: { name?: string; fonts: { heading: string; body: string }; accents: string[] };
  assets: { logoDataUrl: string|null; logoPlacement: LogoPlacement; logoScale: number };
  selections: { layouts: string[]; infographics: string[] };
  coverDividers?: CoverPayload | null;
};

router.post("/", async (req, res) => {
  try {
    const b = req.body as BuildBody;

    const pptx = new PPTXGenJS();
    pptx.layout = b.size === "4x3" ? "LAYOUT_4x3" : "LAYOUT_16x9";
    const slideW = pptx.width, slideH = pptx.height;

    // --- Masters: use handoff if present ---
    if (b.coverDividers) {
      const cd = normalizeCD(b.coverDividers, b.size);
      defineCoverMasterFromCD(pptx, cd, slideW, slideH);
      defineDividerMasterFromCD(pptx, cd, slideW, slideH);
    } else {
      // minimal defaults
      pptx.defineSlideMaster({
        title: "Master Title",
        background: { color: rgb(b.brand.accents[0], "0EA5E9") },
        objects: [
          { text: { text: b.brand.name || "Your Company", options: { x: 1, y: 2, fontSize: 36, color: "FFFFFF", fontFace: b.brand.fonts.heading, bold: true } } },
          ...(b.assets.logoDataUrl && b.assets.logoPlacement !== "none" ? [logoObject(b.assets, slideW, slideH)] : []),
        ],
      });
      pptx.defineSlideMaster({
        title: "Section Divider",
        background: { color: rgb(b.brand.accents[1], "F99F1B") },
        objects: [
          { text: { text: "Section Title", options: { x: 1, y: 2, fontSize: 28, color: "FFFFFF", fontFace: b.brand.fonts.heading, bold: true } } },
          ...(b.assets.logoDataUrl && b.assets.logoPlacement !== "none" ? [logoObject(b.assets, slideW, slideH)] : []),
        ],
      });
    }

    // Title slide
    pptx.addSlide({ masterName: "CoverMaster" }).addText("", { x: 0, y: 0, w: 0, h: 0 }); // noop to ensure slide created
    // Section divider
    pptx.addSlide({ masterName: "DividerMaster" });

    // Body layouts
    b.selections.layouts.forEach((layoutId) => {
      const s = pptx.addSlide();
      s.addText(layoutId, { x: 1, y: 0.6, fontSize: 20, fontFace: b.brand.fonts.heading });
      s.addText("Content area", { x: 1, y: 1.4, fontSize: 14, fontFace: b.brand.fonts.body, color: rgb(b.brand.accents[2], "333333") });
      addLogoToSlide(s, b.assets, slideW, slideH);
    });

    // Infographics
    b.selections.infographics.forEach((inf) => {
      const s = pptx.addSlide();
      s.addText(`Infographic: ${inf}`, { x: 1, y: 0.6, fontSize: 20, fontFace: b.brand.fonts.heading });
      const sections = parseInt(inf.match(/\d+/)?.[0] || "2", 10);
      const totalW = slideW - 1.0;
      const boxW = (totalW - (sections - 1) * 0.1) / sections;
      for (let i = 0; i < sections; i++) {
        s.addShape(pptx.ShapeType.rect, {
          x: 0.5 + i * (boxW + 0.1),
          y: 1.6,
          w: boxW,
          h: 2.2,
          fill: { color: rgb(b.brand.accents[i % b.brand.accents.length], "6B7280") },
        });
      }
      addLogoToSlide(s, b.assets, slideW, slideH);
    });

    const buf = await pptx.write("nodebuffer");
    res.setHeader("Content-Disposition", 'attachment; filename="ibrandbiz-template.pptx"');
    res.setHeader("Content-Type", "application/vnd.openxmlformats-officedocument.presentationml.presentation");
    res.send(buf);
  } catch (e) {
    console.error("PPT build error", e);
    res.status(500).json({ error: "Failed to build PPTX" });
  }
});

export default router;

// ---------- helpers ----------
function rgb(maybeHex?: string, fallbackHex?: string) {
  const h = (maybeHex || fallbackHex || "000000").replace("#", "");
  return h.length === 6 ? h.toUpperCase() : "000000";
}

function addLogoToSlide(
  slide: PPTXGenJS.Slide,
  assets: { logoDataUrl: string|null; logoPlacement: LogoPlacement; logoScale: number },
  slideW: number,
  slideH: number
) {
  if (!assets.logoDataUrl || assets.logoPlacement === "none") return;
  const base = 1.2;
  const w = base * (assets.logoScale || 1);
  const h = w;
  const m = 0.25;
  let x = m, y = m;
  switch (assets.logoPlacement) {
    case "top-left": x = m; y = m; break;
    case "top-right": x = slideW - w - m; y = m; break;
    case "bottom-left": x = m; y = slideH - h - m; break;
    case "bottom-right": x = slideW - w - m; y = slideH - h - m; break;
  }
  slide.addImage({ data: assets.logoDataUrl, x, y, w, h });
}

function logoObject(
  assets: { logoDataUrl: string|null; logoPlacement: LogoPlacement; logoScale: number },
  slideW: number,
  slideH: number
): PPTXGenJS.SlideMasterObject {
  const base = 1.2;
  const w = base * (assets.logoScale || 1);
  const h = w;
  const m = 0.25;
  let x = -999, y = -999;
  switch (assets.logoPlacement) {
    case "top-left": x = m; y = m; break;
    case "top-right": x = slideW - w - m; y = m; break;
    case "bottom-left": x = m; y = slideH - h - m; break;
    case "bottom-right": x = slideW - w - m; y = slideH - h - m; break;
    case "none": default: break;
  }
  return { image: { data: assets.logoDataUrl!, x, y, w, h } };
}

// Normalize incoming cover/dividers to current size (in case user saved at a different size)
function normalizeCD(cd: CoverPayload, currentSize: "16x9"|"4x3"): CoverPayload {
  if (cd.size === currentSize) return cd;
  return { ...cd, size: currentSize }; // we only use relative placements
}

function defineCoverMasterFromCD(
  pptx: PPTXGenJS,
  cd: CoverPayload,
  slideW: number,
  slideH: number
) {
  const objects: PPTXGenJS.SlideMasterObject[] = [];

  if (cd.style === "band") {
    objects.push({ rect: { x: 0, y: 0, w: "100%", h: "100%", fill: { color: rgb(cd.brand.accents[2], "111827") } } });
    objects.push({ rect: { x: 0, y: slideH*0.32, w: "100%", h: slideH*0.36, fill: { color: rgb(cd.brand.accents[0], "0EA5E9") } } });
  } else if (cd.style === "left") {
    objects.push({ rect: { x: 0, y: 0, w: "100%", h: "100%", fill: { color: rgb(cd.brand.accents[2], "111827") } } });
    objects.push({ rect: { x: 0.6, y: slideH*0.26, w: 0.12, h: slideH*0.48, fill: { color: rgb(cd.brand.accents[1], "F99F1B") } } });
  } else if (cd.style === "wash") {
    objects.push({ rect: { x: 0, y: 0, w: "100%", h: "100%", fill: { color: rgb(cd.brand.accents[2], "111827") } } });
    objects.push({ rect: { x: 0, y: 0, w: "100%", h: "100%", fill: { color: rgb(cd.brand.accents[0], "0EA5E9"), transparency: 60 } } });
  } else {
    objects.push({ rect: { x: 0, y: 0, w: "100%", h: "100%", fill: { color: rgb(cd.brand.accents[0], "0EA5E9") } } });
  }

  objects.push({
    text: { text: cd.content.title || "Title", options: { x: 0.8, y: slideH*0.32, w: slideW-1.6, h: 1.6, align: "center", fontFace: cd.brand.fonts.heading, fontSize: 42, bold: true, color: "FFFFFF" } }
  });
  if (cd.content.subtitle?.trim()) {
    objects.push({
      text: { text: cd.content.subtitle!, options: { x: 1.2, y: slideH*0.48, w: slideW-2.4, h: 1.1, align: "center", fontFace: cd.brand.fonts.body, fontSize: 20, color: "FFFFFF" } }
    });
  }
  if (cd.assets.logoDataUrl && cd.assets.logoPlacement !== "none") {
    objects.push(logoObject(cd.assets, slideW, slideH));
  }

  pptx.defineSlideMaster({ title: "CoverMaster", objects });
}

function defineDividerMasterFromCD(
  pptx: PPTXGenJS,
  cd: CoverPayload,
  slideW: number,
  slideH: number
) {
  const objects: PPTXGenJS.SlideMasterObject[] = [];

  if (cd.style === "band") {
    objects.push({ rect: { x: 0, y: 0, w: "100%", h: "100%", fill: { color: rgb(cd.brand.accents[2], "111827") } } });
    objects.push({ rect: { x: 0, y: slideH*0.35, w: "100%", h: slideH*0.30, fill: { color: rgb(cd.brand.accents[1], "F99F1B") } } });
  } else if (cd.style === "left") {
    objects.push({ rect: { x: 0, y: 0, w: "100%", h: "100%", fill: { color: rgb(cd.brand.accents[0], "0EA5E9") } } });
    objects.push({ rect: { x: 0.6, y: slideH*0.3, w: 0.12, h: slideH*0.40, fill: { color: "FFFFFF" } } });
  } else if (cd.style === "wash") {
    objects.push({ rect: { x: 0, y: 0, w: "100%", h: "100%", fill: { color: rgb(cd.brand.accents[2], "111827") } } });
    objects.push({ rect: { x: 0, y: 0, w: "100%", h: "100%", fill: { color: rgb(cd.brand.accents[1], "F99F1B"), transparency: 65 } } });
  } else {
    objects.push({ rect: { x: 0, y: 0, w: "100%", h: "100%", fill: { color: rgb(cd.brand.accents[1], "F99F1B") } } });
  }

  objects.push({
    text: { text: "Section Title", options: { x: 0.8, y: slideH*0.40, w: slideW-1.6, h: 1.2, align: "center", fontFace: cd.brand.fonts.heading, fontSize: 32, bold: true, color: "FFFFFF" } }
  });
  if (cd.assets.logoDataUrl && cd.assets.logoPlacement !== "none") {
    objects.push(logoObject(cd.assets, slideW, slideH));
  }

  pptx.defineSlideMaster({ title: "DividerMaster", objects });
}

What you can do now

In Cover & Divider Templates, click Use as Template Cover to save the master design.

In Presentation Template, you’ll see a green status card showing it’s active.

Click Generate Template → your PPTX uses that cover & divider set for the masters, and builds the rest of the slides with your selections and brand settings.