Skip to content

Commit 19de795

Browse files
authored
Fix Open Design example logo loading (#5765)
* Fix Open Design example logo loading Generated-By: looper 0.0.0-dev (runner=worker, agent=codex) * Stabilize visual home cold start checks Generated-By: looper 0.0.0-dev (runner=worker, agent=codex)
1 parent cb5fd3b commit 19de795

4 files changed

Lines changed: 65 additions & 15 deletions

File tree

apps/web/src/components/HomeHero.tsx

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2257,11 +2257,20 @@ function PluginPromptPresets({
22572257
);
22582258
}
22592259

2260+
const FIRST_PARTY_WEB_CLONE_SITE_ICONS: Record<string, string> = {
2261+
'open-design.ai': '/logo.svg',
2262+
};
2263+
2264+
function webCloneFaviconUrl(domain: string): string {
2265+
return `https://www.google.com/s2/favicons?sz=128&domain=${encodeURIComponent(domain)}`;
2266+
}
2267+
22602268
// A Website-clone text example ("Website URL to clone: https://open-design.ai") —
2261-
// pull the site out so the card can show the site's own favicon + bare domain
2262-
// instead of the raw prompt line. Returns null for non-URL examples so the
2263-
// generic text card renders unchanged.
2264-
function webCloneExampleSite(example: string): { domain: string; faviconUrl: string } | null {
2269+
// pull the site out so the card can show the site's own mark + bare domain
2270+
// instead of the raw prompt line. First-party bundled examples use local assets
2271+
// so the first screen is stable without waiting on a remote favicon service.
2272+
// Returns null for non-URL examples so the generic text card renders unchanged.
2273+
function webCloneExampleSite(example: string): { domain: string; iconUrl: string; fallbackIconUrl?: string } | null {
22652274
const match = example.match(/https?:\/\/[^\s"'<>]+/i);
22662275
if (!match) return null;
22672276
let hostname: string;
@@ -2271,12 +2280,11 @@ function webCloneExampleSite(example: string): { domain: string; faviconUrl: str
22712280
return null;
22722281
}
22732282
if (!hostname || !hostname.includes('.')) return null;
2274-
// The site's own favicon, resolved at render time via Google's public service
2275-
// — no third-party brand assets are bundled into the repo, and a broken/blocked
2276-
// fetch falls back to a lettered tile.
2283+
const firstPartyIcon = FIRST_PARTY_WEB_CLONE_SITE_ICONS[hostname];
22772284
return {
22782285
domain: hostname,
2279-
faviconUrl: `https://www.google.com/s2/favicons?sz=128&domain=${encodeURIComponent(hostname)}`,
2286+
iconUrl: firstPartyIcon ?? webCloneFaviconUrl(hostname),
2287+
...(firstPartyIcon ? { fallbackIconUrl: webCloneFaviconUrl(hostname) } : {}),
22802288
};
22812289
}
22822290

@@ -2289,10 +2297,16 @@ function WebClonePromptExampleCard({
22892297
pulse: boolean;
22902298
onPick: (example: string) => void;
22912299
}) {
2292-
const [iconFailed, setIconFailed] = useState(false);
2300+
const [iconStage, setIconStage] = useState<'primary' | 'fallback' | 'failed'>('primary');
22932301
const site = webCloneExampleSite(example);
22942302
const domain = site?.domain ?? example;
22952303
const monogram = (domain.replace(/[^a-z0-9]/i, '')[0] ?? '?').toUpperCase();
2304+
let iconUrl: string | null = null;
2305+
if (site && iconStage === 'primary') {
2306+
iconUrl = site.iconUrl;
2307+
} else if (site && iconStage === 'fallback') {
2308+
iconUrl = site.fallbackIconUrl ?? null;
2309+
}
22962310
return (
22972311
<button
22982312
type="button"
@@ -2302,12 +2316,15 @@ function WebClonePromptExampleCard({
23022316
title={domain}
23032317
>
23042318
<span className="home-hero__site-badge" aria-hidden>
2305-
{site && !iconFailed ? (
2319+
{site && iconUrl ? (
23062320
<img
2307-
src={site.faviconUrl}
2321+
src={iconUrl}
23082322
alt=""
2309-
loading="lazy"
2310-
onError={() => setIconFailed(true)}
2323+
loading="eager"
2324+
fetchPriority="high"
2325+
onError={() => {
2326+
setIconStage((stage) => (stage === 'primary' && site.fallbackIconUrl ? 'fallback' : 'failed'));
2327+
}}
23112328
/>
23122329
) : (
23132330
<span className="home-hero__site-monogram">{monogram}</span>

apps/web/tests/components/HomeView.web-clone-tracking.test.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,39 @@ describe('web-clone example-card tracking', () => {
141141
).toBe(true);
142142
});
143143

144+
it('renders the contracted Open Design site card with a local eager logo', async () => {
145+
writeHomeGuideStage('done');
146+
stubPlugins();
147+
renderHome();
148+
149+
fireEvent.click(await screen.findByTestId('home-hero-rail-web-clone'));
150+
const siteCard = (await screen.findAllByTestId('home-hero-prompt-example'))[0]!;
151+
const logo = siteCard.querySelector<HTMLImageElement>('.home-hero__site-badge img');
152+
expect(logo?.getAttribute('src')).toBe('/logo.svg');
153+
expect(logo?.getAttribute('loading')).toBe('eager');
154+
expect(logo?.getAttribute('fetchpriority')).toBe('high');
155+
});
156+
157+
it('falls back when the local Open Design site card logo cannot load', async () => {
158+
writeHomeGuideStage('done');
159+
stubPlugins();
160+
renderHome();
161+
162+
fireEvent.click(await screen.findByTestId('home-hero-rail-web-clone'));
163+
const siteCard = (await screen.findAllByTestId('home-hero-prompt-example'))[0]!;
164+
const localLogo = siteCard.querySelector<HTMLImageElement>('.home-hero__site-badge img');
165+
expect(localLogo?.getAttribute('src')).toBe('/logo.svg');
166+
167+
fireEvent.error(localLogo!);
168+
const remoteFallback = siteCard.querySelector<HTMLImageElement>('.home-hero__site-badge img');
169+
expect(remoteFallback?.getAttribute('src')).toBe(
170+
'https://www.google.com/s2/favicons?sz=128&domain=open-design.ai',
171+
);
172+
173+
fireEvent.error(remoteFallback!);
174+
expect(siteCard.querySelector('.home-hero__site-monogram')?.textContent).toBe('O');
175+
});
176+
144177
it('fires element=example_prompt with chip_id=web-clone when a text example is picked', async () => {
145178
writeHomeGuideStage('done');
146179
stubPlugins();

e2e/lib/playwright/visual.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ export async function mockSignedInVelaAccount(
575575
}
576576

577577
export async function waitForVisualReady(page: Page): Promise<void> {
578-
await page.getByText('Loading Open Design…').waitFor({ state: 'hidden', timeout: T.long });
578+
await page.getByText('Loading Open Design…').waitFor({ state: 'hidden', timeout: T.xlong });
579579
await expect(page.getByTestId('home-hero')).toBeVisible({ timeout: T.medium });
580580
await expect(page.getByTestId('home-hero-input')).toBeVisible({ timeout: T.medium });
581581
await page.evaluate(async () => {

e2e/playwright.visual.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default defineConfig({
1313
testDir: './ui',
1414
testMatch: 'visual-*.test.ts',
1515
outputDir: './ui/reports/visual-test-results',
16-
timeout: Number(process.env.OD_PLAYWRIGHT_TIMEOUT) || 60_000,
16+
timeout: Number(process.env.OD_PLAYWRIGHT_TIMEOUT) || 240_000,
1717
expect: {
1818
timeout: 10_000,
1919
},

0 commit comments

Comments
 (0)