feat : use cover image guide instead of shit image generater
CI / Verify (push) Successful in 1m7s
CI / Build immutable images and deploy (push) Successful in 2m13s

This commit is contained in:
2026-09-11 17:28:38 +07:00 Unverified
parent ecbb8dda55
commit 1164715d64
5 changed files with 14 additions and 232 deletions
@@ -1,28 +0,0 @@
import { connection } from "next/server";
import { getCachedPublicGuide } from "@/lib/cache/public";
import {
generateOgImage,
ogImageContentType,
ogImageSize,
} from "@/lib/og-image";
export const alt = "Buzz Guide character guide";
export const size = ogImageSize;
export const contentType = ogImageContentType;
export default async function Image({
params,
}: {
params: Promise<{ character: string }>;
}) {
await connection();
const guide = await getCachedPublicGuide((await params).character);
if (!guide) return new Response("Not found", { status: 404 });
return generateOgImage({
title: guide.name,
sub: guide.overview,
characterCard: true,
backgroundUrl: guide.character.nameCardImageUrl,
characterUrl: guide.character.splashImageUrl ?? guide.character.imageUrl,
});
}
+8 -1
View File
@@ -11,7 +11,14 @@ export const instant = false;
export async function generateMetadata({ params }: { params: Params }): Promise<Metadata> { export async function generateMetadata({ params }: { params: Params }): Promise<Metadata> {
const { character } = await params; const { character } = await params;
const guide = await getCachedPublicGuide(character); const guide = await getCachedPublicGuide(character);
return guide ? { title: `${guide.name}`, description: guide.overview } : { title: "ไม่พบไกด์" }; return guide
? {
title: `${guide.name}`,
description: guide.overview,
openGraph: { images: [guide.coverUrl] },
twitter: { images: [guide.coverUrl] },
}
: { title: "ไม่พบไกด์" };
} }
export default async function GuideSectionPage({ params }: { params: Params }) { export default async function GuideSectionPage({ params }: { params: Params }) {
-28
View File
@@ -1,28 +0,0 @@
import { connection } from "next/server";
import { getCachedPublicGuide } from "@/lib/cache/public";
import {
generateOgImage,
ogImageContentType,
ogImageSize,
} from "@/lib/og-image";
export const alt = "Buzz Guide character guide";
export const size = ogImageSize;
export const contentType = ogImageContentType;
export default async function Image({
params,
}: {
params: Promise<{ character: string }>;
}) {
await connection();
const guide = await getCachedPublicGuide((await params).character);
if (!guide) return new Response("Not found", { status: 404 });
return generateOgImage({
title: guide.name,
sub: guide.overview,
characterCard: true,
backgroundUrl: guide.character.nameCardImageUrl,
characterUrl: guide.character.splashImageUrl ?? guide.character.imageUrl,
});
}
+6 -1
View File
@@ -15,7 +15,12 @@ export async function generateMetadata({
}): Promise<Metadata> { }): Promise<Metadata> {
const guide = await getCachedPublicGuide((await params).character); const guide = await getCachedPublicGuide((await params).character);
return guide return guide
? { title: `${guide.name}`, description: guide.overview } ? {
title: `${guide.name}`,
description: guide.overview,
openGraph: { images: [guide.coverUrl] },
twitter: { images: [guide.coverUrl] },
}
: { title: "ไม่พบไกด์" }; : { title: "ไม่พบไกด์" };
} }
-174
View File
@@ -1,174 +0,0 @@
/* eslint-disable @next/next/no-img-element */
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { ImageResponse } from "next/og";
import sharp from "sharp";
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 {
const normalized = value.replace(/\s+/gu, " ").trim();
return normalized.length > length
? `${normalized.slice(0, length - 1).trimEnd()}…`
: normalized;
}
function getBannerData(): Promise<string> {
bannerDataPromise ??= readFile(
join(process.cwd(), "public/web_banner.png"),
"base64",
).then((data) => `data:image/png;base64,${data}`);
return bannerDataPromise;
}
function getFontData(): Promise<Buffer> {
fontDataPromise ??= readFile(join(process.cwd(), "public/Kanit-SemiBold.ttf"));
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;
const promise = fetch(url)
.then(async (response) => {
if (!response.ok) return null;
const png = await sharp(Buffer.from(await response.arrayBuffer()))
.png()
.toBuffer();
return `data:image/png;base64,${png.toString("base64")}`;
})
.catch(() => null);
imageDataPromises.set(url, promise);
return promise;
}
export async function generateOgImage({
title,
sub,
characterCard = false,
backgroundUrl,
characterUrl,
}: {
title: string;
sub: string;
characterCard?: boolean;
backgroundUrl?: string | null;
characterUrl?: string | null;
}) {
const [
bannerSrc,
kanit,
characterFont,
characterBackgroundSrc,
characterArtSrc,
] = await Promise.all([
characterCard ? null : getBannerData(),
getFontData(),
characterCard ? getCharacterFontData() : null,
characterCard && backgroundUrl ? getImageData(backgroundUrl) : null,
characterCard && characterUrl ? getImageData(characterUrl) : null,
]);
const backgroundSrc = characterCard ? characterBackgroundSrc : bannerSrc;
return new ImageResponse(
<div
style={{
display: "flex",
width: "1222px",
height: "560px",
fontFamily: "Kanit",
color: "#fff",
textShadow: "0 2px 8px rgba(0, 0, 0, 0.8)",
position: "relative",
overflow: "hidden",
}}
>
{backgroundSrc ? (
<img src={backgroundSrc} alt="" width={1222} height={560} style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover" }} />
) : (
<div style={{ position: "absolute", inset: 0, background: "#111827" }} />
)}
{characterCard && characterArtSrc ? (
<img
src={characterArtSrc}
alt=""
width={480}
height={360}
style={{
position: "absolute",
top: 0,
right: 0,
width: "58%",
height: "100%",
objectFit: "cover",
objectPosition: "center",
maskImage: "linear-gradient(90deg, transparent 0%, black 28%, black 100%)",
}}
/>
) : null}
{characterCard ? (
<div
style={{
position: "absolute",
inset: 0,
background: "linear-gradient(90deg, rgba(3, 8, 18, 0.88) 0%, rgba(3, 8, 18, 0.62) 45%, rgba(3, 8, 18, 0.08) 100%)",
}}
/>
) : null}
<div
style={{
position: "absolute",
left: "60px",
display: "flex",
flexDirection: "column",
right: characterCard ? "560px" : "450px",
...(characterCard
? { top: 0, bottom: 0, justifyContent: "center" }
: { top: "160px", bottom: "100px" }),
}}
>
{characterCard ? (
<div style={{ display: "flex", color: "#d8f4ee", fontSize: 28 }}>
ไกด์เกนชินไม่ใช่เกมมือถือ
</div>
) : null}
<div style={{ display: "flex", fontFamily: characterCard ? "Genshin" : "Kanit", fontSize: 64, marginTop: characterCard ? "8px" : 0 }}>
{shorten(title, 24)}
</div>
<div
style={{
display: "flex",
fontSize: 24,
marginTop: characterCard ? "8px" : "-20px",
color: "#fff",
}}
>
{shorten(sub, 92)}
</div>
</div>
</div>,
{
...ogImageSize,
fonts: [
{ name: "Kanit", data: kanit, style: "normal", weight: 600 },
...(characterFont
? [{ name: "Genshin", data: characterFont, style: "normal" as const, weight: 400 as const }]
: []),
],
},
);
}