Here’s a clean, drop-in “Cover & Divider Templates” feature — UI page + backend route — that generates a PPTX with a Cover and Section Divider slides based on:

the size (16:9 or 4:3) and

a prompt you provide (I translate it into a style).

It uses your Brand Kit (colors/fonts) and supports Brand Logo vs Uploaded Logo with placement/size controls (same pattern you loved).

0) Dependencies

(Already using these in your project.)

npm i pptxgenjs express

1) Frontend — New page

Create src/pages/business-assets/templates/CoverDividerTemplates.tsx

import React, { useMemo, useState } from "react";
import { useBrandKit } from "../../../utils/brand";
import { useLocalStorage } from "../../../hooks/useLocalStorage";
import { fileToDataUrl } from "../../../utils/file";
import { Upload, Trash2 } from "lucide-react";

type Size = "16x9" | "4x3";
type LogoPlacement = "none" | "top-left" | "top-right" | "bottom-left" | "bottom-right";

function placementToStyle(p: LogoPlacement) {
  const base: React.CSSProperties = { position: "absolute" };
  if (p === "none") return { ...base, display: "none" };
  if (p.includes("top")) base.top = "6%"; else base.bottom = "6%";
  if (p.includes("left")) base.left = "6%"; else base.right = "6%";
  return base;
}

