Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Generate the image in a NestJS service, return its bytes through StreamableFile, and reference the public route from an absolute og:image URL. The controller is small; the important engineering work is choosing a renderer that runs in your Node.js environment, constraining inputs, loading fonts and assets predictably, and setting cache behavior deliberately.

What the request flow looks like

When a crawler requests a page, it reads the page’s metadata and then fetches the URL in og:image. Your NestJS application can expose a route such as https://example.com/og/article-slug.png. That route loads the article data, renders a deterministic template to PNG, and sends the resulting bytes with image/png.

  1. The page includes an absolute Open Graph image URL.
  2. A crawler requests that URL without a browser session.
  3. NestJS validates the identifier and asks a rendering service for PNG bytes.
  4. NestJS returns those bytes with the correct content type and cache headers.

The metadata pattern and public URL requirement are described in Vercel’s OG image guide. The response technique below follows NestJS’s documented StreamableFile API in its file-response documentation.

Choose a renderer that fits your deployment

NestJS does not prescribe one OG renderer. Keep rendering behind an application service so you can use a library compatible with your deployed Node.js runtime, whether the app runs on a long-lived server, a container, or a restricted function environment.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Using Vercel’s ImageResponse stack

Vercel documents @vercel/og and ImageResponse; its implementation uses Satori and Resvg to convert supported markup and CSS into PNG. The documented subset supports basic flexbox and absolute positioning, but not CSS grid. Fonts may be supplied as TTF, OTF, or WOFF, with TTF or OTF preferred for parsing speed. See the ImageResponse API reference. Integrating that library into NestJS is an application choice, not an officially documented NestJS recipe, so validate it against your exact Node.js runtime and hosting adapter.

Dimensions, assets and bundle limits

Vercel’s reference lists 1200×630 pixels as the default, and its guide recommends that size for OG images. Treat it as a documented recommendation rather than a universal requirement for every social network. The same guide describes a 500 KB maximum bundle for its setup, counting JSX, CSS, fonts, images and other assets; that limit is specific to that deployment model. Other hosts can impose different limits.

Build the NestJS endpoint

1. Define a rendering service contract

Keep the controller independent of the rendering library. The service should return a Buffer and should reject unknown slugs instead of silently producing a misleading image.

import { Injectable, NotFoundException } from '@nestjs/common';

@Injectable()
export class OgImageService {
  async renderPng(slug: string): Promise<Buffer> {
    const page = await this.loadPage(slug);
    if (!page) throw new NotFoundException();

    // Call your runtime-compatible renderer here.
    // It must return PNG bytes for the supplied, validated data.
    return this.rendererToPng({
      title: page.title,
      description: page.description,
      author: page.author,
    });
  }

  private async loadPage(slug: string) {
    // Replace with your repository or database query.
    return { title: 'Example article', description: 'A short summary', author: 'Itechguides' };
  }

  private async rendererToPng(input: {
    title: string; description: string; author: string;
  }): Promise<Buffer> {
    throw new Error('Implement with a renderer supported by your deployment');
  }
}

The deliberately explicit renderer boundary prevents a sample from implying that NestJS itself supplies HTML-to-PNG conversion. Pin the renderer version, keep the template deterministic, and test it in the same runtime image used in production.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

2. Return bytes with StreamableFile

import { Controller, Get, Param, StreamableFile } from '@nestjs/common';
import { OgImageService } from './og-image.service';

@Controller('og')
export class OgController {
  constructor(private readonly images: OgImageService) {}

  @Get(':slug')
  async image(@Param('slug') slug: string): Promise<StreamableFile> {
    const png = await this.images.renderPng(slug);
    return new StreamableFile(png, { type: 'image/png' });
  }
}

StreamableFile accepts a Buffer (a Uint8Array) or a readable stream and lets Nest set the response type. This framework-managed path is generally easier to keep portable between Express and Fastify than taking ownership of the native response object.

3. Add explicit cache headers

Choose the policy according to your URL design. A versioned URL such as /og/article-slug.v3.png can use a long immutable lifetime. A mutable URL should use a shorter TTL or an invalidation strategy. Vercel’s ImageResponse reference documents a public immutable cache-control header for its own response; do not assume another renderer or Nest adapter applies that policy automatically.

import { Controller, Get, Header, Param, StreamableFile } from '@nestjs/common';

@Controller('og')
export class OgController {
  constructor(private readonly images: OgImageService) {}

  @Get(':slug')
  @Header('Cache-Control', 'public, max-age=300, s-maxage=3600')
  async image(@Param('slug') slug: string): Promise<StreamableFile> {
    return new StreamableFile(await this.images.renderPng(slug), {
      type: 'image/png',
    });
  }
}

Use a different TTL when your publishing workflow requires immediate replacement. Store generated bytes in object storage or a cache when rendering is expensive, but make cache keys include every value that changes the image.

