/** * URL builders for our asset routes. The path basename is the friendly filename * (used as the browser tab title and "Save as…" suggestion). The query string * carries the actual lookup key. */ function safe(name: string): string { // Browser-safe: encode anything that's not alphanumeric, dash, dot, or underscore. return encodeURIComponent(name); } export function imageUrl(opts: { id: number; code: string | null; ext?: string; v?: string }): string { const ext = opts.ext ?? ".jpg"; const params = new URLSearchParams({ id: String(opts.id) }); if (opts.v) params.set("v", opts.v); if (opts.code) { const friendly = `${opts.code}${ext}`; return `/api/image/${safe(friendly)}?${params.toString()}`; } const friendly = `image-${opts.id}${ext}`; return `/api/image/${safe(friendly)}?${params.toString()}`; } export function thumbUrl(opts: { thumbPath: string; code?: string | null; id?: number | null }): string { // thumbPath is the SHA-based filename (".webp"). Friendly name uses code if available. const friendly = `${opts.code ?? (opts.id != null ? `image-${opts.id}` : "thumb")}.webp`; return `/api/thumb/${safe(friendly)}?p=${encodeURIComponent(opts.thumbPath)}`; } export function portraitUrl(opts: { path: string; slug?: string | null; slot?: "1" | "2" | "3" | "4" | "h" }): string { const ext = opts.path.match(/\.[^.]+$/)?.[0] ?? ".jpg"; const slotSuffix = opts.slot && opts.slot !== "1" ? `-${opts.slot === "h" ? "l" : `p${opts.slot}`}` : ""; const friendly = `${opts.slug ?? "portrait"}${slotSuffix}${ext}`; return `/api/portrait/${safe(friendly)}?p=${encodeURIComponent(opts.path)}`; } export function categoryCoverUrl(filename: string): string { return `/api/category-cover-file/${safe(filename)}`; } export function collectionCoverUrl(filename: string): string { return `/api/collection-cover-file/${safe(filename)}`; }