diff --git a/middleware.js b/middleware.js new file mode 100644 index 00000000..27d97eb2 --- /dev/null +++ b/middleware.js @@ -0,0 +1,43 @@ +// Routes crawler requests for `/?v=` 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); +} diff --git a/package-lock.json b/package-lock.json index 3a4fc0da..417e0987 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 6a407dce..f0cbc788 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/vercel.json b/vercel.json index e2e36021..9b0e9cb6 100644 --- a/vercel.json +++ b/vercel.json @@ -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" + } ] } ]