Publish the metadata

Place an absolute, publicly reachable URL in the page head. Relative paths, private hostnames, authentication requirements and robots or firewall rules that block crawlers will prevent a preview from loading.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<meta property="og:image" content="https://example.com/og/how-to-generate-images.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />

Generate the URL from your canonical public origin, not from an internal request host header unless that header has been trusted and normalized. Test the deployed route with the social preview debugger or crawler used by your publishing process; behavior differs across platforms and no single crawler result proves every network can fetch it.

Template and input safety

Constrain dynamic text

Validate a slug against the format your application actually uses, fetch only the matching record, and bound text before it reaches the renderer. Vercel’s dynamic-title example slices a title to 100 characters; that is an example of bounding input, not a complete security policy. Decide whether to truncate by Unicode code points, words or rendered width, and provide an ellipsis that your template handles.

Do not turn the endpoint into a proxy

Do not accept arbitrary HTML, CSS or remote image URLs from an unauthenticated request. If remote assets are necessary, allow-list hosts, enforce timeouts and size limits, and reject redirects to private network addresses. Prefer local, bundled fonts and logos so a slow or unavailable third-party host cannot make every preview fail.

Use renderer-supported CSS

When using the documented Satori/Resvg path, build layouts with flexbox and absolute positioning rather than CSS grid. Supply fonts in a supported format and load them deterministically. Keep templates small enough for the deployment’s bundle and memory limits.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Failure modes and fixes

The crawler receives HTML or a blank image

  • Confirm the route returns status 200 and Content-Type: image/png, not an exception page.
  • Log renderer errors separately from controller errors and return a deliberate 4xx/5xx response when the slug is unknown.
  • Open the exact production URL without cookies or a logged-in session.

Fonts or logos are missing

  • Use an absolute, allow-listed asset path or bundle the file with the service.
  • Verify the deployed filesystem contains the font; a local relative path may not exist in a container.
  • Check that the file format is accepted by the renderer.

The image is stale

  • Inspect Cache-Control and any CDN cache key.
  • Use a versioned URL when content changes must be immutable.
  • Shorten the TTL or purge the cache for mutable URLs.

Requests time out or consume too much memory

  • Set bounded database, asset and rendering timeouts.
  • Cache identical renders and coalesce concurrent requests for the same key.
  • Reduce font and image assets and avoid unbounded user text.
  • Move expensive rendering to a queue or worker if your hosting environment has strict request limits.

Using @Res() unexpectedly changes behavior

NestJS documents that @Res() opts a route into library-specific response management. @Res({ passthrough: true }) lets you set response details while leaving the rest of handling to Nest. Direct native piping takes control of the response and can affect post-controller interceptor behavior. Prefer StreamableFile when it meets your needs; consult the NestJS controller documentation before mixing response styles.

Or skip the browser setup

If you need a clean screenshot of a rendered page rather than maintaining a browser-and-renderer pipeline, ScreenshotNeo provides a GET-based screenshot API and an MCP server for AI agents. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing status.

Use the API from a build job, preview service or NestJS worker:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

See the ScreenshotNeo documentation for parameters. Its MCP tools—take_screenshot, get_page_info and capture_pdf—work with Claude, Cursor and other MCP clients. Every plan includes the full feature set; the free plan provides 1,000 screenshots per month without a card, and paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Operational checklist

  • Use a stable, public HTTPS URL in og:image.
  • Return PNG bytes and image/png from the Nest route.
  • Validate identifiers and bound all dynamic text.
  • Keep remote assets allow-listed, sized and time-limited.
  • Use renderer-supported CSS and deterministic fonts.
  • Choose cache headers that match mutable or versioned URLs.
  • Monitor render latency, memory, error rates and cache hit behavior.
  • Verify the production URL as an unauthenticated crawler would.

Frequently Asked Questions

Can NestJS generate an OG image without a browser?

Yes. A renderer can produce PNG bytes in a service, and NestJS can return those bytes with StreamableFile. The renderer must support your Node.js runtime and template requirements.

Best Value
Sale
Repeat Offender FB Addict - Straight Outta FB Jail T-Shirt
  • Facebook addiction humor design. The Straight Outta FB Jail design is a fun gift for all the social media addicts in your life.
  • You know someone who only looks at their smartphone and addicted to FB and Co. . Then this graphic is the perfect gift!
  • Lightweight, Classic fit, Double-needle sleeve and bottom hem

What URL should og:image contain?

Use an absolute HTTPS URL that is publicly reachable by the crawlers used by your publishing platforms.

Is 1200×630 mandatory?

No. Vercel recommends 1200×630 and documents it as the ImageResponse default, but platform requirements and renderer behavior can differ.

Should I use StreamableFile or @Res()?

Use StreamableFile when you want Nest-managed response handling. Choose @Res() only when you need adapter-specific control and understand its interceptor and portability implications.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.