DEV Community

Jonathan Gamble
Jonathan Gamble

Posted on

Using OG Image Outside of Node

The future of JavaScript on the server is NOT just NodeJS. We have Bun, Deno, and Cloudflare.

I've been trying to get Vercel's og/image package to deploy outside of NextJS and Vercel for a long time; particularly I wanted it to work on SvelteKit and Cloudflare.

But... finally... I mean, finally... we have it.

TL;DR

I provide demo and source code for getting og/image to work in NextJS, SvelteKit, Nuxt, and AnalogJS in different edge environments.

Let's see what every option for deployment and framework looks like:

Serverless Functions

If you're deploying to regular Serverless functions that use NodeJS, you shouldn't have any problems with packages.

NextJS and Vercel Edge

@vercel/og was built for NextJS, so it works out of the box. Use vercel/og for pages directory, or next/og for the new app directory API endpoint.

import { ImageResponse } from 'next/og';

export const runtime = 'edge';

export async function GET() {

    try {
        return new ImageResponse(
            (
                <div
                    style={{
                        height: '100%',
                        width: '100%',
                        display: 'flex',
                        flexDirection: 'column',
                        alignItems: 'center',
                        justifyContent: 'center',
                        backgroundColor: 'white',
                        padding: '40px',
                    }}
                >
                    <div
                        style={
                            {
                                fontSize: 60,
                                fontWeight: 'bold',
                                color: 'black',
                                textAlign: 'center',
                            }
                        }
                    >
                        Welcome to My Site
                    </div>
                    < div
                        style={{
                            fontSize: 30,
                            color: '#666',
                            marginTop: '20px',
                        }
                        }
                    >
                        Generated with Next.js ImageResponse
                    </div>
                </div>
            ),
            {
                width: 1200,
                height: 630,
            }
        )
    } catch (e) {
        console.log(`${(e as Error).message}`)
        return new Response(`Failed to generate the image`, {
            status: 500,
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

Repo: GitHub
Demo: Vercel Edge

As you can imagine, it works out of the box!

NextJS Vercel Edge

NextJS and Cloudflare

In order to use NextJS on Cloudflare, you must install OpenNext. og/next also works here out of the box!

With the core original ImageResponse plugin, you can use tailwind instead of styles with the tw attribute.

import { ImageResponse } from "next/og";


export async function GET() {

  return new ImageResponse(
    <div tw="flex h-full w-full flex-col items-center justify-center
 bg-white p-10">
      <div tw="text-center text-[60px] font-bold text-black">
        Welcome to My Site
      </div>
      <div tw="mt-5 text-[30px] text-gray-600">
        Generated with NextJS ImageResponse and deployed to Cloudflare
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Repo: GitHub
Demo: Cloudflare

NextJS Cloudflare

SvelteKit and Vercel Edge

Recently, the creator of @cf-wasm/og was kind enough to get it working for SvelteKit!

I copied the ideas from @ethercorps/sveltekit-og and made it so you can pass in a Svelte component directly!

image-card.svelte

You can call the component whatever you want. This package also uses satori-html, which automatically converts class with tailwind, to tw and react JSX under the hood. You can create variables, and pass them as well if you like!

<script lang="ts">
  const { title, website }: { title?: string; website?: string } = $props();
</script>

<div
  class="bg-slate-50 text-slate-700 w-full h-full flex items-center 
justify-center p-6"
>
  <div
    class="m-1.5 p-6 w-full h-full rounded-3xl text-[72px] flex flex-col
 border-2 border-slate-700 text-slate-700"
  >
    {title?.slice(0, 80) || "Default Title"}
    <hr class="border border-slate-700 w-full" />
    <p class="text-[52px] font-bold flex justify-center text-slate-700">
      {website || "Default Website"}
    </p>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

image-response.ts

import type { Component } from 'svelte';
import { render } from 'svelte/server';
import { ImageResponse as OGImageResponse } from '@cf-wasm/og';
import { html } from 'satori-html';

export const prerender = false;

export const ImageResponse = async <T extends Record<string, unknown>>(
    component: Component<T>,
    options?: ConstructorParameters<typeof OGImageResponse>['1'],
    props?: T
) => {
    const result = render(component as Component, { props });
    return await OGImageResponse.async(html(result.body), options);
};
Enter fullscreen mode Exit fullscreen mode

/og/+server.ts

import { type RequestHandler } from "@sveltejs/kit";
import { ImageResponse } from "$lib/image-response";
import ImageCard from "$lib/image-card.svelte";


export const prerender = false;


export const GET = (async ({ url }) => {

  const { width, height } = Object.fromEntries(url.searchParams);

  return await ImageResponse(
    ImageCard,
    {
      width: Number(width) || 1600,
      height: Number(height) || 900
    },
    {
      title: 'Custom OG Image',
      website: 'Generated with Svelte, Vercel, and Cloudflare WASM!'
    }
  );

}) satisfies RequestHandler;
Enter fullscreen mode Exit fullscreen mode

@ethercorps/sveltekit-og also now works on the Edge, but I wouldn't recommend it until the bundle is smaller problem is fixed.

Demo: Vercel Edge
Repo: GitHub

As a SvelteKit developer, I'm supper happy about this one!

SvelteKit Vercel Edge

SvelteKit and Cloudflare

You can also get it to work on Cloudflare with a few caveats.

Import from workered

import { ImageResponse as OGImageResponse } from '@cf-wasm/og/workerd';
Enter fullscreen mode Exit fullscreen mode

Use Custom Vite Plugin

import tailwindcss from '@tailwindcss/vite';
import devtoolsJson from 'vite-plugin-devtools-json';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
import cloudflareModules from '@cf-wasm/plugins/vite-additional-modules';

export default defineConfig({
    ssr: {
        noExternal: [/@cf-wasm\/.*/]
    },
    plugins: [
        tailwindcss(),
        sveltekit(),
        devtoolsJson(),
        cloudflareModules({ target: "edge-light" })
    ]
});
Enter fullscreen mode Exit fullscreen mode

Repo: GitHub
Demo: Cloudflare

SvelteKit Cloudflare

Nuxt and Vercel Edge

The Nuxt-SEO plugin works out of the box, however, there is a current bug where it won't deploy on Cloudflare or Vercel Edge correctly. You can downgrade to v5.1.9 to get this to work. However, it is a premade image that does not allow you to customize the style.

Thankfully, you can use the same @cf-wasm/og plugin to work in Nuxt!

OG Component

Like the SvelteKit version, you can write you code directly in a Vue Component!

<script setup lang="ts">
defineProps<{ title: string }>()
</script>


<template>
  <div
    class="flex h-full w-full flex-col items-center justify-center
 bg-white p-10"
  >
    <div class="text-center text-[60px] font-bold text-black">
      {{ title }}
    </div>
    <div class="mt-5 text-[30px] text-gray-600">
      Generated with Nuxt ImageResponse and deployed to Vercel Edge
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

og.get.ts

You must import the component and render it in the server endpoint.

import { ImageResponse } from '@cf-wasm/og';
import { html } from 'satori-html';
import { createSSRApp, h } from 'vue';
import { renderToString } from 'vue/server-renderer';
import Og from '~/components/og.vue';


export default defineEventHandler(async () => {

    const app = createSSRApp({
        render: () => h(Og, { title : 'Welcome to My Image' })
    })

    const data = await renderToString(app)

    return ImageResponse.async(html(data), {
        width: 1200,
        height: 630
    })
})
Enter fullscreen mode Exit fullscreen mode

Nuxt Config

Make sure to install, and add the @vitejs/plugin-vue and use the @cf-wasm/plugin.

// https://nuxt.com/docs/api/configuration/nuxt-config
import additionalModules from "@cf-wasm/plugins/nitro-additional-modules"
import vue from "@vitejs/plugin-vue"

export default defineNuxtConfig({
  compatibilityDate: '2025-07-15',
  devtools: { enabled: true },
  nitro: {
    preset: 'vercel-edge',
    modules: [additionalModules({ target: "edge-light" })],
    rollupConfig: {
      plugins: [vue()]
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

Repo: GitHub
Demo: Vercel Edge

Nuxt Vercel Edge

Nuxt and Cloudflare

You can deploy to Cloudflare with a small change:

import { ImageResponse } from '@cf-wasm/og';
Enter fullscreen mode Exit fullscreen mode

and

// https://nuxt.com/docs/api/configuration/nuxt-config
import additionalModules from "@cf-wasm/plugins/nitro-additional-modules"
import vue from "@vitejs/plugin-vue"

export default defineNuxtConfig({
  compatibilityDate: '2025-07-15',
  devtools: { enabled: true },
  nitro: {
    preset: 'cloudflare-module',
    modules: [additionalModules({ target: "edge-light" })],
    rollupConfig: {
      plugins: [vue()]
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

Repo: GitHub
Demo: Cloudflare

Nuxt Cloudflare

AnalogJS and Cloudflare

Analog works similarly thanks to @cf-wasm/og again!

Vite Config

/// <reference types="vitest" />

import { defineConfig } from 'vite';
import analog from '@analogjs/platform';
import tailwindcss from '@tailwindcss/vite';
import additionalModules from "@cf-wasm/plugins/nitro-additional-modules"

// https://vitejs.dev/config/


export default defineConfig(() => ({
  build: {
    target: ['es2020'],
  },
  resolve: {
    mainFields: ['module'],
  },
  plugins: [
    analog({
      ssr: true,
      static: false,
      nitro: {
        preset: 'cloudflare-module',
        cloudflare: {
          deployConfig: true,
          nodeCompat: true
        },
        modules: [additionalModules({ target: "edge-light" })],
        compatibilityDate: "2025-07-15"
      }
    }),
    tailwindcss()
  ]
}));
Enter fullscreen mode Exit fullscreen mode

and import from:

import { ImageResponse } from '@cf-wasm/og';
Enter fullscreen mode Exit fullscreen mode

Repo: GitHub
Demo: Cloudflare

Analog Cloudflare

AnalogJS and Vercel Edge

/// <reference types="vitest" />

import { defineConfig } from 'vite';
import analog from '@analogjs/platform';
import tailwindcss from '@tailwindcss/vite';
import additionalModules from "@cf-wasm/plugins/nitro-additional-modules"

// https://vitejs.dev/config/


export default defineConfig(() => ({
  build: {
    target: ['es2020'],
  },
  resolve: {
    mainFields: ['module'],
  },
  plugins: [
    analog({
      ssr: false,
      static: false,
      nitro: {
        preset: 'vercel-edge',
        modules: [additionalModules({ target: "edge-light" })],
        compatibilityDate: "2025-07-15"
      }
    }),
    tailwindcss()
  ]
}));
Enter fullscreen mode Exit fullscreen mode

Repo: GitHub
Demo: Vercel Edge

Analog Vercel Edge

Bonus - SvelteKit with Vercel Bun

Vercel bun works as expected with @vercel/og! No workarounds. However, there is currently a satori-html Bun bug if you're NOT using JSX. I assume it will be fixed in the coming weeks. Either way, Vercel Bun is currently still experimental.

I used AI to create a small satori parser, but don't count on it to work for anything complicated.

Image Response

import type { Component } from 'svelte';
import { render } from 'svelte/server';
import { ImageResponse as OGImageResponse } from '@vercel/og';
import { htmlToOgStrict as html } from './parse-html';
//import { html } from 'satori-html';

// https://github.com/oven-sh/bun/pull/15047

export const prerender = false;


export const ImageResponse = <T extends Record<string, unknown>>(
    component: Component<T>,
    options?: ConstructorParameters<typeof OGImageResponse>['1'],
    props?: T
) => {
    const result = render(component as Component, { props });
    return new OGImageResponse(html(result.body), options);
};
Enter fullscreen mode Exit fullscreen mode

Repo: GitHub
Demo: Vercel Bun

SvelteKit Vercel Bun

Deno Deployments

Netlify and Supabase Edge Functions currently use Deno under the hood.

Deno has an ImageResponse that works in Supabase and should equally work for Netlify deployments.

I didn't create a demo, but you shouldn't have any issues.

Other Frameworks

I have not tested anything on SolidStart, TanStack Start, or React Router. I am personally not a React fan, but it is probably easier to get them to work than other frameworks. SolidStart uses Nitro, so it should be similar. Qwik 2 will probably use Nitro when it is released as well.

Please let me know if you test other frameworks in non-Node environments, and I will post it here.

Vercel Edge Sunsetting?

It has recently come to my attention that Vercel Edge is going away, sort of. SvelteKit and Nitro have marked the Edge option as soon to be depreciated.

What is interesting to me, is there is nothing in NextJS about depreciation, BUT there is a recommendation to migrate away from the Edge Runtime.

According to a Vercel team member, it will probably never go away, but will just stick in maintenance mode... which maybe the same thing.

Summary

You can now deploy everywhere the og/image, there just may be a few configurations.

Thank you Deo for your hard work on this and helping me debug!

J

Top comments (0)