diff --git a/backend/src/embeds.ts b/backend/src/embeds.ts
new file mode 100644
index 0000000..4231636
--- /dev/null
+++ b/backend/src/embeds.ts
@@ -0,0 +1,156 @@
+import { getVideoById } from "./services/db";
+import { getStreamUrl, getObjectUrl } from "./services/rustfs";
+import type { VideoEntry } from "./types/video";
+import { readFileSync } from "fs";
+
+const BASE_URL = process.env.BASE_URL || "https://surf.nathan.rip";
+
+function escapeHtml(s: string): string {
+ return s
+ .replace(/&/g, "&")
+ .replace(/"/g, """)
+ .replace(//g, ">");
+}
+
+function formatRunTime(seconds: number): string {
+ const mins = Math.floor(seconds / 60);
+ const secs = seconds % 60;
+ return `${mins}:${secs.toFixed(3).padStart(6, "0")}`;
+}
+
+function mapDisplayName(mapName: string): string {
+ return mapName.replace(/_/g, " ").toUpperCase();
+}
+
+function pageTitle(video: VideoEntry): string {
+ return `${mapDisplayName(video.mapName)} — ${video.playerName} · surf.nathan.rip`;
+}
+
+function ogTitle(video: VideoEntry): string {
+ return mapDisplayName(video.mapName);
+}
+
+function ogDescription(video: VideoEntry): string {
+ const parts: string[] = [formatRunTime(video.runTime)];
+ if (video.tier != null) parts.push(`Tier ${video.tier}`);
+ return `${video.playerName} on ${mapDisplayName(video.mapName)} · ${parts.join(" · ")}`;
+}
+
+function buildOgsHtml(video: VideoEntry): string {
+ const runUrl = `${BASE_URL}/run/${video.id}`;
+ const title = ogTitle(video);
+ const description = ogDescription(video);
+ const oembedUrl = `${BASE_URL}/api/oembed/${video.id}`;
+
+ const thumbnailUrl = video.thumbnailKey
+ ? getObjectUrl(video.thumbnailKey)
+ : undefined;
+ const videoStreamUrl = `${BASE_URL}${getStreamUrl(video.id)}`;
+
+ let tags =
+ // oEmbed discovery — lets Discord (and others) find the JSON endpoint
+ ` \n` +
+ ` \n` +
+ ` \n` +
+ ` \n` +
+ ` \n` +
+ ` `;
+
+ if (thumbnailUrl) {
+ tags +=
+ `\n \n` +
+ ` \n` +
+ ` `;
+ }
+
+ // Video embed — Discord & Facebook
+ tags +=
+ `\n \n` +
+ ` \n` +
+ ` \n` +
+ ` `;
+
+ // Twitter / X
+ tags +=
+ `\n \n` +
+ ` \n` +
+ ` `;
+
+ if (thumbnailUrl) {
+ tags += `\n `;
+ }
+
+ // Steam uses the same og: tags, nothing extra needed
+
+ return tags;
+}
+
+export interface OembedResponse {
+ type: string;
+ version: string;
+ title: string;
+ author_name: string;
+ author_url: string;
+ provider_name: string;
+ provider_url: string;
+ thumbnail_url?: string;
+ html?: string;
+}
+
+export function getOembed(id: string): OembedResponse | null {
+ const video = getVideoById(id);
+ if (!video) return null;
+
+ const thumbnailUrl = video.thumbnailKey
+ ? getObjectUrl(video.thumbnailKey)
+ : undefined;
+
+ return {
+ type: "video",
+ version: "1.0",
+ title: ogTitle(video),
+ author_name: video.playerName,
+ author_url: `https://dashboard.momentum-mod.org/maps/${video.mapName}`,
+ provider_name: "surf.nathan.rip",
+ provider_url: BASE_URL,
+ thumbnail_url: thumbnailUrl,
+ };
+}
+
+/**
+ * For a given run ID, return the SPA's index.html with OG meta tags
+ * injected into
and the updated. If the run doesn't exist,
+ * returns the plain SPA html (React Router will show a 404). If the
+ * static index.html isn't available, returns null.
+ */
+export function renderRunPage(
+ id: string,
+ staticDir: string,
+): { html: string; contentType: string } | null {
+ const indexPath = `${staticDir}/index.html`;
+
+ let indexHtml: string;
+ try {
+ indexHtml = readFileSync(indexPath, "utf-8");
+ } catch {
+ return null;
+ }
+
+ const video = getVideoById(id);
+ if (!video) {
+ // Serve the plain SPA — React Router handles the 404
+ return { html: indexHtml, contentType: "text/html" };
+ }
+
+ const ogTags = buildOgsHtml(video);
+ const title = pageTitle(video);
+ const enhancedHtml = indexHtml
+ .replace("", `${ogTags}\n `)
+ .replace(
+ /[^<]*<\/title>/,
+ `${escapeHtml(title)}`,
+ );
+
+ return { html: enhancedHtml, contentType: "text/html" };
+}
diff --git a/backend/src/index.ts b/backend/src/index.ts
index 1bf75e1..8679d45 100644
--- a/backend/src/index.ts
+++ b/backend/src/index.ts
@@ -2,8 +2,7 @@ import { Elysia, status } from "elysia";
import { cors } from "@elysiajs/cors";
import { videoRoutes } from "./routes/videos";
import { initDb } from "./services/db";
-import { join } from "path";
-import { existsSync, statSync } from "fs";
+import { renderRunPage, getOembed } from "./embeds";
initDb();
@@ -16,28 +15,20 @@ const app = new Elysia()
exposeHeaders: ["Content-Range", "Accept-Ranges", "Content-Length"],
}),
)
- .onError(({ error }) => {
- console.error("Unhandled error:", error);
- return status(500, { error: "Internal server error" });
- })
.use(videoRoutes)
.get("/api/health", () => ({ status: "ok" }))
- .get("*", ({ request }) => {
- const url = new URL(request.url);
- const pathname = url.pathname;
-
- if (pathname.startsWith("/api")) return status(404, "Not found");
-
- const filePath = join(STATIC_DIR, pathname);
- if (existsSync(filePath) && statSync(filePath).isFile())
- return Bun.file(filePath);
-
- const indexPath = join(STATIC_DIR, "index.html");
- if (existsSync(indexPath)) return Bun.file(indexPath);
-
- return status(404, "Not found");
+ .get("/api/oembed/:id", ({ params: { id } }) => {
+ const oembed = getOembed(id);
+ if (!oembed) return status(404, { error: "Not found" });
+ return oembed;
+ })
+ .get("/run/:id", ({ params: { id } }) => {
+ const result = renderRunPage(id, STATIC_DIR);
+ if (!result) return status(404, "Not found");
+ return new Response(result.html, {
+ headers: { "Content-Type": result.contentType },
+ });
})
.listen(PORT);
console.log(`surf.nathan.rip running on port ${PORT}`);
-console.log(`Serving static files from ${STATIC_DIR}`);
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 013acb4..f732cbd 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -10,7 +10,7 @@ export default function App() {
}>
} />
- } />
+ } />
} />
diff --git a/frontend/src/components/VideoCard.tsx b/frontend/src/components/VideoCard.tsx
index 8dfac79..c3eef05 100644
--- a/frontend/src/components/VideoCard.tsx
+++ b/frontend/src/components/VideoCard.tsx
@@ -52,7 +52,7 @@ export default function VideoCard({ video }: { video: VideoListItem }) {
return (
diff --git a/frontend/src/pages/VideoDetail.tsx b/frontend/src/pages/VideoDetail.tsx
index 8aea319..7a480b5 100644
--- a/frontend/src/pages/VideoDetail.tsx
+++ b/frontend/src/pages/VideoDetail.tsx
@@ -66,8 +66,14 @@ export default function VideoDetail() {
useEffect(() => {
if (!id) return;
fetchVideo(id)
- .then(setVideo)
+ .then((v) => {
+ setVideo(v);
+ document.title = `${getMapDisplayName(v.mapName)} — ${v.playerName} · surf.nathan.rip`;
+ })
.catch((err) => setError(err.message));
+ return () => {
+ document.title = "surf.nathan.rip";
+ };
}, [id]);
if (error) {