There’s something magical about Open Graph images.
They’re tiny, silent ambassadors that travel with your links showing up on Twitter, LinkedIn, and Slack to make your content look polished and intentional.
But when I started working with TanStack Start, I noticed something missing:
Where was the “dynamic OG image” experience we’d gotten so used to from Next.js and Vercel?
So, we built it ourselves.
And spoiler: it’s easier than you think.
If you’ve ever used vercel/og
, you know how simple it is to generate dynamic images at runtime. You pass a few query parameters, and out pops a beautiful preview with custom text, background, or even user avatars.
That’s not built into TanStack Start. But the good news is that TanStack’s server routes are flexible enough to handle the same use case. We just need the right renderer.
That’s where Takumi comes in, a Rust-powered alternative to Satori (the engine behind vercel/og
), with WebAssembly support and blazing performance.
Together, these two tools let us bring dynamic image generation into TanStack’s world, no Node canvas hacks required.
Here’s the simple idea:
Create a server route in TanStack Start that renders a JSX component into an image, using Takumi under the hood.
We can then pass in query parameters (like title, author, or background color), and the route will return a new OG image on the fly.
This is perfect for blog posts, changelogs, or any content-heavy site where every page deserves its own visual identity.
Start by installing Takumi’s core packages:
bun add @takumi-rs/core @takumi-rs/helpers @takumi-rs/image-response
That’s all you need to start generating images.
If you’re deploying to Vercel, there’s one small configuration tweak you’ll need. In your vite.config.ts
, tell Nitro to include Takumi’s binaries:
nitro: {
preset: "vercel",
externals: {
traceInclude: [
"node_modules/@takumi-rs/core",
"node_modules/@takumi-rs/image-response",
"node_modules/@takumi-rs/helpers",
"node_modules/@takumi-rs/core-linux-x64-gnu",
"node_modules/@takumi-rs/core-linux-arm64-gnu",
"node_modules/@takumi-rs/core-darwin-arm64",
"node_modules/@takumi-rs/core-darwin-x64",
],
},
},
This ensures the right binary gets shipped with your build, regardless of platform.
Let’s set up a new route at src/routes/api/og.tsx
.
This will handle all incoming image requests.
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/og")({
server: {
handlers: {
GET: async ({ request }) => {
// We'll handle the image generation here
},
},
},
});
Now you have a live endpoint /api/og
that can receive query parameters like ?title=Hello+World&author=Guido
.
At the heart of this system is a simple JSX component.
It defines how your OG image looks:
function OgImage({
title = "Your amazing website.",
author = "Your name",
}: {
title: string;
author: string;
}) {
return (
<div
style={{
backgroundColor: "#ffffff",
width: "100%",
height: "100%",
padding: "5% 10%",
display: "flex",
flexDirection: "column",
}}
>
<p
style={{
color: "#000000",
fontSize: "60px",
fontWeight: "normal",
paddingTop: "3%",
}}
>
{title}
</p>
<div style={{ marginTop: "auto" }}>
<p
style={{
color: "#000000",
fontSize: "30px",
fontWeight: "bold",
marginLeft: "auto",
}}
>
{author}
</p>
</div>
</div>
);
}
It’s minimal and readable, perfect for iteration.
You can swap in Tailwind-style utilities, gradients, or even SVG logos later.
Now, let’s bring Takumi into the mix.
Inside our GET
handler, we’ll dynamically import Takumi’s ImageResponse
(so it doesn’t bloat the client bundle) and render our component.
export const Route = createFileRoute("/og")({
server: {
handlers: {
GET: async ({ request }) => {
const { ImageResponse } = await import("@takumi-rs/image-response");
const url = new URL(request.url);
const title = url.searchParams.get("title");
const author = url.searchParams.get("author");
return new ImageResponse(<OgImage title={title} author={author} />, {
width: 1200,
height: 630,
});
},
},
},
});
Now, when you visit /og?title=Hello%20World&author=Guido
, you’ll get a fully rendered OG image generated at runtime.
To make your OG images visible to crawlers, include your route in the meta tags of each page.
// src/routes/__root.tsx
export const Route = createRootRouteWithContext<RouterAppContext>()({
head: () => ({
meta: [
{
name: "og:image",
content: "YOUR_URL/og?title=YOUR_TITLE&author=YOUR_AUTHOR",
},
],
}),
component: RootDocument,
});
Now, when someone shares your link, Twitter or Slack will automatically pull the generated image.
Once you’ve got the basics down, the possibilities open up:
Because Takumi is written in Rust, it handles all this with impressive speed and minimal memory use.
It’s modern, efficient, and built for production.
Dynamic OG images aren’t just a vanity feature: they’re a small but powerful way to extend your brand and make every link you share feel intentional.
With TanStack Start’s flexibility and Takumi’s performance, you can bring that experience to your own stack without friction or vendor lock-in.
Dynamic, fast, and fully type-safe.