最近在調整 cake 的履歷時發現只要 hover 網址就會顯示該網站的 og image 、title、description
像這樣滑鼠移標移到網址上,就會顯示該網站的基本資訊
看起來酷酷ㄉ
就決定也來做一個!
我自己找到一個套件、AI 也有提供一個套件
但後來決定用自己找的套件
因為比較簡單一點點 hehe ◔.̮◔✧
我是使用這個套件 url-metadata
使用方式是自己寫一個 api 請求
我是使用 Next.js App Router
路徑是:/api/metadata/route.ts
import { NextResponse } from 'next/server';
import urlMetadata from 'url-metadata';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const targetUrl = searchParams.get('url'); // 傳入要抓取的網址
if (!targetUrl) {
return NextResponse.json(
{ error: 'URL parameter is required' },
{ status: 400 },
);
}
try {
const metadata = await urlMetadata(targetUrl, {
maxRedirects: 1, // 限制 HTTP 重定向的次數
timeout: 5000, // 設定請求超時時間
});
return NextResponse.json({
favico: metadata.favicons[0]?.href || '',
title: metadata.title || '',
description: metadata.description || '',
image: metadata['og:image'] || '',
});
} catch (error) {
// 處理超時或重定向過多的錯誤
return NextResponse.json(
{ error: 'Failed to fetch metadata' },
{ status: 500 },
);
}
}
有給 AI 校正過程式碼
maxRedirects 、timeout 是安全性和效能的保護措施參數,防止惡意或錯誤網站造成的攻擊或錯誤等
我需要的資料有網站的 favicons、title、description、og:image
metadata 回傳的是一大包物件,裡面有很多參數、值可以取用~
因為我有兩個以上會用到這支 API ,所以就寫了一個函式統一管理
export const getMetaData = async (url: string) => {
const response = await fetch(`/api/metadata?url=${url}`);
const metadata = await response.json();
const urlName = url.split('/').pop() || '-';
const favico =
metadata.favico !== ''
? metadata.favico.includes('https')
? metadata.favico
: metadata.favico.split('/')[1].includes(urlName)
? `${url}${metadata.favico.split(urlName)[1]}`
: `${url}${metadata.favico}`
: '';
return {
title: metadata.title,
description: metadata.description,
image: metadata.image,
favicon: favico,
};
};
因為網站 favico 放的連結方式都不太ㄧ樣
這邊寫法有因應我要放的連結網站的 favico 額外做判斷
因為是自己看的
所以這個三元判斷寫得很不易閱讀 > <
最後就是畫面的呈現~
這邊使用 Shadcn UI 的 Tooltip 元件做修改
export const MetaInfoLink = React.memo(
({ url, children }: { url: string; children: React.ReactNode }) => {
const [metadata, setMetadata] = useState<Record<string, string>>({});
const [isPending, startTransition] = useTransition();
const [imageError, setImageError] = useState(false);
const [faviconError, setFaviconError] = useState(false);
const fetchMetaData = async () => {
// 防止重複請求:如果正在載入中或已經有資料就不重複請求
if (isPending || metadata.title) return;
startTransition(async () => {
try {
const metadata = await getMetaData(url);
setMetadata(metadata);
setImageError(false); // 重置圖片錯誤狀態
setFaviconError(false); // 重置 favicon 錯誤狀態
} catch (error) {
console.error('Failed to fetch metadata:', error);
}
});
};
return (
<Tooltip>
<TooltipTrigger onMouseOver={fetchMetaData}>{children}</TooltipTrigger>
<TooltipContent className="max-w-[335px] w-[100vw] bg-white overflow-hidden p-0 shadow-xl">
{isPending || metadata?.title ? (
<>
{isPending ? (
<div className="w-full min-h-[250px] flex justify-center items-center">
<Flower2 className="w-8 h-8 text-main animate-bounce" />
</div>
) : (
<div className="w-full max-w-full overflow-hidden">
{metadata.image !== '' && !imageError ? (
<img
src={metadata.image}
alt={metadata.title}
className="w-full max-h-[250px] object-cover object-center"
onError={() => setImageError(true)}
/>
) : (
<div className="w-full min-h-[180px] flex justify-center items-center bg-gray-50">
<Flower2 className="w-8 h-8 text-main" />
</div>
)}
<div className="w-full py-2 px-4">
<div className="flex items-start gap-4 mb-2">
{metadata.favicon !== '' && !faviconError ? (
<img
src={metadata.favicon}
alt={`${metadata.title}_favico`}
className="w-8 h-8 flex-shrink-0 object-contain"
onError={() => setFaviconError(true)}
/>
) : (
<span className="text-black text-xl flex-shrink-0">
ꙩ_ꙩ
</span>
)}
<div className="flex-1 min-w-0">
<p className="text-black text-sm font-extrabold truncate">
{metadata.title}
</p>
<p className="text-xs font-bold text-sub">
{url?.split('//')[1]}
</p>
<p className="text-xs font-bold text-sub whitespace-normal break-words text-justify mt-2">
{metadata.description}
</p>
</div>
</div>
</div>
</div>
)}
</>
) : (
<div className="min-h-[180px] flex flex-col justify-center items-center">
<p className="text-2xl text-main">◔̯◔</p>
<p className="text-main font-bold mt-4">JUST CLICK IT</p>
</div>
)}
</TooltipContent>
</Tooltip>
);
},
);
使用 useTransition 的 isPending 做 loading 效果
因為有些抓到的 favico 或是 og image 的圖片 url 是錯誤的
所以有使用這兩個 state 讓我去做錯誤的顯示判斷
使用 onError 偵測 favicon 載入失敗
onError={() => setImageError(true)}
onError={() => setFaviconError(true)}
之後就會得到以下結果嚕~
完整呈現:
沒有 og image 時:
沒有 favico 時:
還做了讀取時的小小動畫
參考:Preview link details before navigation