[og] Route crawlers via middleware, not a rewrite

The OG function deployed and worked (`/api/og?v=<nevent>` returns the real
title, description, thumbnail and player card), but crawlers hitting
`/?v=<nevent>` still got the site-wide card: Vercel evaluates `rewrites` AFTER
the filesystem, and `/` matches the static index.html, so the rewrite never
fired.

Middleware runs before the filesystem check, so that is where the routing
belongs. Replace the dead `rewrites` block with a root middleware that matches
only `/`, bails immediately when there is no `v` param or the user-agent is not
a known preview crawler, and otherwise rewrites to /api/og.

Real viewers are unaffected -- they fall through to the untouched SPA. Note the
one behavioral difference from the rewrite design: middleware now executes on
homepage requests (matcher is `/` only) rather than being invoked solely for
crawlers. It returns next() after two cheap checks.

Verified on the live unstable deployment that /api/og already renders
`x-bitvid-og: video` with og:type video.other for the real note; this only
fixes how crawlers reach it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 5 committed 2026-07-28 14:40:20 -04:00
1 parent e5784893a8
commit 04075ff56b
4 files changed
+63 -17

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
// Routes crawler requests for `/?v=<nevent>` to the per-video OpenGraph
// renderer (api/og.js).
//
// This has to be middleware rather than a `rewrites` entry in vercel.json:
// rewrites are evaluated AFTER the filesystem, and `/` matches the static
// index.html, so the rewrite never fired and crawlers kept getting the
// site-wide card. Middleware runs before the filesystem check.
//
// Deliberately minimal. It matches only `/`, bails immediately when there is no
// `v` param or the visitor is not a known preview crawler, and never touches
// the response for a real viewer — they continue to the untouched SPA. If this
// file is removed the site keeps working, just with the generic card.
import { next, rewrite } from "@vercel/edge";
import { isCrawlerUserAgent } from "./api/_lib/ogRenderer.mjs";
export const config = {
// Only the share-link path. Every other route skips middleware entirely.
matcher: "/",
};
export default function middleware(request) {
let url;
try {
url = new URL(request.url);
} catch (err) {
return next();
}
// A bare homepage visit is not a share link.
if (!url.searchParams.get("v")) {
return next();
}
const userAgent = request.headers?.get?.("user-agent") || "";
if (!isCrawlerUserAgent(userAgent)) {
return next();
}
const target = new URL(url.toString());
target.pathname = "/api/og";
return rewrite(target);
}
+7
View File
@@ -11,6 +11,7 @@
"dependencies": {
"@floating-ui/dom": "^1.7.4",
"@tailwindcss/typography": "^0.5.19",
"@vercel/edge": "^1.3.1",
"autoprefixer": "^10.4.24",
"cross-env": "^10.1.0",
"esbuild": "^0.27.3",
@@ -1478,6 +1479,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@vercel/edge": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@vercel/edge/-/edge-1.3.1.tgz",
"integrity": "sha512-k0tKWeYEWOv6qU6+Z8+hv2Nt/u3vFHRZdB0uvwE0DxoddU/5kWX/Chu8F6ojvqmRlnZwuX/7kyZFVeCP6DxqBg==",
"license": "Apache-2.0"
},
"node_modules/@vitest/expect": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
+1
View File
@@ -85,6 +85,7 @@
"dependencies": {
"@floating-ui/dom": "^1.7.4",
"@tailwindcss/typography": "^0.5.19",
"@vercel/edge": "^1.3.1",
"autoprefixer": "^10.4.24",
"cross-env": "^10.1.0",
"esbuild": "^0.27.3",
+12 -17
View File
@@ -7,37 +7,32 @@
"unstable": true
}
},
"rewrites": [
{
"source": "/",
"has": [
{ "type": "query", "key": "v" },
{
"type": "header",
"key": "user-agent",
"value": ".*(facebookexternalhit|Twitterbot|Slackbot|Discordbot|TelegramBot|WhatsApp|LinkedInBot|Pinterest|redditbot|Embedly|Iframely|Mastodon|Bluesky|SkypeUriPreview|vkShare|Googlebot|bingbot|Applebot|YandexBot|DuckDuckBot|Baiduspider|PetalBot|primal).*"
}
],
"destination": "/api/og"
}
],
"headers": [
{
"source": "/sw.min.js",
"headers": [
{ "key": "Cache-Control", "value": "no-cache, must-revalidate" }
{
"key": "Cache-Control",
"value": "no-cache, must-revalidate"
}
]
},
{
"source": "/",
"headers": [
{ "key": "Cache-Control", "value": "no-cache, must-revalidate" }
{
"key": "Cache-Control",
"value": "no-cache, must-revalidate"
}
]
},
{
"source": "/(.*\\.html)",
"headers": [
{ "key": "Cache-Control", "value": "no-cache, must-revalidate" }
{
"key": "Cache-Control",
"value": "no-cache, must-revalidate"
}
]
}
]