@@ -21,7 +21,7 @@ import { apiJsonWithLiveIndex, domainsJsonWithLiveIndex, upsertLiveIndex } from
2121import { setChat , setWebBackend , discoverWithProgress , preserveSlugs } from "./operations.ts" ;
2222import { contextWeb , naiveWeb } from "../src/lib/contextdev.ts" ;
2323import { DOMAIN_ALIASES , canonicalDomain } from "../src/lib/domain-aliases.ts" ;
24- import { isJunkDomain , registrableDomain } from "../src/lib/favicon.ts" ;
24+ import { isJunkDomain , logoHost , registrableDomain } from "../src/lib/favicon.ts" ;
2525import { isSdkNotCli } from "../src/lib/surface-classify.ts" ;
2626import { renderOgPng , type OgFonts , type OgImageData } from "../src/lib/og.tsx" ;
2727import type { Surface } from "../src/lib/surface-view.ts" ;
@@ -452,6 +452,29 @@ async function handleRequest(
452452 return healthz ( env ) ;
453453 }
454454
455+ // A neutral mark for a domain with no logo on file: its first letter on a
456+ // tinted square, deterministic per domain so the same service always looks
457+ // the same. Served with the same cache headers as a real logo.
458+ const letterLogo = ( domain : string , size : number ) : Response => {
459+ const letter = ( domain . replace ( / ^ w w w \. / , "" ) [ 0 ] ?? "?" ) . toUpperCase ( ) ;
460+ // A stable hue per domain — recognisable at a glance, never garish.
461+ let hash = 0 ;
462+ for ( const char of domain ) hash = ( hash * 31 + char . charCodeAt ( 0 ) ) % 360 ;
463+ const svg =
464+ `<svg xmlns="http://www.w3.org/2000/svg" width="${ size } " height="${ size } " viewBox="0 0 64 64">` +
465+ `<rect width="64" height="64" rx="12" fill="hsl(${ hash } 12% 88%)"/>` +
466+ `<text x="32" y="33" fill="hsl(${ hash } 14% 34%)" font-family="ui-sans-serif,system-ui,sans-serif" ` +
467+ `font-size="34" font-weight="600" text-anchor="middle" dominant-baseline="central">${ letter } </text>` +
468+ `</svg>` ;
469+ return new Response ( svg , {
470+ headers : {
471+ "content-type" : "image/svg+xml; charset=utf-8" ,
472+ "access-control-allow-origin" : "*" ,
473+ "cache-control" : "public, max-age=86400" ,
474+ } ,
475+ } ) ;
476+ } ;
477+
455478 // Logo proxy — the single logo source for executor clients (and anything
456479 // else): /logo/{domain}?theme=light|dark&sz=64. Proxies context.dev Logo
457480 // Link, falling back to Google's favicon service when the client id is
@@ -461,8 +484,11 @@ async function handleRequest(
461484 // upstream's own 24h Cache-Control — no KV/R2.
462485 const logoMatch = / ^ \/ l o g o \/ ( [ ^ / ] + ) \/ ? $ / . exec ( url . pathname ) ;
463486 if ( logoMatch ) {
464- const domain = registrableDomain ( decodeURIComponent ( logoMatch [ 1 ] ) . trim ( ) . toLowerCase ( ) ) ;
465- if ( ! domain ) return json ( { error : "not a public registrable domain" } , 400 ) ;
487+ const domain = logoHost ( decodeURIComponent ( logoMatch [ 1 ] ) . trim ( ) . toLowerCase ( ) ) ;
488+ // Only a host that could never carry a logo is refused. Everything else
489+ // gets an image — see letterLogo below for why an error is the wrong
490+ // answer here.
491+ if ( ! domain ) return json ( { error : "not a usable logo host" } , 400 ) ;
466492 const theme = url . searchParams . get ( "theme" ) ;
467493 const size = Math . min ( Math . max ( Number ( url . searchParams . get ( "sz" ) ) || 64 , 16 ) , 256 ) ;
468494
@@ -498,7 +524,16 @@ async function handleRequest(
498524 `https://www.google.com/s2/favicons?domain=${ domain } &sz=${ size } ` ,
499525 ) . catch ( ( ) => null ) ;
500526 }
501- if ( ! upstream || ! isImage ( upstream ) ) return json ( { error : "no logo found" } , 404 ) ;
527+ // A LOGO ENDPOINT MUST RETURN A LOGO. Clients put this URL in an <img>;
528+ // a JSON 404 renders as a broken image, and since the failure is silent
529+ // to onError-less callers it reads as "the icon system is broken" rather
530+ // than "this brand has no mark on file". A letter placeholder is a
531+ // truthful answer to "show me something for this domain".
532+ if ( ! upstream || ! isImage ( upstream ) ) {
533+ const placeholder = letterLogo ( domain , size ) ;
534+ ctx . waitUntil ( cache . put ( cacheKey , placeholder . clone ( ) ) ) ;
535+ return placeholder ;
536+ }
502537
503538 const res = new Response ( upstream . body , {
504539 headers : {
0 commit comments