diff --git a/bun.lockb b/bun.lockb index 06e8afc..ad0c102 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/projects/backend/package.json b/projects/backend/package.json index 11414b1..dfbcb6e 100644 --- a/projects/backend/package.json +++ b/projects/backend/package.json @@ -14,12 +14,14 @@ "@ssr/common": "workspace:common", "@tqman/nice-logger": "^1.0.1", "@typegoose/typegoose": "^12.8.0", + "@vercel/og": "^0.6.3", "elysia": "latest", "elysia-autoroutes": "^0.5.0", "elysia-decorators": "^1.0.2", "elysia-helmet": "^2.0.0", "elysia-rate-limit": "^4.1.0", - "mongoose": "^8.7.0" + "mongoose": "^8.7.0", + "react": "^18.3.1" }, "devDependencies": { "bun-types": "latest" diff --git a/projects/website/src/app/(pages)/api/og/player/route.tsx b/projects/backend/src/common/open-graph.tsx similarity index 73% rename from projects/website/src/app/(pages)/api/og/player/route.tsx rename to projects/backend/src/common/open-graph.tsx index 094dd31..8dc59ec 100644 --- a/projects/website/src/app/(pages)/api/og/player/route.tsx +++ b/projects/backend/src/common/open-graph.tsx @@ -1,19 +1,10 @@ -import { ImageResponse } from "next/og"; -import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; -import { NextRequest } from "next/server"; -import { formatNumberWithCommas, formatPp } from "@/common/number-utils"; -import { config } from "../../../../../../config"; - -export async function GET(request: NextRequest) { - const playerId = request.nextUrl.searchParams.get("id"); - if (!playerId) { - return new Response(null, { status: 400 }); - } - const player = await scoresaberService.lookupPlayer(playerId); - if (!player) { - return new Response(null, { status: 404 }); - } +import { ImageResponse } from "@vercel/og"; +import { formatNumberWithCommas, formatPp } from "website/src/common/number-utils"; +import { config } from "website/config"; +import React from "react"; +import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token"; +export function generatePlayerOgImage(player: ScoreSaberPlayerToken) { return new ImageResponse( (
@@ -42,7 +33,12 @@ export async function GET(request: NextRequest) {

#{formatNumberWithCommas(player.rank)}

- Player's Country + {/* eslint-disable-next-line @next/next/no-img-element */} + Player's Country

#{formatNumberWithCommas(player.countryRank)}

diff --git a/projects/backend/src/controller/player.controller.ts b/projects/backend/src/controller/player.controller.ts index 9e09b0b..4bfe294 100644 --- a/projects/backend/src/controller/player.controller.ts +++ b/projects/backend/src/controller/player.controller.ts @@ -51,4 +51,14 @@ export default class PlayerController { }; } } + + @Get("/og/:id", { + config: {}, + params: t.Object({ + id: t.String({ required: true }), + }), + }) + public async getOpenGraphImage({ params: { id } }: { params: { id: string } }) { + return await PlayerService.generateOpenGraphImage(id); + } } diff --git a/projects/backend/src/service/player.service.ts b/projects/backend/src/service/player.service.tsx similarity index 93% rename from projects/backend/src/service/player.service.ts rename to projects/backend/src/service/player.service.tsx index 35537a0..26e2b52 100644 --- a/projects/backend/src/service/player.service.ts +++ b/projects/backend/src/service/player.service.tsx @@ -5,6 +5,7 @@ import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token"; import { InternalServerError } from "../error/internal-server-error"; import ScoreSaberPlayerScoreToken from "@ssr/common/types/token/scoresaber/score-saber-player-score-token"; +import { generatePlayerOgImage } from "../common/open-graph"; export class PlayerService { /** @@ -155,4 +156,17 @@ export class PlayerService { `Updated scores set statistic for "${playerName}"(${playerId}), scores today: ${scores.rankedScores} ranked, ${scores.unrankedScores} unranked` ); } + + /** + * Generates the OpenGraph image for the player + * + * @param id the player's id + */ + public static async generateOpenGraphImage(id: string) { + const player = await scoresaberService.lookupPlayer(id); + if (player == undefined) { + return undefined; + } + return generatePlayerOgImage(player); + } } diff --git a/projects/backend/tsconfig.json b/projects/backend/tsconfig.json index 0002c94..39cee78 100644 --- a/projects/backend/tsconfig.json +++ b/projects/backend/tsconfig.json @@ -10,5 +10,6 @@ "skipLibCheck": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, + "jsx": "react" } } diff --git a/projects/website/src/app/(pages)/player/[...slug]/page.tsx b/projects/website/src/app/(pages)/player/[...slug]/page.tsx index 155635c..2d5c015 100644 --- a/projects/website/src/app/(pages)/player/[...slug]/page.tsx +++ b/projects/website/src/app/(pages)/player/[...slug]/page.tsx @@ -95,15 +95,9 @@ export async function generateMetadata(props: Props): Promise { title: `${player.name}`, openGraph: { title: `ScoreSaber Reloaded - ${player.name}`, - // description: ` - // PP: ${formatPp(player.pp)}pp - // Rank: #${formatNumberWithCommas(player.rank)} (#${formatNumberWithCommas(player.countryRank)} ${player.country}) - // Joined ScoreSaber: ${format(player.joinedDate, { date: "medium", time: "short" })} - // - // View the scores for ${player.name}!`, images: [ { - url: `${config.siteUrl}/api/og/player/?id=${player.id}`, + url: `${config.siteApi}/player/og/${player.id}`, }, ], },