From 1d4cfe773cb080ac5f3f28197af0b2883cb148f3 Mon Sep 17 00:00:00 2001 From: gbanyan Date: Tue, 17 Mar 2026 22:14:12 +0800 Subject: [PATCH] fix: load CJK fonts in OG image route and prefer feature_image for Twitter cards OG images rendered without fonts caused blank/tofu text for Chinese titles, breaking Twitter card previews. Now loads Noto Sans TC with in-memory cache. Blog posts also prefer feature_image when available for social card images. Co-Authored-By: Claude Opus 4.6 (1M context) --- app/api/og/route.tsx | 24 ++++++++++++++++++++++++ app/blog/[slug]/page.tsx | 9 +++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/app/api/og/route.tsx b/app/api/og/route.tsx index c0ffd44..8732981 100644 --- a/app/api/og/route.tsx +++ b/app/api/og/route.tsx @@ -1,6 +1,17 @@ import { ImageResponse } from '@vercel/og'; import { NextRequest } from 'next/server'; +const fontCache = new Map(); + +async function loadFont(url: string): Promise { + const cached = fontCache.get(url); + if (cached) return cached; + const res = await fetch(url); + const data = await res.arrayBuffer(); + fontCache.set(url, data); + return data; +} + export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url); @@ -10,6 +21,14 @@ export async function GET(request: NextRequest) { const description = searchParams.get('description') || ''; const tags = searchParams.get('tags')?.split(',').slice(0, 3) || []; + // Load CJK font for Chinese text rendering + const fontData = await loadFont( + 'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-tc@latest/chinese-traditional-400-normal.woff' + ); + const fontBoldData = await loadFont( + 'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-tc@latest/chinese-traditional-700-normal.woff' + ); + const imageResponse = new ImageResponse( (
{ ogImageUrl.searchParams.set('tags', post.tags.slice(0, 3).join(',')); } + // Prefer post's feature_image for social cards; fall back to dynamic OG + const imageUrl = post.feature_image + ? `${siteConfig.url}${post.feature_image.replace('../assets', '/assets')}` + : ogImageUrl.toString(); + return { title: post.title, description: post.description || post.title, @@ -61,7 +66,7 @@ export async function generateMetadata({ params }: Props): Promise { tags: post.tags, images: [ { - url: ogImageUrl.toString(), + url: imageUrl, width: 1200, height: 630, alt: post.title, @@ -72,7 +77,7 @@ export async function generateMetadata({ params }: Props): Promise { card: 'summary_large_image', title: post.title, description: post.description || post.title, - images: [ogImageUrl.toString()], + images: [imageUrl], }, }; }