Skip to content

Next.js

Generate Next.js OG Images with VectorFast Render

Add on-brand Open Graph images to a Next.js App Router site with VectorFast Render, using generateMetadata or opengraph-image, signed URLs or the REST API, and a fair comparison with next/og.

Published 2026-10-30 · Markdown version

VectorFast Render is in early access. Billing is not connected yet, and API keys are issued to waitlist members as access opens. Join the waitlist

Short answer: in a Next.js App Router page, return a VectorFast Render image URL from generateMetadata as openGraph.images. Get the URL either by calling POST /v1/renders on the server (works in early access) or by signing a /i/blog-post.png URL on the server (once per-key signing secrets are issued). Either way the image is rendered once and then served from cache.

This guide uses the blog-post template (1200×630). Any template works the same way. It was written against the Next.js docs for generateMetadata and opengraph-image (Next.js 16, pages last updated 2026-08-19 and 2026-07-09, read on 2026-10-30).

Before you start

  • A VectorFast Render API key in VECTORFAST_API_KEY (see the quickstart). It is a server secret: read it only in Server Components, route handlers, and generateMetadata, never in a 'use client' file.
  • Optional: a brand kit id in VECTORFAST_BRAND_KIT. Without it, renders use your default brand kit.

Option 1: call the REST API in generateMetadata

Create a small server-only helper:

lib/og-image.ts
import 'server-only' // npm install server-only: fails the build if a Client Component imports this file

interface OgCard {
  title: string
  excerpt?: string
  category?: string
  author?: string
}

export async function ogImageUrl(card: OgCard): Promise<string | null> {
  const response = await fetch('https://vectorfast.ai/v1/renders', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.VECTORFAST_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      template: 'blog-post@1',
      width: 1200,
      height: 630,
      brand_kit: process.env.VECTORFAST_BRAND_KIT || undefined,
      params: card,
    }),
    // The same params always return the same URL, so the response can be cached.
    cache: 'force-cache',
  })
  if (!response.ok)
    return null
  const render = await response.json() as { url: string }
  return render.url
}

Use it in the page:

app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
import { getPost } from '@/lib/posts'
import { ogImageUrl } from '@/lib/og-image'

export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
  const { slug } = await params
  const post = await getPost(slug)
  const image = await ogImageUrl({
    title: post.title,
    excerpt: post.summary,
    category: post.category,
    author: post.author,
  })

  return {
    title: post.title,
    openGraph: {
      images: image ? [{ url: image, width: 1200, height: 630, alt: post.title }] : [],
    },
  }
}

What happens:

  • The first time a post's metadata is generated, VectorFast renders the card and returns a signed url.
  • The returned URL is immutable: crawlers and chat apps that fetch it get a one-year Cache-Control: immutable response.
  • The same title, excerpt, and brand kit version always produce the same cache key, so regenerating the page (a rebuild, a revalidation) returns cached: true and does not render again. Once billing is connected, those cache hits cost nothing.
  • If the API is unreachable, the helper returns null and the page still renders without an image; handle that however your site prefers, for example with a static fallback image.

Text longer than a parameter's limit is rejected (title is at most 110 characters for blog-post), so trim long titles before sending them.

Option 2: sign the image URL on your server

A signed URL is computed locally with an HMAC, so it costs no network request at build or render time. The image is rendered the first time someone fetches the URL.

Not available yet

Per-key signing secrets are not issued in early access. Until they are, use option 1; its url is the same kind of signed URL.

Copy signRenderUrl from Signed URLs into lib/sign-render-url.ts, then:

app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
import { getPost } from '@/lib/posts'
import { signRenderUrl } from '@/lib/sign-render-url'

export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
  const { slug } = await params
  const post = await getPost(slug)
  const url = signRenderUrl({
    template: 'blog-post',
    width: 1200,
    height: 630,
    brand: process.env.VECTORFAST_BRAND_KIT_VERSION, // for example "bk_…@3"
    params: { title: post.title, excerpt: post.summary, category: post.category },
  })

  return {
    title: post.title,
    openGraph: { images: [{ url, width: 1200, height: 630, alt: post.title }] },
  }
}

Signed URLs for an account brand kit must pin a version (bk_…@3), so a stored URL never changes pixels. Bump the version in your environment when you change the kit.

Option 3: keep an opengraph-image route

If your site already uses the opengraph-image.tsx file convention, you can keep the route and have it return the VectorFast image. The default export must return a Response:

app/blog/[slug]/opengraph-image.tsx
import { getPost } from '@/lib/posts'
import { ogImageUrl } from '@/lib/og-image'

export const alt = 'Blog post'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'

export default async function Image({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const post = await getPost(slug)
  const url = await ogImageUrl({ title: post.title, excerpt: post.summary })
  if (!url)
    return new Response('Image unavailable', { status: 502 })
  const image = await fetch(url)
  return new Response(image.body, { headers: { 'Content-Type': 'image/png' } })
}

Next.js caches generated opengraph-image routes by default unless they use request-time APIs, so this runs once per post per build or revalidation. The trade-off: your deployment now proxies the image bytes. Options 1 and 2 point crawlers straight at the VectorFast URL instead.

VectorFast Render or next/og?

Next.js ships ImageResponse from next/og (the @vercel/og library), which renders JSX to PNG inside your own functions. It is free, open source, and a good fit when one Next.js app needs OG images and you want to own the layout code.

VectorFast Render fits better when:

  • the same on-brand cards are needed outside this one app (other sites, CI, an AI agent);
  • you want one versioned brand kit, including a logo traced from a PNG, instead of fonts and colors copied into each project;
  • you want renders cached and deduplicated across every caller.

next/og has limits of its own: only flexbox and a subset of CSS, ttf/otf/woff fonts that you load yourself, and a 500 KB bundle limit for the code, fonts, and images in the function (Vercel docs, read 2026-10-30). See the full VectorFast Render vs Vercel OG comparison.

FAQ

Will social apps show the image immediately? The first fetch of a new URL renders the image (tens of milliseconds in local tests, plus font loading; edge timings are not published yet); later fetches are cache hits.

Can I use a 1080×1080 image for LinkedIn or Instagram? Yes. Pick a template that supports that size, such as launch-post or social-square, and pass width and height.

What if a title has Chinese or Japanese characters? Templates load a CJK font subset automatically when the text needs it.