export default function CoverDividerTemplates() {
  const brand = useBrandKit();

  // Controls
  const [size, setSize] = useLocalStorage<Size>("cdt.size", "16x9");
  const [prompt, setPrompt] = useLocalStorage<string>("cdt.prompt", "Minimal modern with accent band");
  const [title, setTitle] = useLocalStorage<string>("cdt.title", "Your Presentation Title");
  const [subtitle, setSubtitle] = useLocalStorage<string>("cdt.subtitle", "Optional subtitle");
  const [useBrandLogo, setUseBrandLogo] = useLocalStorage<boolean>("cdt.useBrandLogo", !!brand.logo?.dataUrl);
  const [uploadedLogo, setUploadedLogo] = useLocalStorage<string | null>("cdt.uploadedLogo", null);
  const [logoPlacement, setLogoPlacement] = useLocalStorage<LogoPlacement>("cdt.logoPlacement", "top-right");
  const [logoScale, setLogoScale] = useLocalStorage<number>("cdt.logoScale", 1);

  const effectiveLogo = useBrandLogo ? (brand.logo?.dataUrl || null) : (uploadedLogo || null);
  const canGenerate = useMemo(() => prompt.trim().length > 1, [prompt]);

  async function onPickLogo(e: React.ChangeEvent<HTMLInputElement>) {
    const f = e.target.files?.[0];
    if (!f) return;
    setUploadedLogo(await fileToDataUrl(f));
    setUseBrandLogo(false);
  }

  async function generate() {
    const body = {
      size,
      prompt,
      brand: {
        name: brand.name,
        fonts: { heading: brand.fonts?.heading || "Inter", body: brand.fonts?.body || "Inter" },
        accents: [
          brand.colors.primary || "#0ea5e9",
          brand.colors.accent || "#f99f1b",
          brand.colors.neutral || "#64748b",
          "#8b5cf6",
          "#22c55e",
          "#ef4444",
        ],
      },
      content: { title, subtitle },
      assets: { logoDataUrl: effectiveLogo, logoPlacement, logoScale },
    };

    const res = await fetch("/api/covers/generate", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (!res.ok) {
      const j = await res.json().catch(() => ({}));
      alert(j.error || "Generate failed");
      return;
    }
    const blob = await res.blob();
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `${brand.name || "Brand"} - Cover+Dividers.pptx`;
    a.click();
    URL.revokeObjectURL(url);
  }

  return (
    <div className="p-6 space-y-6">
      <div className="flex items-center justify-between">
        <h1 className="text-2xl font-bold">Cover & Divider Templates</h1>
        <div className="flex gap-2">
          <button
            onClick={generate}
            disabled={!canGenerate}
            className={`px-4 py-2 rounded-lg ${canGenerate ? "bg-emerald-600 hover:bg-emerald-700 text-white" : "bg-slate-700 text-slate-300 cursor-not-allowed"}`}
          >
            Generate Cover & Dividers
          </button>
        </div>
      </div>

      <div className="grid xl:grid-cols-[360px,1fr] gap-6">
        {/* Left: Controls */}
        <aside className="rounded-xl border border-white/10 bg-slate-900/60 p-4 space-y-4">
          <div>
            <div className="text-sm text-slate-400">Size</div>
            <div className="mt-2 flex gap-2">
              {(["16x9","4x3"] as Size[]).map(s => (
                <button key={s} onClick={() => setSize(s)}
                  className={`px-3 py-1.5 rounded-lg border text-sm ${size===s?"bg-white/10 border-white/20":"border-white/10 hover:bg-white/5"}`}>
                  {s}
                </button>
              ))}
            </div>
          </div>

          <div>
            <div className="text-sm text-slate-400">Prompt</div>
            <textarea
              value={prompt}
              onChange={(e) => setPrompt(e.target.value)}
              className="mt-1 w-full min-h-[100px] rounded-lg bg-slate-950/60 border border-white/10 p-2 text-slate-100"
              placeholder="Describe the vibe (e.g., Minimal tech gradient, Editorial with left bar, Bold color band…)"
            />
            <p className="mt-1 text-[11px] text-slate-400">Your prompt sets the style for both Cover and Divider.</p>
          </div>

          <div>
            <div className="text-sm text-slate-400">Title & Subtitle</div>
            <input
              value={title}
              onChange={(e)=>setTitle(e.target.value)}
              className="mt-1 w-full rounded-lg bg-slate-950/60 border border-white/10 p-2 text-slate-100"
              placeholder="Title"
            />
            <input
              value={subtitle}
              onChange={(e)=>setSubtitle(e.target.value)}
              className="mt-2 w-full rounded-lg bg-slate-950/60 border border-white/10 p-2 text-slate-100"
              placeholder="Subtitle (optional)"
            />
          </div>

          {/* Logo controls */}
          <div>
            <div className="text-sm text-slate-400 mb-1">Logo</div>
            <label className="inline-flex items-center gap-2">
              <input type="checkbox" className="accent-sky-500" checked={useBrandLogo} onChange={(e)=>setUseBrandLogo(e.target.checked)} />
              <span className="text-sm text-slate-300">Use Brand Kit logo</span>
            </label>

            {!useBrandLogo && (
              <>
                {uploadedLogo ? (
                  <div className="relative rounded-lg border border-white/10 bg-slate-950/60 p-2 mt-2">
                    <img src={uploadedLogo} className="max-h-20 object-contain mx-auto" />
                    <button className="absolute top-2 right-2 p-1.5 rounded bg-rose-600 text-white hover:bg-rose-700" onClick={()=>setUploadedLogo(null)}>
                      <Trash2 className="w-4 h-4" />
                    </button>
                  </div>
                ) : (
                  <label className="block mt-2">
                    <input type="file" accept="image/*" onChange={onPickLogo} className="hidden" />
                    <div className="cursor-pointer inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-white/10 hover:bg-white/5">
                      <Upload className="w-4 h-4" /> Upload Logo
                    </div>
                  </label>
                )}
              </>
            )}

            <div className="mt-3 grid grid-cols-2 gap-3">
              <div>
                <div className="text-xs text-slate-400 mb-1">Placement</div>
                <div className="flex flex-wrap gap-2">
                  {(["none","top-left","top-right","bottom-left","bottom-right"] as LogoPlacement[]).map(p=>(
                    <button key={p}
                      className={`px-2.5 py-1.5 rounded border text-xs ${logoPlacement===p?"bg-white/10 border-white/20":"border-white/10 hover:bg-white/5"}`}
                      onClick={()=>setLogoPlacement(p)}
                    >{p.replace("-"," ")}</button>
                  ))}
                </div>
              </div>
              <div>
                <div className="text-xs text-slate-400 mb-1">Logo size</div>
                <input type="range" min={0.6} max={2} step={0.1} value={logoScale} onChange={(e)=>setLogoScale(parseFloat(e.target.value))} className="w-full" />
              </div>
            </div>
          </div>

          {/* Mini preview */}
          <div>
            <div className="text-xs text-slate-400 mb-1">Preview (approx.)</div>
            <div className={`relative w-full ${size==="16x9"?"aspect-video":"aspect-[4/3]"} rounded-lg border border-dashed border-white/15 bg-white`}>
              {/* Title/Sub hints */}
              <div className="absolute left-[6%] right-[6%] top-[30%] h-6 rounded" style={{background:"rgba(0,0,0,0.06)"}}/>
              <div className="absolute left-[10%] right-[10%] top-[45%] h-4 rounded" style={{background:"rgba(0,0,0,0.05)"}}/>
              {/* Logo */}
              <div style={{
                ...placementToStyle(logoPlacement),
                width: `${8*logoScale}%`,
                aspectRatio: "1/1",
                display: logoPlacement==="none"?"none":"block"
              }}>
                {(effectiveLogo)
                  ? <img src={effectiveLogo} className="w-full h-full object-contain" />
                  : <div className="w-full h-full bg-black/85 text-white flex items-center justify-center text-[10px] rounded">LOGO</div>}
              </div>
            </div>
          </div>
        </aside>

        {/* Right: About */}
        <section className="rounded-xl border border-white/10 bg-slate-900/60 p-5 space-y-2">
          <h2 className="text-lg font-semibold">How it works</h2>
          <ol className="list-decimal pl-5 text-slate-300 space-y-1">
            <li>Choose size (16:9 or 4:3).</li>
            <li>Describe the vibe in the prompt.</li>
            <li>Set title/subtitle and logo options.</li>
            <li>Click <b>Generate Cover & Dividers</b> — you’ll download a PPTX with a Cover and a Divider master, styled from the prompt and your brand kit.</li>
          </ol>
          <p className="text-xs text-slate-400">The generated file can be used standalone or injected into your Presentation Template workflow.</p>
        </section>
      </div>
    </div>
  );
}

2) Wire the route in your app navigation

