diff --git a/src/lib/server/search/searxng.ts b/src/lib/server/search/searxng.ts index dbea01e..e88dfdb 100644 --- a/src/lib/server/search/searxng.ts +++ b/src/lib/server/search/searxng.ts @@ -198,9 +198,13 @@ async function enrichThumbnail(url: string): Promise { return image; } -async function enrichMissingThumbnails(hits: WebHit[]): Promise { - const queue = hits.filter((h) => !h.thumbnail); - if (queue.length === 0) return; +async function enrichAllThumbnails(hits: WebHit[]): Promise { + // Always fetch the page image even when SearXNG gave us a thumbnail — + // the search engine's thumbnail is typically 150-200px, while og:image + // / JSON-LD image on the page is the full-resolution recipe photo. + // The 30-min URL cache makes repeat searches instant. + if (hits.length === 0) return; + const queue = [...hits]; const LIMIT = 6; const workers = Array.from({ length: Math.min(LIMIT, queue.length) }, async () => { while (queue.length > 0) { @@ -268,7 +272,7 @@ export async function searchWeb( if (hits.length >= limit) break; } if (opts.enrichThumbnails !== false) { - await enrichMissingThumbnails(hits); + await enrichAllThumbnails(hits); } return hits; } diff --git a/tests/integration/searxng.test.ts b/tests/integration/searxng.test.ts index 0b778fc..ccbc069 100644 --- a/tests/integration/searxng.test.ts +++ b/tests/integration/searxng.test.ts @@ -140,19 +140,48 @@ describe('searchWeb', () => { } }); - it('leaves existing thumbnails untouched (no enrichment fetch)', async () => { - const db = openInMemoryForTest(); - addDomain(db, 'chefkoch.de'); - respondWith([ - { - url: 'https://www.chefkoch.de/rezepte/1/x.html', - title: 'X', - thumbnail: 'https://cdn.chefkoch/x.jpg' - } - ]); - // enrichment enabled, but thumbnail is set → no fetch expected, no hang - const hits = await searchWeb(db, 'x', { searxngUrl: baseUrl }); - expect(hits[0].thumbnail).toBe('https://cdn.chefkoch/x.jpg'); + it('upgrades low-res SearXNG thumbnail with HQ og:image from page', async () => { + const pageServer = createServer((_req, res) => { + res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); + res.end( + '' + ); + }); + await new Promise((r) => pageServer.listen(0, '127.0.0.1', r)); + const addr = pageServer.address() as AddressInfo; + const pageUrl = `http://127.0.0.1:${addr.port}/dish`; + try { + const db = openInMemoryForTest(); + addDomain(db, '127.0.0.1'); + respondWith([ + { url: pageUrl, title: 'Dish', thumbnail: 'https://searxng-cdn/small-thumb.jpg' } + ]); + const hits = await searchWeb(db, 'dish', { searxngUrl: baseUrl }); + expect(hits[0].thumbnail).toBe('https://cdn.example/hq.jpg'); + } finally { + await new Promise((r) => pageServer.close(() => r())); + } + }); + + it('keeps SearXNG thumbnail when page has no image', async () => { + const pageServer = createServer((_req, res) => { + res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); + res.end('no images here'); + }); + await new Promise((r) => pageServer.listen(0, '127.0.0.1', r)); + const addr = pageServer.address() as AddressInfo; + const pageUrl = `http://127.0.0.1:${addr.port}/noimg`; + try { + const db = openInMemoryForTest(); + addDomain(db, '127.0.0.1'); + respondWith([ + { url: pageUrl, title: 'X', thumbnail: 'https://searxng-cdn/fallback.jpg' } + ]); + const hits = await searchWeb(db, 'x', { searxngUrl: baseUrl }); + expect(hits[0].thumbnail).toBe('https://searxng-cdn/fallback.jpg'); + } finally { + await new Promise((r) => pageServer.close(() => r())); + } }); it('filters out forum/magazine/listing URLs', async () => {