here’s a single, copy-paste prompt for Replit that fixes the Icons Library end-to-end so your uploaded icons actually render (no more black circles or fallback stars).

It does three things:

Uses the correct endpoint (/api/icons/list) everywhere.

Serves real SVG from the backend (with proper image/svg+xml and safe normalization).

Frontend loads the SVG (inline) and falls back to the PNG preview only if needed.

🔧 PROMPT FOR REPLIT — Fix Icons Library (serve + render real SVGs)
Backend

File: server/routes/iconLibraryRoutes.js

Ensure /api/icons/list returns each icon with either an inline svg string (preferred for small files) or a svgUrl to fetch on demand.

Add /api/icons/:id/svg to stream sanitized SVG with correct headers.

Normalize SVG so it renders with your color picker (currentColor), strip unsafe attrs, and make sure viewBox exists.

// server/routes/iconLibraryRoutes.js
const express = require("express");
const fs = require("fs/promises");
const path = require("path");
const router = express.Router();

// If you already have ObjectStorageService, keep it;
// we’ll try cloud first then local fallback.
const ObjectStorageService = require("../objectStorage"); // adjust path if needed

// --- SVG sanitizer/normalizer (server-side) ---
function normalizeSvg(svg) {
  if (!svg) return "";
  let s = String(svg);

  // Strip scripts/foreign objects/links with javascript:
  s = s
    .replace(/<script[\s\S]*?<\/script>/gi, "")
    .replace(/<foreignObject[\s\S]*?<\/foreignObject>/gi, "")
    .replace(/\son\w+="[^"]*"/gi, "") // inline event handlers
    .replace(/\shref="javascript:[^"]*"/gi, "");

  // Ensure <svg ...> has viewBox; prefer keeping natural aspect.
  if (!/viewBox=/.test(s)) {
    // quick infer from width/height if present
    const w = /width="(\d+(?:\.\d+)?)"/i.exec(s)?.[1] || "24";
    const h = /height="(\d+(?:\.\d+)?)"/i.exec(s)?.[1] || "24";
    s = s.replace(/<svg/i, `<svg viewBox="0 0 ${w} ${h}"`);
  }
  // Remove fixed width/height so it can scale in container:
  s = s.replace(/\swidth="[^"]*"/i, "").replace(/\sheight="[^"]*"/i, "");

  // Default paint to currentColor if none set on root:
  if (!/fill=/.test(s)) s = s.replace(/<svg/i, `<svg fill="currentColor"`);
  // Common case: outline icons with strokes only; enforce stroke color if missing:
  if (!/stroke=/.test(s)) s = s.replace(/<svg/i, `<svg stroke="currentColor"`);

  return s;
}

// Helper to read an icon's SVG from storage (cloud → local fallback)
async function loadIconSvg(id, filename) {
  // Expected storage keys (adjust to your actual paths):
  // Cloud/public: icons/svg/{id}/{filename}.svg
  const object = new ObjectStorageService();
  const publicPaths = object.getPublicObjectSearchPaths?.() || ["public-objects"];
  const rel = `icons/svg/${id}/${filename}`;
  const tryKeys = publicPaths.map((p) => `${p}/${rel}`);

  // Try cloud first
  for (const key of tryKeys) {
    try {
      const buf = await object.downloadObject(key);
      if (buf) return buf.toString("utf8");
    } catch {}
  }
  // Local fallback
  const localFile = path.join(process.cwd(), "uploads", "icons", "svg", id, filename);
  try {
    const buf = await fs.readFile(localFile, "utf8");
    return buf;
  } catch {}
  return "";
}

// GET /api/icons/list
// Must return: { icons: [{ id, name, style, tags, previewUrl, svg?, svgUrl? }] }
router.get("/list", async (req, res) => {
  try {
    // Load from your DB - replace with real model:
    // Each row: { id, name, style, tags, svgFilename, pngPreviewFilename }
    const rows = await require("../db/icons").listAll(); // <-- implement or adapt

    const icons = [];
    for (const r of rows) {
      const item = {
        id: String(r.id),
        name: r.name,
        style: r.style || "outlined",
        tags: r.tags || [],
        previewUrl: `/api/icons/${r.id}/preview`, // already implemented in your project
      };

      // Try to inline small SVGs (< 40KB) for instant display:
      if (r.svgFilename) {
        const raw = await loadIconSvg(r.id, r.svgFilename);
        if (raw && Buffer.byteLength(raw, "utf8") <= 40 * 1024) {
          item.svg = normalizeSvg(raw);
        } else if (raw) {
          item.svgUrl = `/api/icons/${r.id}/svg`;
        }
      }
      icons.push(item);
    }

    res.json({ icons });
  } catch (e) {
    console.error("icons/list error", e);
    res.status(500).json({ error: "Failed to list icons" });
  }
});