Add to routes/nav (where you added Presentation Template earlier).

// routes.tsx (excerpt)
const CoverDividerTemplates = React.lazy(() => import("./pages/business-assets/templates/CoverDividerTemplates"));
/* in NAV */
{ label: "Cover & Divider Templates", path: "/business-assets/templates/cover-dividers", icon: <Presentation className="w-4 h-4" /> },
/* in ROUTES */
{ path: "/business-assets/templates/cover-dividers", element: <CoverDividerTemplates /> },

3) Backend — /api/covers/generate

Create server/api/covers/generate.ts

This translates the user prompt into a lightweight theme (band / centered / left / wash) and builds:

Master Title (Cover)

Master Divider (Section Divider)

Applies logo per placement/scale

Uses brand accents + fonts

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

const router = Router();

type ReqBody = {
  size: "16x9" | "4x3";
  prompt: string;
  brand: { name?: string; fonts: { heading: string; body: string }; accents: string[] };
  content: { title: string; subtitle?: string };
  assets: { logoDataUrl: string | null; logoPlacement: "none"|"top-left"|"top-right"|"bottom-left"|"bottom-right"; logoScale: number };
};

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

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

    // Choose a style based on prompt keywords
    const style = pickStyle(b.prompt);

    // Masters: Cover
    const coverMasterObjects: PPTXGenJS.SlideMasterObject[] = [];
    // Backgrounds by style
    if (style === "band") {
      coverMasterObjects.push({ rect: { x: 0, y: 0, w: "100%", h: "100%", fill: { color: hex(b.brand.accents[2] || "#111827") } } });
      // middle band
      coverMasterObjects.push({ rect: { x: 0, y: slideH*0.32, w: "100%", h: slideH*0.36, fill: { color: hex(b.brand.accents[0] || "#0ea5e9") } } });
    } else if (style === "left") {
      coverMasterObjects.push({ rect: { x: 0, y: 0, w: "100%", h: "100%", fill: { color: hex(b.brand.accents[2] || "#111827") } } });
      // left bar
      coverMasterObjects.push({ rect: { x: 0.6, y: slideH*0.26, w: 0.12, h: slideH*0.48, fill: { color: hex(b.brand.accents[1] || "#f99f1b") } } });
    } else if (style === "wash") {
      coverMasterObjects.push({ rect: { x: 0, y: 0, w: "100%", h: "100%", fill: { color: hex(b.brand.accents[2] || "#111827") } } });
      // translucent wash
      coverMasterObjects.push({ rect: { x: 0, y: 0, w: "100%", h: "100%", fill: { color: hex(b.brand.accents[0] || "#0ea5e9"), transparency: 60 } } });
    } else {
      // centered default
      coverMasterObjects.push({ rect: { x: 0, y: 0, w: "100%", h: "100%", fill: { color: hex(b.brand.accents[0] || "#0ea5e9") } } });
    }

    // Title/Sub placeholders in master
    coverMasterObjects.push({
      text: { text: b.content.title || "Title", options: { x: 0.8, y: slideH*0.32, w: slideW-1.6, h: 1.6, align: "center", fontFace: b.brand.fonts.heading, fontSize: 42, bold: true, color: "FFFFFF" } }
    });
    if (b.content.subtitle?.trim()) {
      coverMasterObjects.push({
        text: { text: b.content.subtitle!, options: { x: 1.2, y: slideH*0.48, w: slideW-2.4, h: 1.1, align: "center", fontFace: b.brand.fonts.body, fontSize: 20, color: "FFFFFF" } }
      });
    }

    // Add logo (if any) to master objects
    if (b.assets.logoDataUrl && b.assets.logoPlacement !== "none") {
      coverMasterObjects.push(logoObject(b.assets, slideW, slideH));
    }

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

    // Masters: Divider
    const dividerObjects: PPTXGenJS.SlideMasterObject[] = [];
    if (style === "band") {
      dividerObjects.push({ rect: { x: 0, y: 0, w: "100%", h: "100%", fill: { color: hex(b.brand.accents[2] || "#111827") } } });
      dividerObjects.push({ rect: { x: 0, y: slideH*0.35, w: "100%", h: slideH*0.30, fill: { color: hex(b.brand.accents[1] || "#f99f1b") } } });
    } else if (style === "left") {
      dividerObjects.push({ rect: { x: 0, y: 0, w: "100%", h: "100%", fill: { color: hex(b.brand.accents[0] || "#0ea5e9") } } });
      dividerObjects.push({ rect: { x: 0.6, y: slideH*0.3, w: 0.12, h: slideH*0.40, fill: { color: "FFFFFF" } } });
    } else if (style === "wash") {
      dividerObjects.push({ rect: { x: 0, y: 0, w: "100%", h: "100%", fill: { color: hex(b.brand.accents[2] || "#111827") } } });
      dividerObjects.push({ rect: { x: 0, y: 0, w: "100%", h: "100%", fill: { color: hex(b.brand.accents[1] || "#f99f1b"), transparency: 65 } } });
    } else {
      dividerObjects.push({ rect: { x: 0, y: 0, w: "100%", h: "100%", fill: { color: hex(b.brand.accents[1] || "#f99f1b") } } });
    }
    dividerObjects.push({
      text: { text: "Section Title", options: { x: 0.8, y: slideH*0.40, w: slideW-1.6, h: 1.2, align: "center", fontFace: b.brand.fonts.heading, fontSize: 32, bold: true, color: "FFFFFF" } }
    });
    if (b.assets.logoDataUrl && b.assets.logoPlacement !== "none") {
      dividerObjects.push(logoObject(b.assets, slideW, slideH));
    }

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

    // Slides using masters
    pptx.addSlide({ masterName: "CoverMaster" });
    pptx.addSlide({ masterName: "DividerMaster" });

    // Return file
    const buf = await pptx.write("nodebuffer");
    res.setHeader("Content-Disposition", 'attachment; filename="cover+dividers.pptx"');
    res.setHeader("Content-Type", "application/vnd.openxmlformats-officedocument.presentationml.presentation");
    res.send(buf);
  } catch (e: any) {
    console.error("[covers/generate] error", e);
    res.status(500).json({ error: "Failed to generate Cover & Dividers" });
  }
});

