From 98435fa903d2a9edb664737f2f448448f91fc5c4 Mon Sep 17 00:00:00 2001 From: BrickheadJohnny <92519134+BrickheadJohnny@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:46:12 +0100 Subject: [PATCH] fix: cache og image responses (#1596) * feat: optimize og image generation * fix(og images): use edge runtime * fix(linkpreview): catch all routes and double check imageUrl --- src/components/common/LinkPreviewHead.tsx | 10 +- .../[group].tsx => [[...params]]/index.tsx} | 65 ++-- .../linkpreview/[timestamp]/[guild]/index.tsx | 275 -------------- .../api/linkpreview/[timestamp]/index.tsx | 343 ------------------ 4 files changed, 43 insertions(+), 650 deletions(-) rename src/pages/api/linkpreview/{[timestamp]/[guild]/[group].tsx => [[...params]]/index.tsx} (80%) delete mode 100644 src/pages/api/linkpreview/[timestamp]/[guild]/index.tsx delete mode 100644 src/pages/api/linkpreview/[timestamp]/index.tsx diff --git a/src/components/common/LinkPreviewHead.tsx b/src/components/common/LinkPreviewHead.tsx index b8aaeb83f3..5a1bd6f3fd 100644 --- a/src/components/common/LinkPreviewHead.tsx +++ b/src/components/common/LinkPreviewHead.tsx @@ -4,8 +4,16 @@ type Props = { path: string } +const getTimestamp = () => { + const rounded = new Date() + rounded.setMinutes(0) + rounded.setSeconds(0) + rounded.setMilliseconds(0) + return rounded.getTime() +} + const LinkPreviewHead = ({ path }: Props) => { - const url = `https://guild.xyz/api/linkpreview/${Date.now()?.toString()}/${path}` + const url = `https://guild.xyz/api/linkpreview/${getTimestamp()}/${path}` return ( diff --git a/src/pages/api/linkpreview/[timestamp]/[guild]/[group].tsx b/src/pages/api/linkpreview/[[...params]]/index.tsx similarity index 80% rename from src/pages/api/linkpreview/[timestamp]/[guild]/[group].tsx rename to src/pages/api/linkpreview/[[...params]]/index.tsx index b50f4fb18b..8cae52a3d0 100644 --- a/src/pages/api/linkpreview/[timestamp]/[guild]/[group].tsx +++ b/src/pages/api/linkpreview/[[...params]]/index.tsx @@ -10,32 +10,34 @@ export const config = { const interFont = loadGoogleFont("Inter", "400") const interBoldFont = loadGoogleFont("Inter", "700") const dystopianFont = fetch( - new URL("../../../../../../public/fonts/Dystopian-Black.woff", import.meta.url) + new URL("../../../../../public/fonts/Dystopian-Black.woff", import.meta.url) ).then((res) => res.arrayBuffer()) const handler = async (req, _) => { const { protocol, host } = req.nextUrl const baseUrl = `${protocol}//${host}` - const [, urlName, groupUrlName] = + const [, guildUrlName, pageUrlName] = req.nextUrl?.pathname ?.replace("/api/linkpreview", "") ?.split("/") ?.filter((param) => !!param) ?? [] - if (!urlName || !groupUrlName) return new ImageResponse(<>, { status: 404 }) + if (!guildUrlName) return new Response(undefined, { status: 404 }) - const [guild, groups, guildRoles]: [Guild, Guild["groups"], Guild["roles"]] = + const [guild, guildRoles, pages]: [Guild, Guild["roles"], Guild["groups"]] = await Promise.all([ - fetch(`${env.NEXT_PUBLIC_API.replace("v1", "v2")}/guilds/${urlName}`).then( - (res) => res.json() - ), fetch( - `${env.NEXT_PUBLIC_API.replace("v1", "v2")}/guilds/${urlName}/groups` + `${env.NEXT_PUBLIC_API.replace("v1", "v2")}/guilds/${guildUrlName}` ).then((res) => res.json()), fetch( - `${env.NEXT_PUBLIC_API.replace("v1", "v2")}/guilds/${urlName}/roles` + `${env.NEXT_PUBLIC_API.replace("v1", "v2")}/guilds/${guildUrlName}/roles` ).then((res) => res.json()), + !!pageUrlName + ? fetch( + `${env.NEXT_PUBLIC_API.replace("v1", "v2")}/guilds/${guildUrlName}/groups` + ).then((res) => res.json()) + : undefined, ]).catch(() => [null, null, null]) if (!guild?.id) @@ -44,13 +46,13 @@ const handler = async (req, _) => { statusText: "Guild not found", }) - const group = groups?.find((g) => g.urlName === groupUrlName) + const page = pages?.find((p) => p.urlName === pageUrlName) - if (!group) - return new Response(undefined, { - status: 404, - statusText: "Group not found", - }) + const name = page?.name ?? guild.name + const image = page?.imageUrl || guild.imageUrl + const roles = !!page + ? guildRoles.filter((role) => role.groupId === page.id) + : guildRoles try { const [interFontData, interBoldFontData, dystopianFontData] = await Promise.all([ @@ -59,11 +61,9 @@ const handler = async (req, _) => { dystopianFont, ]) - const roles = guildRoles?.map((role) => role.name) - - const safeGroupDescription = group.description?.replaceAll("\n", "") - - const imageUrl = group.imageUrl ?? guild.imageUrl + const safeDescription = ( + !!page ? page.description : guild.description + )?.replaceAll("\n", "") return new ImageResponse(
{ {/* eslint-disable-next-line @next/next/no-img-element */} {group.name}

{ textOverflow: "ellipsis", }} > - {group.name} + {name}

@@ -214,9 +214,9 @@ const handler = async (req, _) => { color: "white", }} > - {group.description ? ( - `${safeGroupDescription?.slice(0, 80)}${ - (safeGroupDescription?.length ?? 0) > 80 ? "..." : "" + {safeDescription ? ( + `${safeDescription?.slice(0, 80)}${ + safeDescription?.length > 80 ? "..." : "" }` ) : (
@@ -260,6 +260,9 @@ const handler = async (req, _) => {
, { + headers: { + "Cache-Control": "s-maxage=3600", // 1 hour + }, width: 800, height: 450, fonts: [ diff --git a/src/pages/api/linkpreview/[timestamp]/[guild]/index.tsx b/src/pages/api/linkpreview/[timestamp]/[guild]/index.tsx deleted file mode 100644 index 59f32dabf9..0000000000 --- a/src/pages/api/linkpreview/[timestamp]/[guild]/index.tsx +++ /dev/null @@ -1,275 +0,0 @@ -import { env } from "env" -import loadGoogleFont from "fonts/loadGoogleFont" -import { ImageResponse } from "next/og" -import { Guild } from "types" - -export const config = { - runtime: "edge", -} - -const interFont = loadGoogleFont("Inter", "400") -const interBoldFont = loadGoogleFont("Inter", "700") -const dystopianFont = fetch( - new URL("../../../../../../public/fonts/Dystopian-Black.woff", import.meta.url) -).then((res) => res.arrayBuffer()) - -const handler = async (req, _) => { - const { protocol, host } = req.nextUrl - const baseUrl = `${protocol}//${host}` - - const [, urlName] = - req.nextUrl?.pathname - ?.replace("/api/linkpreview", "") - ?.split("/") - ?.filter((param) => !!param) ?? [] - - if (!urlName) return new ImageResponse(<>, { status: 404 }) - - const [guild, guildRoles]: [Guild, Guild["roles"]] = await Promise.all([ - fetch(`${env.NEXT_PUBLIC_API.replace("v1", "v2")}/guilds/${urlName}`).then( - (res) => res.json() - ), - fetch(`${env.NEXT_PUBLIC_API.replace("v1", "v2")}/guilds/${urlName}/roles`).then( - (res) => res.json() - ), - ]).catch(() => [null, null]) - - if (!guild?.id) - return new Response(undefined, { - status: 404, - statusText: "Guild not found", - }) - - try { - const [interFontData, interBoldFontData, dystopianFontData] = await Promise.all([ - interFont, - interBoldFont, - dystopianFont, - ]) - - const roles = guildRoles?.map((role) => role.name) - - const safeGuildDescription = guild.description?.replaceAll("\n", "") - - return new ImageResponse( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - Guilders - -
- -
-
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - {guild.name} -
-

- {guild.name} -

-
- -
-
{`${new Intl.NumberFormat("en", { notation: "compact" }).format( - guild?.memberCount ?? 0 - )} members`}
- -
{`${roles?.length || 0} roles`}
-
- -
- {guild.description ? ( - `${safeGuildDescription?.slice(0, 80)}${ - safeGuildDescription?.length > 80 ? "..." : "" - }` - ) : ( -
-
- {"That's a great party in there!"} -
-
{"I dare you to be the plus one."}
-
- )} -
- -
- {/* eslint-disable-next-line @next/next/no-img-element */} - Guild.xyz -
- Guild.xyz -
-
-
-
, - { - width: 800, - height: 450, - fonts: [ - { - name: "Inter", - data: interFontData, - style: "normal", - weight: 400, - }, - { - name: "Inter", - data: interBoldFontData, - style: "normal", - weight: 700, - }, - { - name: "Dystopian", - data: dystopianFontData, - style: "normal", - }, - ], - } - ) - } catch (e: any) {} -} - -export default handler diff --git a/src/pages/api/linkpreview/[timestamp]/index.tsx b/src/pages/api/linkpreview/[timestamp]/index.tsx deleted file mode 100644 index ef63d658ac..0000000000 --- a/src/pages/api/linkpreview/[timestamp]/index.tsx +++ /dev/null @@ -1,343 +0,0 @@ -import { env } from "env" -import loadGoogleFont from "fonts/loadGoogleFont" -import { ImageResponse } from "next/og" -import { GuildBase } from "types" - -export const config = { - runtime: "edge", -} - -const interFont = loadGoogleFont("Inter", "400") -const dystopianFont = fetch( - new URL("../../../../../public/fonts/Dystopian-Black.woff", import.meta.url) -).then((res) => res.arrayBuffer()) - -const handler = async (req, _) => { - const { protocol, host } = req.nextUrl - const baseUrl = `${protocol}//${host}` - - try { - const [guilds, interFontData, dystopianFontData] = await Promise.all([ - fetch(`${env.NEXT_PUBLIC_API.replace("v1", "v2")}/guilds`) - .then((res) => res.json()) - .catch((_) => []), - interFont, - dystopianFont, - ]) - - return new ImageResponse( -
-
- {guilds?.slice(0, 8).map((guild) => ( - - ))} -
- -
- -
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - Guild.xyz -

- Guild -

-
- -
- Manage roles -
-
- in your community -
-
- based on tokens & NFTs -
-
-
, - { - width: 800, - height: 450, - fonts: [ - { - name: "Inter", - data: interFontData, - style: "normal", - weight: 400, - }, - { - name: "Dystopian", - data: dystopianFontData, - style: "normal", - }, - ], - } - ) - } catch (e: any) { - return new Response(`Failed to generate the image`, { - status: 500, - }) - } -} - -type GuildCardProps = { - guild: GuildBase - baseUrl: string -} - -const GuildCard = ({ guild, baseUrl }: GuildCardProps): JSX.Element => ( -
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - {guild.name} -
-
-

- {guild.name} -

- -
-
- - - - - - - - - {new Intl.NumberFormat("en", { notation: "compact" }).format( - guild.memberCount ?? 0 - )} - -
- -
- {`${guild.rolesCount} roles`} -
-
-
-
-) - -export default handler