HyperFrames runs anywhere Node and Chromium do — including Vercel Functions. Render dynamic MP4, OG images, and scheduled cron videos with the same primitives you already use for HTTP routes.
Chromium is hungry. Give the render function 3 GB and the full 5-minute timeout. For longer videos, kick off an async job and poll for the result.
// vercel.json
{
"functions": {
"app/api/render/route.ts": {
"memory": 3008,
"maxDuration": 300
}
},
"crons": [
{ "path": "/api/render/weekly-digest", "schedule": "0 13 * * MON" }
]
}You can keep using next/og for still social cards, or render a single HyperFrames frame as a PNG for richer compositions. Either way, the route looks identical.
// app/og/[slug]/route.tsx — render a 1200x630 PNG for OG.
import { ImageResponse } from "next/og";
export async function GET(_req: Request, { params }: { params: { slug: string } }) {
return new ImageResponse(
(
<div style={{ display: "flex", fontSize: 96, background: "#0a0a0a", color: "#f6f5f1" }}>
{params.slug}
</div>
),
{ width: 1200, height: 630 },
);
}Vercel Cron triggers the function every Monday. It fetches the week's digest, renders an MP4, stores it in Blob, and posts the URL to your Slack.
// app/api/render/weekly-digest/route.ts
import { renderHtml } from "hyperframes";
import { put } from "@vercel/blob";
export const runtime = "nodejs";
export const maxDuration = 300;
export async function GET() {
const html = await fetch("https://my-app.com/digest/this-week", { cache: "no-store" })
.then(r => r.text());
const mp4 = await renderHtml({ html, width: 1080, height: 1920, fps: 30 });
const { url } = await put(`digests/${Date.now()}.mp4`, mp4, { access: "public" });
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: "POST",
body: JSON.stringify({ text: `Weekly digest ready: ${url}` }),
});
return Response.json({ ok: true, url });
}Functions, edge caching, blob storage, cron — all the pieces of a video pipeline you already pay for.