| name | logo-management |
| description | Manage car logo fetching, scraping, and Vercel Blob storage in the logos package. Use when updating logo sources, debugging brand name normalization, or managing logo cache. |
| allowed-tools | Read, Edit, Bash, Grep, Glob |
Logo Management Skill
This skill helps you manage car logos in packages/logos/.
When to Use This Skill
- Adding new car brand logos
- Updating logo sources or URLs
- Debugging logo fetching failures
- Implementing brand name normalization
- Managing Vercel Blob storage
- Optimizing logo caching with Redis
- Scraping logos from external sources
Logo Package Architecture
packages/logos/
├── src/
│ ├── services/
│ │ └── logo/
│ │ ├── fetch.ts # Logo fetching logic
│ │ ├── list.ts # List available logos
│ │ └── download.ts # Download logos to Blob
│ ├── infra/
│ │ └── storage/
│ │ └── blob.ts # Vercel Blob service
│ ├── utils/
│ │ └── normalize.ts # Brand name normalization
│ └── index.ts # Package exports
├── scripts/
│ ├── fetch-logos.ts # Fetch all logos
│ └── upload-to-blob.ts # Upload to Vercel Blob
└── package.json
Core Functionality
Brand Name Normalization
// packages/logos/src/utils/normalize.ts
/**
* Normalize brand names to match logo file names
*/
export function normalizeBrandName(brand: string): string {
return brand
.toLowerCase()
.trim()
.replace(/\s+/g, "-") // Replace spaces with hyphens
.replace(/[^a-z0-9-]/g, "") // Remove special characters
.replace(/-+/g, "-") // Remove duplicate hyphens
.replace(/^-|-$/g, ""); // Remove leading/trailing hyphens
}
// Examples
normalizeBrandName("Mercedes-Benz"); // "mercedes-benz"
normalizeBrandName("BMW"); // "bmw"
normalizeBrandName("Land Rover"); // "land-rover"
normalizeBrandName("Alfa Romeo"); // "alfa-romeo"
/**
* Handle special cases and aliases
*/
const BRAND_ALIASES: Record<string, string> = {
"mercedes": "mercedes-benz",
"merc": "mercedes-benz",
"benz": "mercedes-benz",
"vw": "volkswagen",
"bmw": "bmw",
"landrover": "land-rover",
"alfa": "alfa-romeo",
};
export function resolveBrandAlias(brand: string): string {
const normalized = normalizeBrandName(brand);
return BRAND_ALIASES[normalized] || normalized;
}
Logo Fetching
// packages/logos/src/services/logo/fetch.ts
import { redis } from "@sgcarstrends/utils";
import { normalizeBrandName } from "../../utils/normalize";
const LOGO_CDN_BASE = "https://cdn.example.com/logos";
const CACHE_TTL = 7 * 24 * 60 * 60; // 7 days
export async function getLogoUrl(brand: string): Promise<string | null> {
const normalizedBrand = normalizeBrandName(brand);
const cacheKey = `logo:url:${normalizedBrand}`;
// Check cache
const cached = await redis.get<string>(cacheKey);
if (cached) {
console.log(`Logo cache hit: ${normalizedBrand}`);
return cached;
}
// Fetch logo URL
const logoUrl = await fetchLogoUrl(normalizedBrand);
if (logoUrl) {
// Cache the URL
await redis.set(cacheKey, logoUrl, { ex: CACHE_TTL });
}
return logoUrl;
}
async function fetchLogoUrl(brand: string): Promise<string | null> {
const possibleExtensions = ["svg", "png", "jpg"];
for (const ext of possibleExtensions) {
const url = `${LOGO_CDN_BASE}/${brand}.${ext}`;
try {
const response = await fetch(url, { method: "HEAD" });
if (response.ok) {
console.log(`Found logo: ${url}`);
return url;
}
} catch (error) {
console.error(`Failed to fetch ${url}:`, error);
}
}
console.warn(`No logo found for: ${brand}`);
return null;
}
Vercel Blob Storage
// packages/logos/src/infra/storage/blob.ts
import { put, list, del } from "@vercel/blob";
import { redis } from "@sgcarstrends/utils";
const BLOB_PREFIX = "logos";
export class LogoBlobService {
/**
* Upload logo to Vercel Blob
*/
async upload(brand: string, file: Buffer | File): Promise<string> {
const normalizedBrand = normalizeBrandName(brand);
const fileName = `${BLOB_PREFIX}/${normalizedBrand}.png`;
const blob = await put(fileName, file, {
access: "public",
addRandomSuffix: false,
});
// Cache blob URL
await redis.set(
`logo:blob:${normalizedBrand}`,
blob.url,
{ ex: 7 * 24 * 60 * 60 } // 7 days
);
console.log(`Uploaded logo to: ${blob.url}`);
return blob.url;
}
/**
* List all logos in Blob storage
*/
async list(): Promise<string[]> {
const { blobs } = await list({ prefix: BLOB_PREFIX });
return blobs.map(blob => blob.url);
}
/**
* Delete logo from Blob storage
*/
async delete(brand: string): Promise<void> {
const normalizedBrand = normalizeBrandName(brand);
const fileName = `${BLOB_PREFIX}/${normalizedBrand}.png`;
await del(fileName);
// Invalidate cache
await redis.del(`logo:blob:${normalizedBrand}`);
console.log(`Deleted logo: ${fileName}`);
}
/**
* Get logo URL from Blob storage
*/
async getUrl(brand: string): Promise<string | null> {
const normalizedBrand = normalizeBrandName(brand);
const cacheKey = `logo:blob:${normalizedBrand}`;
// Check cache
const cached = await redis.get<string>(cacheKey);
if (cached) {
return cached;
}
// List and find
const logos = await this.list();
const logoUrl = logos.find(url => url.includes(normalizedBrand));
if (logoUrl) {
// Cache the result
await redis.set(cacheKey, logoUrl, { ex: 7 * 24 * 60 * 60 });
}
return logoUrl || null;
}
}
export const logoBlobService = new LogoBlobService();
Logo Scraping
// packages/logos/src/services/logo/scrape.ts
import * as cheerio from "cheerio";
interface ScrapedLogo {
brand: string;
url: string;
source: string;
}
/**
* Scrape logos from car manufacturer websites
*/
export async function scrapeLogos(): Promise<ScrapedLogo[]> {
const sources = [
{
name: "CarLogos.org",
url: "https://www.carlogos.org/car-brands/",
selector: ".car-brand-logo img",
},
// Add more sources as needed
];
const logos: ScrapedLogo[] = [];
for (const source of sources) {
try {
const html = await fetch(source.url).then(res => res.text());
const $ = cheerio.load(html);
$(source.selector).each((_, element) => {
const $el = $(element);
const brand = $el.attr("alt") || "";
const url = $el.attr("src") || "";
if (brand && url) {
logos.push({
brand: normalizeBrandName(brand),
url: url.startsWith("http") ? url : new URL(url, source.url).href,
source: source.name,
});
}
});
console.log(`Scraped ${logos.length} logos from ${source.name}`);
} catch (error) {
console.error(`Failed to scrape ${source.name}:`, error);
}
}
return logos;
}
/**
* Download and upload scraped logos to Blob
*/
export async function processScrape dLogos(logos: ScrapedLogo[]) {
for (const logo of logos) {
try {
// Download logo
const response = await fetch(logo.url);
const buffer = Buffer.from(await response.arrayBuffer());
// Upload to Blob
await logoBlobService.upload(logo.brand, buffer);
console.log(`Processed logo: ${logo.brand}`);
} catch (error) {
console.error(`Failed to process ${logo.brand}:`, error);
}
}
}
Public API
// packages/logos/src/index.ts
export { getLogoUrl } from "./services/logo/fetch";
export { logoBlobService } from "./infra/storage/blob";
export { normalizeBrandName, resolveBrandAlias } from "./utils/normalize";
export { scrapeLogos, processScrapedLogos } from "./services/logo/scrape";
Usage in Applications
In API Routes
// apps/api/src/routes/logos.ts
import { Hono } from "hono";
import { getLogoUrl } from "@sgcarstrends/logos";
const app = new Hono();
app.get("/logos/:brand", async (c) => {
const brand = c.req.param("brand");
const logoUrl = await getLogoUrl(brand);
if (!logoUrl) {
return c.json({ error: "Logo not found" }, 404);
}
return c.json({ brand, logoUrl });
});
export default app;
In Next.js Components
// apps/web/src/components/car-logo.tsx
"use client";
import { useState, useEffect } from "react";
import Image from "next/image";
interface CarLogoProps {
brand: string;
size?: number;
}
export function CarLogo({ brand, size = 64 }: CarLogoProps) {
const [logoUrl, setLogoUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/logos/${brand}`)
.then(res => res.json())
.then(data => {
setLogoUrl(data.logoUrl);
setLoading(false);
})
.catch(() => {
setLoading(false);
});
}, [brand]);
if (loading) {
return <div className="animate-pulse bg-gray-200 rounded" style={{ width: size, height: size }} />;
}
if (!logoUrl) {
return (
<div
className="flex items-center justify-center bg-gray-100 rounded text-gray-500"
style={{ width: size, height: size }}
>
{brand[0].toUpperCase()}
</div>
);
}
return (
<Image
src={logoUrl}
alt={`${brand} logo`}
width={size}
height={size}
className="object-contain"
/>
);
}
Scripts
Fetch All Logos
// packages/logos/scripts/fetch-logos.ts
import { getLogoUrl } from "../src/services/logo/fetch";
import { logoBlobService } from "../src/infra/storage/blob";
const BRANDS = [
"Toyota",
"Honda",
"BMW",
"Mercedes-Benz",
"Audi",
"Volkswagen",
"Nissan",
"Mazda",
"Hyundai",
"Kia",
"Ford",
"Chevrolet",
"Tesla",
"Porsche",
"Ferrari",
"Lamborghini",
"Lexus",
"Volvo",
"Jaguar",
"Land Rover",
];
async function fetchAllLogos() {
console.log(`Fetching logos for ${BRANDS.length} brands...\n`);
for (const brand of BRANDS) {
try {
const logoUrl = await getLogoUrl(brand);
if (logoUrl) {
console.log(`✓ ${brand}: ${logoUrl}`);
// Download and upload to Blob
const response = await fetch(logoUrl);
const buffer = Buffer.from(await response.arrayBuffer());
await logoBlobService.upload(brand, buffer);
} else {
console.log(`✗ ${brand}: Not found`);
}
} catch (error) {
console.error(`✗ ${brand}: Error -`, error);
}
}
console.log("\n✅ Logo fetch complete!");
}
fetchAllLogos()
.then(() => process.exit(0))
.catch((error) => {
console.error("Failed to fetch logos:", error);
process.exit(1);
});
Add to package.json:
{
"scripts": {
"fetch-logos": "tsx scripts/fetch-logos.ts",
"scrape-logos": "tsx scripts/scrape-logos.ts"
}
}
Run scripts:
# Fetch logos from CDN
pnpm -F @sgcarstrends/logos fetch-logos
# Scrape logos from websites
pnpm -F @sgcarstrends/logos scrape-logos
Scrape and Upload
// packages/logos/scripts/scrape-logos.ts
import { scrapeLogos, processScrapedLogos } from "../src/services/logo/scrape";
async function main() {
console.log("Scraping car logos...\n");
const logos = await scrapeLogos();
console.log(`\nFound ${logos.length} logos`);
console.log("Uploading to Vercel Blob...\n");
await processScrapedLogos(logos);
console.log("\n✅ Scraping complete!");
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error("Failed to scrape logos:", error);
process.exit(1);
});
Caching Strategy
Multi-Layer Caching
import { redis } from "@sgcarstrends/utils";
import { LRUCache } from "lru-cache";
// L1 Cache - In-memory
const memoryCache = new LRUCache<string, string>({
max: 100,
ttl: 5 * 60 * 1000, // 5 minutes
});
// L2 Cache - Redis
// L3 Cache - Vercel Blob
export async function getCachedLogoUrl(brand: string): Promise<string | null> {
const normalizedBrand = normalizeBrandName(brand);
// Check L1 (memory)
const memCached = memoryCache.get(normalizedBrand);
if (memCached) {
console.log("L1 cache hit");
return memCached;
}
// Check L2 (Redis)
const redisCached = await redis.get<string>(`logo:${normalizedBrand}`);
if (redisCached) {
console.log("L2 cache hit");
memoryCache.set(normalizedBrand, redisCached);
return redisCached;
}
// Check L3 (Blob)
const blobUrl = await logoBlobService.getUrl(normalizedBrand);
if (blobUrl) {
console.log("L3 cache hit");
// Populate L1 and L2
memoryCache.set(normalizedBrand, blobUrl);
await redis.set(`logo:${normalizedBrand}`, blobUrl, { ex: 7 * 24 * 60 * 60 });
return blobUrl;
}
console.log("Cache miss");
return null;
}
Fallback Strategy
export async function getLogoWithFallback(brand: string): Promise<string> {
const normalizedBrand = normalizeBrandName(brand);
// Try 1: Get from cache/blob
let logoUrl = await getCachedLogoUrl(normalizedBrand);
if (logoUrl) {
return logoUrl;
}
// Try 2: Fetch from CDN
logoUrl = await getLogoUrl(normalizedBrand);
if (logoUrl) {
return logoUrl;
}
// Try 3: Scrape from web
const scrapedLogos = await scrapeLogos();
const scraped = scrapedLogos.find(l => l.brand === normalizedBrand);
if (scraped) {
return scraped.url;
}
// Fallback: Return placeholder
return `/images/logo-placeholder.png`;
}
Testing
// packages/logos/src/utils/__tests__/normalize.test.ts
import { describe, it, expect } from "vitest";
import { normalizeBrandName, resolveBrandAlias } from "../normalize";
describe("Brand Name Normalization", () => {
it("normalizes brand names correctly", () => {
expect(normalizeBrandName("Mercedes-Benz")).toBe("mercedes-benz");
expect(normalizeBrandName("BMW")).toBe("bmw");
expect(normalizeBrandName("Land Rover")).toBe("land-rover");
expect(normalizeBrandName("Alfa Romeo")).toBe("alfa-romeo");
});
it("handles special characters", () => {
expect(normalizeBrandName("Audi (A4)")).toBe("audi-a4");
expect(normalizeBrandName("Range Rover")).toBe("range-rover");
});
it("resolves aliases", () => {
expect(resolveBrandAlias("VW")).toBe("volkswagen");
expect(resolveBrandAlias("Merc")).toBe("mercedes-benz");
expect(resolveBrandAlias("BMW")).toBe("bmw");
});
});
Run tests:
pnpm -F @sgcarstrends/logos test
Environment Variables
# Vercel Blob
BLOB_READ_WRITE_TOKEN=vercel_blob_token_here
Performance Optimization
Batch Upload
export async function batchUploadLogos(
logos: Array<{ brand: string; file: Buffer }>
) {
const results = await Promise.allSettled(
logos.map(({ brand, file }) =>
logoBlobService.upload(brand, file)
)
);
const succeeded = results.filter(r => r.status === "fulfilled").length;
const failed = results.filter(r => r.status === "rejected").length;
console.log(`Uploaded: ${succeeded}, Failed: ${failed}`);
return results;
}
Pre-warming Cache
export async function prewarmLogoCache() {
const commonBrands = ["Toyota", "Honda", "BMW", "Mercedes-Benz"];
for (const brand of commonBrands) {
await getCachedLogoUrl(brand);
}
console.log("Logo cache prewarmed");
}
References
- Vercel Blob: Use Context7 for latest docs
- Related files:
packages/logos/src/- Logo package sourcepackages/logos/scripts/- Utility scripts- Root CLAUDE.md - Project documentation
Best Practices
- Normalize Names: Always normalize brand names before lookup
- Cache Aggressively: Use multi-layer caching
- Fallbacks: Provide placeholder images for missing logos
- Lazy Loading: Only fetch logos when needed
- Batch Operations: Use batch uploads for multiple logos
- Error Handling: Handle missing logos gracefully
- Testing: Test normalization and caching logic
- Monitoring: Track cache hit rates and missing logos