- {iconUrl && !imgError ? (
-

setImgError(true)}
- className="discovery-card-icon-img"
- style={{
- width: 44,
- height: 44,
- borderRadius: 10,
- }}
- />
- ) : (
-
- {service.name[0]}
-
- )}
+
;
+ }
+
+ const needsInvert = isDark === manifest.lightBg.has(service.id);
+
+ return (
+

setImgError(true)}
+ />
+ );
+}
+
+export function ServiceLogoFallback({
+ name,
+ size = 28,
+}: {
+ name: string;
+ size?: number;
+}) {
+ const initials = name
+ .split(/[\s-]+/)
+ .slice(0, 2)
+ .map((w) => w.charAt(0).toUpperCase())
+ .join("");
+
+ return (
+
1
+ ? Math.round(size * 0.36)
+ : Math.round(size * 0.46),
+ fontWeight: 600,
+ letterSpacing: "-0.02em",
+ color: "var(--vocs-text-color-secondary)",
+ border:
+ "1px solid light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.08))",
+ }}
+ >
+ {initials || "?"}
+
+ );
+}
+
+const EMPTY_MANIFEST: IconManifest = {
+ transparent: new Set(),
+ lightBg: new Set(),
+};
+
+function useIconManifest(): IconManifest {
+ const [manifest, setManifest] = useState
(EMPTY_MANIFEST);
+ useEffect(() => {
+ fetchIconManifest().then(setManifest);
+ }, []);
+ return manifest;
+}
diff --git a/src/components/ServicesPage.tsx b/src/components/ServicesPage.tsx
index c7aaad15..8d3c90ec 100644
--- a/src/components/ServicesPage.tsx
+++ b/src/components/ServicesPage.tsx
@@ -3,8 +3,9 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Link } from "vocs";
import type { Category, Endpoint, Service } from "../data/registry";
-import { fetchServices, iconUrl } from "../data/registry";
+import { fetchServices } from "../data/registry";
import { ServiceDiscovery } from "./ServiceDiscovery";
+import { ServiceLogo } from "./ServiceLogo";
export const CATEGORY_LABELS: Record = {
ai: "AI",
@@ -2943,38 +2944,8 @@ function BorderlessBadge({ children }: { children: React.ReactNode }) {
// Service icon with optional first-party overlay
// ---------------------------------------------------------------------------
-function FallbackIcon({ name }: { name: string }) {
- const initials = name
- .split(/[\s-]+/)
- .slice(0, 2)
- .map((w) => w.charAt(0).toUpperCase())
- .join("");
- return (
- 1 ? 10 : 13,
- fontWeight: 600,
- letterSpacing: "-0.02em",
- color: "var(--vocs-text-color-secondary)",
- border:
- "1px solid light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.08))",
- }}
- >
- {initials || "?"}
-
- );
-}
-
function ServiceIcon({ service: s }: { service: Service }) {
const isFirstParty = s.integration !== "third-party";
- const [imgError, setImgError] = useState(false);
return (
- {s.id && !imgError ? (
-
})
setImgError(true)}
- />
- ) : (
-
- )}
+
{isFirstParty && (
${letter}`;
@@ -18,7 +15,7 @@ function letterSvg(id: string): string {
async function blobGet(
id: string,
-): Promise<{ body: ReadableStream; contentType: string } | null> {
+): Promise<{ data: ArrayBuffer; contentType: string } | null> {
if (!BLOB_TOKEN) return null;
try {
for (const ext of ["png", "svg"]) {
@@ -29,11 +26,11 @@ async function blobGet(
});
if (blobs.length > 0) {
const res = await fetch(blobs[0].url);
- if (res.ok && res.body) {
+ if (res.ok) {
const ct =
res.headers.get("content-type") ??
(ext === "svg" ? "image/svg+xml" : `image/${ext}`);
- return { body: res.body, contentType: ct };
+ return { data: await res.arrayBuffer(), contentType: ct };
}
}
}
@@ -43,41 +40,41 @@ async function blobGet(
return null;
}
-async function staticIcon(
- request: Request,
- id: string,
-): Promise {
- try {
- const origin = new URL(request.url).origin;
- const res = await fetch(`${origin}/icons/${id}.svg`);
- if (res.ok) {
- return new Response(await res.text(), {
- headers: { ...FALLBACK_HEADERS, ...CACHE_HEADERS },
- });
- }
- } catch {
- // static file not available
- }
- return null;
-}
-
export async function GET(request: Request) {
- const id = new URL(request.url).searchParams.get("id");
+ const url = new URL(request.url);
+ const id = url.searchParams.get("id");
if (!id) return new Response("Missing id parameter", { status: 400 });
- // 1. Static override (public/icons/*.svg) — hand-curated icons take priority
- const override = await staticIcon(request, id);
- if (override) return override;
-
- // 2. Vercel Blob (logo.dev sync)
+ // 1. Vercel Blob (synced from logo.dev)
const blob = await blobGet(id);
if (blob) {
- return new Response(blob.body, {
+ return new Response(blob.data, {
headers: { "Content-Type": blob.contentType, ...CACHE_HEADERS },
});
}
+ // 2. Live logo.dev fallback
+ if (LOGODEV_PK) {
+ const domain = url.searchParams.get("domain");
+ if (domain) {
+ try {
+ const res = await fetch(logoDevUrl(domain, { token: LOGODEV_PK }));
+ if (res.ok) {
+ return new Response(await res.arrayBuffer(), {
+ headers: { "Content-Type": "image/png", ...CACHE_HEADERS },
+ });
+ }
+ } catch (e) {
+ console.error(`[icon] logo.dev fallback error for ${domain}:`, e);
+ }
+ }
+ }
+
// 3. Letter SVG (guaranteed — never 404)
- console.warn(`[icon] no icon found for ${id}, generating letter fallback`);
- return new Response(letterSvg(id), { headers: FALLBACK_HEADERS });
+ return new Response(letterSvg(id), {
+ headers: {
+ "Content-Type": "image/svg+xml",
+ "Cache-Control": "public, s-maxage=3600, stale-while-revalidate",
+ },
+ });
}
diff --git a/src/pages/_api/api/og.tsx b/src/pages/_api/api/og.tsx
index d8798f59..e2d67fa6 100644
--- a/src/pages/_api/api/og.tsx
+++ b/src/pages/_api/api/og.tsx
@@ -1,7 +1,7 @@
// @ts-nocheck – server-only, uses Vite ?raw import and resvg native module
import { initWasm, Resvg } from "@resvg/resvg-wasm";
import resvgWasm from "@resvg/resvg-wasm/index_bg.wasm?url";
-import ogDescriptions from "../../../generated/og-descriptions.json";
+import imageDescriptions from "../../../generated/og-descriptions.json";
import templateSvg from "./og-template.svg?raw";
const BLOB = "https://wgfdjv2jfqz2dlpx.public.blob.vercel-storage.com";
@@ -230,7 +230,7 @@ export async function GET(request: Request) {
const rawDescription = url.searchParams.get("description") || "";
const path = decodeURIComponent(url.searchParams.get("path") || "");
const description =
- (ogDescriptions as Record)[path] || rawDescription;
+ (imageDescriptions as Record)[path] || rawDescription;
const category = getCategoryForPath(path);
const subcategory = getSubcategoryForPath(path);
diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx
index c05a9e22..ee0dd2b3 100644
--- a/src/pages/_layout.tsx
+++ b/src/pages/_layout.tsx
@@ -447,14 +447,14 @@ export default function Layout(props: React.PropsWithChildren) {
)}