export default router;

// ----- helpers -----
function hex(x?: string) {
  const h = (x || "").replace("#","");
  return (h.length===6 ? h : "000000").toUpperCase();
}
function logoObject(
  assets: ReqBody["assets"],
  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 = 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;
    default: x = -999; y = -999; // hidden
  }
  return { image: { data: assets.logoDataUrl!, x, y, w, h } };
}

// crude style inference from prompt
function pickStyle(prompt: string): "centered"|"band"|"left"|"wash" {
  const p = (prompt || "").toLowerCase();
  if (p.includes("band") || p.includes("stripe") || p.includes("bar")) return "band";
  if (p.includes("left") || p.includes("editorial") || p.includes("sidebar")) return "left";
  if (p.includes("wash") || p.includes("overlay") || p.includes("photo")) return "wash";
  if (p.includes("center") || p.includes("hero")) return "centered";
  return "centered";
}


Mount it in your server:

// server/server.ts
import coversRoute from "./api/covers/generate";
app.use("/api/covers/generate", coversRoute);

4) What this gives you right now

A new Cover & Divider Templates page where the user:

picks size, enters a prompt, sets title/subtitle,

chooses Brand Logo vs Upload, and placement/size,

clicks Generate to download a PPTX with a Cover master and a Divider master, styled to the prompt and brand colors.

No PDFs, no font packaging — just crisp, branded PPTX masters ready to inject into your template flow