Change /video/ route to /run/, add embed support

This commit is contained in:
CallMeVerity
2026-06-04 22:42:39 +01:00
parent b9190a9b44
commit 31e110af5f
5 changed files with 177 additions and 24 deletions
+156
View File
@@ -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, "&lt;")
.replace(/>/g, "&gt;");
}
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
` <link rel="alternate" type="application/json+oembed" href="${escapeHtml(oembedUrl)}" title="${escapeHtml(title)}" />\n` +
` <meta property="og:title" content="${escapeHtml(title)}" />\n` +
` <meta property="og:description" content="${escapeHtml(description)}" />\n` +
` <meta property="og:url" content="${runUrl}" />\n` +
` <meta property="og:type" content="video.other" />\n` +
` <meta property="og:site_name" content="surf.nathan.rip" />`;
if (thumbnailUrl) {
tags +=
`\n <meta property="og:image" content="${thumbnailUrl}" />\n` +
` <meta property="og:image:width" content="1280" />\n` +
` <meta property="og:image:height" content="720" />`;
}
// Video embed — Discord & Facebook
tags +=
`\n <meta property="og:video" content="${videoStreamUrl}" />\n` +
` <meta property="og:video:type" content="video/mp4" />\n` +
` <meta property="og:video:width" content="1280" />\n` +
` <meta property="og:video:height" content="720" />`;
// Twitter / X
tags +=
`\n <meta name="twitter:card" content="summary_large_image" />\n` +
` <meta name="twitter:title" content="${escapeHtml(title)}" />\n` +
` <meta name="twitter:description" content="${escapeHtml(description)}" />`;
if (thumbnailUrl) {
tags += `\n <meta name="twitter:image" content="${thumbnailUrl}" />`;
}
// 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 <head> and the <title> 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("</head>", `${ogTags}\n </head>`)
.replace(
/<title>[^<]*<\/title>/,
`<title>${escapeHtml(title)}</title>`,
);
return { html: enhancedHtml, contentType: "text/html" };
}
+12 -21
View File
@@ -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}`);
+1 -1
View File
@@ -10,7 +10,7 @@ export default function App() {
<Routes>
<Route element={<Layout />}>
<Route index element={<Home />} />
<Route path="video/:id" element={<VideoDetail />} />
<Route path="run/:id" element={<VideoDetail />} />
<Route path="upload" element={<Upload />} />
</Route>
</Routes>
+1 -1
View File
@@ -52,7 +52,7 @@ export default function VideoCard({ video }: { video: VideoListItem }) {
return (
<Link
to={`/video/${video.id}`}
to={`/run/${video.id}`}
className="group flex h-full flex-col shadow no-underline"
>
<div className="relative aspect-video overflow-hidden rounded-t">
+7 -1
View File
@@ -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) {