diff --git a/backend/src/services/x/index.js b/backend/src/services/x/index.js index c50b637..32c65cc 100644 --- a/backend/src/services/x/index.js +++ b/backend/src/services/x/index.js @@ -59,22 +59,27 @@ async function xBotPlugin(fastify, opts) { */ async function resolveCard(tweet) { const nitter = tweet.card && (tweet.card.title || tweet.card.image) ? tweet.card : null; - // Nitter 카드에 이미지가 있으면 그대로 사용 - if (nitter && nitter.image) return nitter; - // 이미지가 없으면 OG로 보강 (YouTube 등 Nitter가 카드 이미지를 안 주는 경우 복구) - const url = (nitter && nitter.url) || extractFirstUrl(tweet.text); - if (url) { - try { - const og = await fetchOgCard(url); - if (og) { - // Nitter 카드가 있으면 제목/설명은 유지하되 OG 이미지로 보강 - return nitter - ? { ...nitter, image: og.image || null, description: nitter.description || og.description } - : og; + // pbs.twimg.com/card_img 이미지는 시간이 지나면 만료(404)되므로 안정적인 원본 OG 이미지로 대체. + // 이미지가 없거나 만료성 트위터 카드 이미지면 OG 보강 시도. + const isEphemeral = nitter?.image && /pbs\.twimg\.com\/card_img\//.test(nitter.image); + const needsImage = !nitter || !nitter.image || isEphemeral; + + if (needsImage) { + const url = (nitter && nitter.url) || extractFirstUrl(tweet.text); + if (url) { + try { + const og = await fetchOgCard(url); + if (og && og.image) { + return nitter + ? { ...nitter, image: og.image, description: nitter.description || og.description } + : og; + } + // OG 이미지가 없고 Nitter 카드도 없으면 OG 카드라도 반환 + if (og && !nitter) return og; + } catch { + // noop } - } catch { - // noop } } return nitter;