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)}
-
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
#{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}`,
},
],
},