Here’s a one-shot package the team can drop in to make previews work and wire up the upload + manifest end-to-end. I’m giving both sides:

the client page (already shared) with bulletproof previews, and

a server upload endpoint that saves files and updates a JSON manifest.

If you’re on Next.js, use the Next API route. If you’re on Vite/CRA + Express, use the Express route. (They’re equivalent.)

1) Client (you already have this)

Use the full drop-in AdminUploadLogoTemplate.tsx I sent just above. It:

Previews SVG (sanitized + token/var injection) and PNG/JPG instantly.

Validates SVG tokens {Brand_Name}, {Tagline}, {Est_Year} and CSS vars --primary/--secondary/--accent on upload.

Posts multipart form to /api/logo-templates/upload.

2A) Next.js API route (recommended)

Install deps:

npm i formidable
npm i -D @types/formidable


pages/api/logo-templates/upload.ts

import type { NextApiRequest, NextApiResponse } from "next";
import formidable, { File } from "formidable";
import path from "path";
import fs from "fs";

export const config = {
  api: { bodyParser: false }, // let formidable handle multipart
};

type ManifestItem = {
  id: string;
  name: string;
  tags: string[];
  svgPath: string;
  previewPath: string;
  createdAt: string;
};

const ensureDir = (p: string) => {
  if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
};

const sanitizeId = (id: string) =>
  (id || "")
    .toLowerCase()
    .trim()
    .replace(/[^a-z0-9\-]/g, "-")
    .replace(/\-+/g, "-");

const parseForm = (req: NextApiRequest) =>
  new Promise<{ fields: formidable.Fields; files: formidable.Files }>(
    (resolve, reject) => {
      const form = formidable({
        multiples: false,
        maxFileSize: 20 * 1024 * 1024,
        keepExtensions: true,
      });
      form.parse(req, (err, fields, files) => {
        if (err) reject(err);
        else resolve({ fields, files });
      });
    }
  );

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== "POST") return res.status(405).json({ error: "Method not allowed" });

  try {
    const { fields, files } = await parseForm(req);

    const id = sanitizeId(String(fields.id || ""));
    const name = String(fields.name || "");
    const tags = String(fields.tags || "")
      .split(",")
      .map((t) => t.trim())
      .filter(Boolean);

    if (!id || !name) return res.status(400).json({ error: "id and name are required" });

    const svg = files.svg as File | undefined;
    const preview = files.preview as File | undefined;

    if (!svg || !preview) return res.status(400).json({ error: "svg and preview are required" });

    // Validate mimetypes/extensions lightly
    const svgOk =
      (svg.mimetype && svg.mimetype === "image/svg+xml") ||
      (svg.originalFilename && svg.originalFilename.toLowerCase().endsWith(".svg"));
    if (!svgOk) return res.status(400).json({ error: "SVG must be image/svg+xml" });

    const previewOk =
      (preview.mimetype && /image\/(png|jpeg)/i.test(preview.mimetype)) ||
      (preview.originalFilename &&
        /\.(png|jpe?g)$/i.test(preview.originalFilename));
    if (!previewOk) return res.status(400).json({ error: "Preview must be PNG or JPG" });

    // Save files under /public/logo-templates/{id}/
    const baseDir = path.join(process.cwd(), "public", "logo-templates", id);
    ensureDir(baseDir);

    const svgPathAbs = path.join(baseDir, "template.svg");
    const previewExt = (preview.originalFilename || "").split(".").pop() || "png";
    const previewPathAbs = path.join(baseDir, `preview.${previewExt}`);

    // Move/copy uploaded temp files
    fs.copyFileSync(svg.filepath, svgPathAbs);
    fs.copyFileSync(preview.filepath, previewPathAbs);

    // Update manifest
    const manifestDir = path.join(process.cwd(), "public", "logo-templates");
    ensureDir(manifestDir);
    const manifestPath = path.join(manifestDir, "manifest.json");

    let manifest: ManifestItem[] = [];
    if (fs.existsSync(manifestPath)) {
      try {
        manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
        if (!Array.isArray(manifest)) manifest = [];
      } catch {
        manifest = [];
      }
    }

    // Paths as served by Next (public/)
    const svgPath = `/logo-templates/${id}/template.svg`;
    const previewPath = `/logo-templates/${id}/` + path.basename(previewPathAbs);

    // Upsert by id
    const idx = manifest.findIndex((m) => m.id === id);
    const item: ManifestItem = {
      id,
      name,
      tags,
      svgPath,
      previewPath,
      createdAt: new Date().toISOString(),
    };
    if (idx >= 0) manifest[idx] = item;
    else manifest.push(item);

    fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf8");

    return res.status(200).json({ ok: true, item });
  } catch (err: any) {
    console.error(err);
    return res.status(500).json({ error: err?.message || "Upload failed" });
  }
}


