Frontend changes
src/utils/pngLogo.ts — replace the placeholder generator with an API call
// src/utils/pngLogo.ts

// Call your backend to generate a photorealistic PNG with transparent background
export async function generateTransparentIconPNG(opts: {
  prompt: string;
  size?: number;        // 512, 768, 1024
  guidance?: number;    // optional, 3-8
  photo?: boolean;      // hint: photorealistic vs illustration
}): Promise<string> {
  const res = await fetch("/api/generate-image", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      prompt: opts.prompt,
      size: opts.size ?? 1024,
      guidance: opts.guidance ?? 5,
      style: opts.photo ? "photo" : "illustration",
      transparent: true,
    }),
  });
  if (!res.ok) {
    const text = await res.text().catch(() => "");
    throw new Error(`Image generation failed: ${res.status} ${text}`);
  }
  const { dataUrl } = await res.json();
  return dataUrl as string; // "data:image/png;base64,...."
}

// (unchanged) composeIconWithText stays as you already have it

(Optional) Improve the preview background so transparency is obvious

Where you render the icon preview, wrap the <img> with a checkerboard:

<div
  className="rounded-lg border p-3 grid place-items-center"
  style={{
    background:
      "conic-gradient(#f8fafc 25%, #eef2f7 0 50%, #f8fafc 0 75%, #eef2f7 0) 0 0/16px 16px",
  }}
>
  {iconUrl ? (
    <img src={iconUrl} alt="Generated icon" className="max-h-40 object-contain" />
  ) : (
    <div className="text-xs text-neutral-500">No image yet.</div>
  )}
</div>


And in your “Generate image (PNG)” button handler, call with photo: true:

const url = await generateTransparentIconPNG({
  prompt: iconPrompt,
  size: 1024,
  guidance: 5,
  photo: true,
});
setIconUrl(url);

Backend (Next.js API route)

This example uses Replicate for:

Generation: FLUX dev (or SDXL) for high-quality images

Background removal: the rembg model to get a true alpha PNG

Install deps:

npm i replicate


Set env var: REPLICATE_API_TOKEN=...

pages/api/generate-image.ts
import type { NextApiRequest, NextApiResponse } from "next";
import Replicate from "replicate";

const replicate = new Replicate({
  auth: process.env.REPLICATE_API_TOKEN!,
});

// Helper to turn ArrayBuffer -> data URL
function bufferToDataUrl(buf: ArrayBuffer, mime = "image/png") {
  const b64 = Buffer.from(buf).toString("base64");
  return `data:${mime};base64,${b64}`;
}

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  try {
    if (req.method !== "POST") return res.status(405).end();

    const {
      prompt,
      size = 1024,
      style = "photo",   // "photo" | "illustration"
      guidance = 5,
      transparent = true,
    } = req.body || {};

    if (!prompt || typeof prompt !== "string") {
      return res.status(400).json({ error: "Missing prompt" });
    }

    // 1) Generate image (FLUX.1 [dev] is fast & high quality)
    //    You can swap to "black-forest-labs/FLUX.1-schnell" or Stable Diffusion XL if you prefer.
    const positivePrompt =
      style === "photo"
        ? `${prompt}, ultra-detailed, photorealistic, 50mm lens, soft lighting, high dynamic range`
        : `${prompt}, clean vector look, flat illustration, minimal, high contrast`;

    const fluxOutput = (await replicate.run(
      "black-forest-labs/flux-dev", // model id
      {
        input: {
          prompt: positivePrompt,
          width: size,
          height: size,
          guidance, // classifier-free guidance
          num_inference_steps: 30,
          output_format: "png", // opaque PNG first
        },
      }
    )) as any;

    // Replicate responses vary slightly by model; normalize to a single URL
    const genUrl: string = Array.isArray(fluxOutput) ? fluxOutput[0] : fluxOutput?.[0] ?? fluxOutput;

    if (!genUrl || typeof genUrl !== "string") {
      return res.status(500).json({ error: "Generation failed" });
    }

    // 2) Remove background to get real transparency
    //    rembg returns PNG with alpha
    const rembgOutput = (await replicate.run("cjwbw/rembg:latest", {
      input: { image: genUrl },
    })) as any;

    const rembgUrl: string =
      typeof rembgOutput === "string" ? rembgOutput : rembgOutput?.output ?? rembgOutput?.[0];

    if (!rembgUrl) {
      return res.status(500).json({ error: "Background removal failed" });
    }

    // 3) Fetch the transparent PNG and return as data URL
    const imageResp = await fetch(rembgUrl);
    if (!imageResp.ok) {
      return res.status(500).json({ error: "Could not fetch processed image" });
    }
    const buf = await imageResp.arrayBuffer();
    const dataUrl = bufferToDataUrl(buf, "image/png");

    return res.status(200).json({ dataUrl });
  } catch (err: any) {
    console.error(err);
    return res.status(500).json({ error: err?.message ?? "Internal error" });
  }
}

Why this fixes your issue

You’ll get a real photorealistic image (not a vector placeholder).

Background is actually transparent thanks to rembg.

The preview shows the checkerboard so users can tell it’s transparent.

The result is a data:image/png;base64,... that your existing Combine button can merge with the chosen font + primary color.