feat : better og-image
This commit is contained in:
+166
-5
@@ -6,11 +6,15 @@ import { join } from "node:path";
|
||||
import { ImageResponse } from "next/og";
|
||||
import sharp from "sharp";
|
||||
|
||||
import { rarityGradient } from "@/lib/catalog/rarity";
|
||||
import { artifactTierCount } from "@/lib/guides/artifacts";
|
||||
|
||||
export const ogImageSize = { width: 1222, height: 560 } as const;
|
||||
export const ogImageContentType = "image/png";
|
||||
|
||||
let bannerDataPromise: Promise<string> | undefined;
|
||||
let fontDataPromise: Promise<Buffer> | undefined;
|
||||
let characterFontDataPromise: Promise<Buffer> | undefined;
|
||||
const imageDataPromises = new Map<string, Promise<string | null>>();
|
||||
|
||||
function shorten(value: string, length: number): string {
|
||||
@@ -20,6 +24,12 @@ function shorten(value: string, length: number): string {
|
||||
: normalized;
|
||||
}
|
||||
|
||||
function metric(value: string): string {
|
||||
return Number(value) === 0
|
||||
? "-%"
|
||||
: `${Number(value).toLocaleString("en-US", { maximumFractionDigits: 2 })}%`;
|
||||
}
|
||||
|
||||
function getBannerData(): Promise<string> {
|
||||
bannerDataPromise ??= readFile(
|
||||
join(process.cwd(), "public/web_banner.png"),
|
||||
@@ -33,6 +43,11 @@ function getFontData(): Promise<Buffer> {
|
||||
return fontDataPromise;
|
||||
}
|
||||
|
||||
function getCharacterFontData(): Promise<Buffer> {
|
||||
characterFontDataPromise ??= readFile(join(process.cwd(), "public/zh-cn.ttf"));
|
||||
return characterFontDataPromise;
|
||||
}
|
||||
|
||||
function getImageData(url: string): Promise<string | null> {
|
||||
const cached = imageDataPromises.get(url);
|
||||
if (cached) return cached;
|
||||
@@ -56,19 +71,57 @@ export async function generateOgImage({
|
||||
characterCard = false,
|
||||
backgroundUrl,
|
||||
characterUrl,
|
||||
recommendedWeapon,
|
||||
recommendedArtifact,
|
||||
}: {
|
||||
title: string;
|
||||
sub: string;
|
||||
characterCard?: boolean;
|
||||
backgroundUrl?: string | null;
|
||||
characterUrl?: string | null;
|
||||
recommendedWeapon?: {
|
||||
name: string;
|
||||
refinement: number;
|
||||
rarity: number | null;
|
||||
imageUrl: string;
|
||||
overallPercent: string | null;
|
||||
personalPercent: string | null;
|
||||
} | null;
|
||||
recommendedArtifact?: {
|
||||
first: {
|
||||
name: string;
|
||||
rarity: number | null;
|
||||
imageUrl: string;
|
||||
};
|
||||
second: {
|
||||
name: string;
|
||||
rarity: number | null;
|
||||
imageUrl: string;
|
||||
} | null;
|
||||
percent: string;
|
||||
} | null;
|
||||
}) {
|
||||
const [bannerSrc, kanit, characterBackgroundSrc, characterArtSrc] =
|
||||
const [
|
||||
bannerSrc,
|
||||
kanit,
|
||||
characterFont,
|
||||
characterBackgroundSrc,
|
||||
characterArtSrc,
|
||||
weaponArtSrc,
|
||||
firstArtifactArtSrc,
|
||||
secondArtifactArtSrc,
|
||||
] =
|
||||
await Promise.all([
|
||||
characterCard ? null : getBannerData(),
|
||||
getFontData(),
|
||||
characterCard ? getCharacterFontData() : null,
|
||||
characterCard && backgroundUrl ? getImageData(backgroundUrl) : null,
|
||||
characterCard && characterUrl ? getImageData(characterUrl) : null,
|
||||
recommendedWeapon ? getImageData(recommendedWeapon.imageUrl) : null,
|
||||
recommendedArtifact ? getImageData(recommendedArtifact.first.imageUrl) : null,
|
||||
recommendedArtifact?.second
|
||||
? getImageData(recommendedArtifact.second.imageUrl)
|
||||
: null,
|
||||
]);
|
||||
const backgroundSrc = characterCard ? characterBackgroundSrc : bannerSrc;
|
||||
|
||||
@@ -80,6 +133,7 @@ export async function generateOgImage({
|
||||
height: "560px",
|
||||
fontFamily: "Kanit",
|
||||
color: "#fff",
|
||||
textShadow: "0 2px 8px rgba(0, 0, 0, 0.8)",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
@@ -123,7 +177,7 @@ export async function generateOgImage({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
right: characterCard ? "560px" : "450px",
|
||||
bottom: "100px",
|
||||
bottom: recommendedWeapon || recommendedArtifact ? "28px" : "100px",
|
||||
}}
|
||||
>
|
||||
{characterCard ? (
|
||||
@@ -131,7 +185,7 @@ export async function generateOgImage({
|
||||
เกนชินไม่ใช่เกมมือถือไกด์
|
||||
</div>
|
||||
) : null}
|
||||
<div style={{ display: "flex", fontSize: 64, marginTop: characterCard ? "8px" : 0 }}>
|
||||
<div style={{ display: "flex", fontFamily: characterCard ? "Genshin" : "Kanit", fontSize: 64, marginTop: characterCard ? "8px" : 0 }}>
|
||||
{shorten(title, 24)}
|
||||
</div>
|
||||
<div
|
||||
@@ -139,16 +193,123 @@ export async function generateOgImage({
|
||||
display: "flex",
|
||||
fontSize: 24,
|
||||
marginTop: characterCard ? "8px" : "-20px",
|
||||
color: "#aaa",
|
||||
color: "#fff",
|
||||
}}
|
||||
>
|
||||
{shorten(sub, 92)}
|
||||
</div>
|
||||
{recommendedWeapon ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
width: "500px",
|
||||
height: "72px",
|
||||
marginTop: "14px",
|
||||
overflow: "hidden",
|
||||
borderRadius: "12px",
|
||||
background: "#fff",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flex: recommendedWeapon.overallPercent || recommendedWeapon.personalPercent ? 3 : 1,
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
overflow: "hidden",
|
||||
borderBottomRightRadius: "48px",
|
||||
background: rarityGradient(recommendedWeapon.rarity),
|
||||
color: "#fff",
|
||||
padding: "6px 28px 6px 10px",
|
||||
}}
|
||||
>
|
||||
{weaponArtSrc ? (
|
||||
<img
|
||||
src={weaponArtSrc}
|
||||
alt=""
|
||||
width={60}
|
||||
height={60}
|
||||
style={{ width: "60px", height: "60px", objectFit: "contain" }}
|
||||
/>
|
||||
) : null}
|
||||
<div style={{ display: "flex", fontSize: 20 }}>
|
||||
{shorten(recommendedWeapon.name, 19)} R{recommendedWeapon.refinement}
|
||||
</div>
|
||||
</div>
|
||||
{recommendedWeapon.overallPercent || recommendedWeapon.personalPercent ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "space-around",
|
||||
color: "#17171c",
|
||||
padding: "6px",
|
||||
}}
|
||||
>
|
||||
{recommendedWeapon.overallPercent ? (
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
|
||||
<div style={{ display: "flex", fontSize: 19 }}>{metric(recommendedWeapon.overallPercent)}</div>
|
||||
<div style={{ display: "flex", fontSize: 9 }}>ทั้งทีม</div>
|
||||
</div>
|
||||
) : null}
|
||||
{recommendedWeapon.personalPercent ? (
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
|
||||
<div style={{ display: "flex", fontSize: 19 }}>{metric(recommendedWeapon.personalPercent)}</div>
|
||||
<div style={{ display: "flex", fontSize: 9 }}>ส่วนตัว</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{recommendedArtifact ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
width: "500px",
|
||||
height: "72px",
|
||||
marginTop: recommendedWeapon ? "10px" : "14px",
|
||||
alignItems: "center",
|
||||
gap: "12px",
|
||||
overflow: "hidden",
|
||||
border: "1px solid #ffffff1f",
|
||||
borderRadius: "12px",
|
||||
background: "#34343d",
|
||||
padding: "6px 12px",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", width: recommendedArtifact.second ? "94px" : "60px" }}>
|
||||
{firstArtifactArtSrc ? (
|
||||
<div style={{ display: "flex", width: "60px", height: "60px", overflow: "hidden", border: "1px solid #ffffff2a", borderRadius: "10px", background: rarityGradient(recommendedArtifact.first.rarity) }}>
|
||||
<img src={firstArtifactArtSrc} alt="" width={60} height={60} style={{ objectFit: "contain" }} />
|
||||
</div>
|
||||
) : null}
|
||||
{recommendedArtifact.second && secondArtifactArtSrc ? (
|
||||
<div style={{ display: "flex", width: "60px", height: "60px", overflow: "hidden", marginLeft: "-24px", border: "1px solid #ffffff2a", borderRadius: "10px", background: rarityGradient(recommendedArtifact.second.rarity) }}>
|
||||
<img src={secondArtifactArtSrc} alt="" width={60} height={60} style={{ objectFit: "contain" }} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div style={{ display: "flex", flex: 1, fontSize: 18 }}>
|
||||
{artifactTierCount(recommendedArtifact.second?.name ?? null)} {shorten(recommendedArtifact.first.name, recommendedArtifact.second ? 12 : 21)}
|
||||
{recommendedArtifact.second ? ` + ${shorten(recommendedArtifact.second.name, 12)}` : ""}
|
||||
</div>
|
||||
<div style={{ display: "flex", color: "#f0b72f", fontSize: 22 }}>
|
||||
{metric(recommendedArtifact.percent)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>,
|
||||
{
|
||||
...ogImageSize,
|
||||
fonts: [{ name: "Kanit", data: kanit, style: "normal", weight: 600 }],
|
||||
fonts: [
|
||||
{ name: "Kanit", data: kanit, style: "normal", weight: 600 },
|
||||
...(characterFont
|
||||
? [{ name: "Genshin", data: characterFont, style: "normal" as const, weight: 400 as const }]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user