import { mkdir, readFile, writeFile } from "node:fs/promises";
type Customer = { name: string; domain: string; height: number };
type WallEntry = Customer & {
etag: string | null;
light: string | null;
dark: string | null;
};
// Optical heights are tuned by eye. All-caps marks need less height.
const CUSTOMERS: Customer[] = [
{ name: "Stripe", domain: "stripe.com", height: 26 },
{ name: "Twilio", domain: "twilio.com", height: 26 },
{ name: "DoorDash", domain: "doordash.com", height: 20 },
{ name: "Vercel", domain: "vercel.com", height: 22 },
{ name: "Discord", domain: "discord.com", height: 24 },
{ name: "Airbnb", domain: "airbnb.com", height: 25 },
{ name: "Spotify", domain: "spotify.com", height: 26 },
{ name: "Datadog", domain: "datadoghq.com", height: 19 },
];
const OUT_DIR = "public/logo-wall";
const MANIFEST = `${OUT_DIR}/manifest.json`;
const sleep = (seconds: number) =>
new Promise((resolve) => setTimeout(resolve, seconds * 1000));
async function lookupBrandmark(domain: string) {
const url = new URL("https://api.logo.dev/v2/brands/brandmark");
url.searchParams.set("domain", domain);
// Retry a 202 (still indexing) until its retry window ends:
// https://www.logo.dev/docs/platform/errors#not-found-vs-still-indexing-202
let deadline = 0;
while (deadline === 0 || Date.now() < deadline) {
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.LOGO_DEV_SECRET_KEY}` },
signal: AbortSignal.timeout(15_000),
});
if (res.status === 202) {
const { metadata } = await res.json();
deadline ||= Date.now() + metadata.pending.expires_in_seconds * 1000;
await sleep(Number(res.headers.get("Retry-After")) || metadata.pending.retry_after_seconds);
continue;
}
if (res.status === 404) {
return null; // No brand, or the brand has no brandmark.
}
if (!res.ok) {
throw new Error(`${domain}: HTTP ${res.status}`);
}
const { data }: { data: { url: string; etag: string } } = await res.json();
return data;
}
throw new Error(`${domain}: still indexing`);
}
async function download(signedUrl: string, theme: "light" | "dark", file: string) {
// Send no key with the signed URL: https://www.logo.dev/docs/brand/introduction#images
const url = new URL(signedUrl);
url.searchParams.set("size", "320");
url.searchParams.set("format", "webp");
url.searchParams.set("theme", theme);
const res = await fetch(url);
if (!res.ok) {
throw new Error(`${file}: HTTP ${res.status}`);
}
await writeFile(`${OUT_DIR}/${file}`, Buffer.from(await res.arrayBuffer()));
return `/logo-wall/${file}`;
}
async function main() {
await mkdir(OUT_DIR, { recursive: true });
const previous: WallEntry[] = JSON.parse(
await readFile(MANIFEST, "utf8").catch(() => "[]")
);
const entries: WallEntry[] = [];
for (const customer of CUSTOMERS) {
const old = previous.find((e) => e.domain === customer.domain);
try {
const mark = await lookupBrandmark(customer.domain);
if (!mark) {
entries.push({ ...customer, etag: null, light: null, dark: null });
} else if (old?.etag === mark.etag && old.light && old.dark) {
entries.push({ ...old, ...customer });
} else {
const slug = customer.domain.replace(/\W/g, "-");
entries.push({
...customer,
etag: mark.etag,
light: await download(mark.url, "light", `${slug}-light.webp`),
dark: await download(mark.url, "dark", `${slug}-dark.webp`),
});
}
} catch (err) {
// Keep the last good copy, so one failed lookup never empties a slot.
console.error(err);
entries.push(old ?? { ...customer, etag: null, light: null, dark: null });
}
}
await writeFile(MANIFEST, JSON.stringify(entries, null, 2));
}
main();