Folder expectations (after first upload):

public/
  logo-templates/
    manifest.json
    {template-id}/
      template.svg
      preview.png (or .jpg)

2B) Express route (if not using Next)

Install deps:

npm i express formidable
npm i -D @types/express @types/formidable


server/routes/logoTemplates.ts

import { Router } from "express";
import formidable, { File } from "formidable";
import fs from "fs";
import path from "path";

const router = Router();

const ensureDir = (p: string) => {
  if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
};

const sanitizeId = (id: string) =>
  (id || "")
    .toLowerCase()
    .trim()
    .replace(/[^a-z0-9\-]/g, "-")
    .replace(/\-+/g, "-");

router.post("/upload", (req, res) => {
  const form = formidable({ keepExtensions: true, maxFileSize: 20 * 1024 * 1024 });
  form.parse(req, (err, fields, files) => {
    if (err) return res.status(400).json({ error: err.message });

    const id = sanitizeId(String(fields.id || ""));
    const name = String(fields.name || "");
    const tags = String(fields.tags || "")
      .split(",")
      .map((t) => t.trim())
      .filter(Boolean);

    if (!id || !name) return res.status(400).json({ error: "id and name are required" });

    const svg = files.svg as File | undefined;
    const preview = files.preview as File | undefined;
    if (!svg || !preview) return res.status(400).json({ error: "svg and preview are required" });

    const svgOk =
      (svg.mimetype && svg.mimetype === "image/svg+xml") ||
      (svg.originalFilename && svg.originalFilename.toLowerCase().endsWith(".svg"));
    if (!svgOk) return res.status(400).json({ error: "SVG must be image/svg+xml" });

    const previewOk =
      (preview.mimetype && /image\/(png|jpeg)/i.test(preview.mimetype)) ||
      (preview.originalFilename && /\.(png|jpe?g)$/i.test(preview.originalFilename || ""));
    if (!previewOk) return res.status(400).json({ error: "Preview must be PNG or JPG" });

    const baseDir = path.join(process.cwd(), "public", "logo-templates", id);
    ensureDir(baseDir);

    const svgPathAbs = path.join(baseDir, "template.svg");
    const previewExt = (preview.originalFilename || "").split(".").pop() || "png";
    const previewPathAbs = path.join(baseDir, `preview.${previewExt}`);

    fs.copyFileSync(svg.filepath, svgPathAbs);
    fs.copyFileSync(preview.filepath, previewPathAbs);

    const manifestDir = path.join(process.cwd(), "public", "logo-templates");
    ensureDir(manifestDir);
    const manifestPath = path.join(manifestDir, "manifest.json");

    let manifest: any[] = [];
    if (fs.existsSync(manifestPath)) {
      try {
        manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
        if (!Array.isArray(manifest)) manifest = [];
      } catch {
        manifest = [];
      }
    }

    const svgPath = `/logo-templates/${id}/template.svg`;
    const previewPath = `/logo-templates/${id}/` + path.basename(previewPathAbs);

    const item = {
      id,
      name,
      tags,
      svgPath,
      previewPath,
      createdAt: new Date().toISOString(),
    };

    const idx = manifest.findIndex((m) => m.id === id);
    if (idx >= 0) manifest[idx] = item;
    else manifest.push(item);

    fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
    res.json({ ok: true, item });
  });
});

export default router;


server/index.ts (snippet to mount route)

import express from "express";
import path from "path";
import logoTemplates from "./routes/logoTemplates";

const app = express();

// Serve /public so previews work when referenced
app.use(express.static(path.join(process.cwd(), "public")));

app.use("/api/logo-templates", logoTemplates);

const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`Server listening on ${port}`));

3) Minimal manifest schema (for reference)
[
  {
    "id": "logo-emblem-law-v1",
    "name": "Law Emblem V1",
    "tags": ["modern", "emblem"],
    "svgPath": "/logo-templates/logo-emblem-law-v1/template.svg",
    "previewPath": "/logo-templates/logo-emblem-law-v1/preview.png",
    "createdAt": "2025-09-26T04:28:00.000Z"
  }
]

4) CSP / Preview gotchas checklist

If using a CSP, allow:

img-src 'self' data:;

style-src 'self' 'unsafe-inline'; (only if needed for inline SVG styles)

default-src 'self';

Ensure the preview container isn’t hidden by a parent overflow: hidden with zero height.

Some browsers sniff file type by extension; we set both accept and MIME types on inputs.