// Serve sanitized SVG content
router.get("/:id/svg", async (req, res) => {
  try {
    const id = req.params.id;
    const row = await require("../db/icons").getById(id);
    if (!row?.svgFilename) return res.status(404).send("Not found");

    const raw = await loadIconSvg(id, row.svgFilename);
    if (!raw) return res.status(404).send("Not found");
    const safe = normalizeSvg(raw);

    res.setHeader("Content-Type", "image/svg+xml; charset=utf-8");
    res.send(safe);
  } catch (e) {
    console.error("icons/:id/svg error", e);
    res.status(500).send("error");
  }
});

module.exports = router;


If you already have /api/icons/:id/preview, keep it as-is (PNG/JPG). We’re just adding /svg and making /list smarter.

Frontend

File: client/src/pages/.../Icons.tsx (your user-facing Icons Library)

Fetch from the right endpoint and use the SVG:

Change the query to /api/icons/list (not /api/icons/imported).

When an item has svg, render it immediately.

If it only has svgUrl, fetch it once and cache it in state.

If neither works, show the PNG previewUrl as a fallback <img>.

Add these helpers near the top:

type IconApi = {
  id: string;
  name: string;
  style: string;
  tags: string[];
  previewUrl?: string;
  svg?: string;
  svgUrl?: string;
};

function useIconsFromApi() {
  const [icons, setIcons] = React.useState<IconApi[]>([]);
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    let cancel = false;
    (async () => {
      try {
        const r = await fetch("/api/icons/list");
        const j = await r.json();
        if (cancel) return;
        setIcons(j.icons || []);
      } catch (e) {
        console.error("icons/list failed", e);
      } finally {
        if (!cancel) setLoading(false);
      }
    })();
    return () => { cancel = true; };
  }, []);

  return { icons, loading, setIcons };
}

async function fetchSvgOnce(url: string): Promise<string | null> {
  try {
    const r = await fetch(url);
    if (!r.ok) return null;
    return await r.text();
  } catch { return null; }
}


Inside your component, replace the old source with this:

const { icons, loading, setIcons } = useIconsFromApi();

// Lazy-load svg for items that only have svgUrl
React.useEffect(() => {
  let cancel = false;
  (async () => {
    const need = icons.filter(i => !i.svg && i.svgUrl);
    if (!need.length) return;
    const updates = await Promise.all(
      need.map(async (i) => {
        const text = await fetchSvgOnce(i.svgUrl!);
        return { id: i.id, svg: text || "" };
      })
    );
    if (cancel) return;
    if (updates.length) {
      setIcons(prev =>
        prev.map(p => {
          const u = updates.find(x => x.id === p.id);
          return u && u.svg ? { ...p, svg: u.svg } : p;
        })
      );
    }
  })();
  return () => { cancel = true; };
}, [icons, setIcons]);


And in your render where you currently do:

const colored = applySvgColor(icon.svg, color);
<div dangerouslySetInnerHTML={{ __html: colored }} />


…update to handle all 3 cases:

const svgMarkup = icon.svg ? applySvgColor(icon.svg, color) : null;

<div className="aspect-square flex items-center justify-center rounded-lg bg-white border border-border">
  {svgMarkup ? (
    <div className="w-16 h-16 md:w-20 md:h-20" dangerouslySetInnerHTML={{ __html: svgMarkup }} />
  ) : icon.previewUrl ? (
    <img
      src={icon.previewUrl}
      alt={icon.name}
      className="w-16 h-16 md:w-20 md:h-20 object-contain"
      loading="lazy"
    />
  ) : (
    <div className="w-16 h-16 md:w-20 md:h-20 rounded bg-gray-100" />
  )}
</div>


Your existing applySvgColor() will now work because the server normalizes icons to currentColor, and we removed fixed width/height from the SVG.

What you don’t need to change

Your uploader/importer (batch save + PNG→SVG conversion) — keep it.

Your attribution/entitlement logic for icons — unchanged.

Your color picker — now it will actually recolor the true SVGs.

Quick test checklist

Upload a small SVG icon; confirm /api/icons/list returns it with svg inline.

Upload a larger SVG; list should return svgUrl and the page should fetch and render it.

PNG/JPG with trace conversion: ensure an SVG exists server-side; page should show the SVG, fall back to preview PNG if not.

Change color picker → icon should recolor (no more black circles).