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) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,17 @@
|
||||
import { ImageResponse } from '@vercel/og';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
const fontCache = new Map<string, ArrayBuffer>();
|
||||
|
||||
async function loadFont(url: string): Promise<ArrayBuffer> {
|
||||
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(
|
||||
(
|
||||
<div
|
||||
@@ -20,6 +39,7 @@ export async function GET(request: NextRequest) {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'space-between',
|
||||
fontFamily: '"Noto Sans TC", sans-serif',
|
||||
backgroundColor: '#0f172a',
|
||||
backgroundImage: 'radial-gradient(circle at 25px 25px, #1e293b 2%, transparent 0%), radial-gradient(circle at 75px 75px, #1e293b 2%, transparent 0%)',
|
||||
backgroundSize: '100px 100px',
|
||||
@@ -155,6 +175,10 @@ export async function GET(request: NextRequest) {
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{ name: 'Noto Sans TC', data: fontData, weight: 400 as const, style: 'normal' as const },
|
||||
{ name: 'Noto Sans TC', data: fontBoldData, weight: 700 as const, style: 'normal' as const },
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -40,6 +40,11 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
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<Metadata> {
|
||||
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<Metadata> {
|
||||
card: 'summary_large_image',
|
||||
title: post.title,
|
||||
description: post.description || post.title,
|
||||
images: [ogImageUrl.toString()],
|
||||
images: [imageUrl],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user