ScreenshotNeo

BlogHow-to

How to Generate Open Graph Images in SvelteKit

Build dynamic Open Graph images in SvelteKit with SvelteKit OG, prerendering, deployment guidance, troubleshooting, and an API alternative.

By the ScreenshotNeo team29 September 20268 min read

How to Generate Open Graph Images in SvelteKit

Dynamic Open Graph images let every SvelteKit page produce a social preview that matches its title, author, product, or documentation entry. The practical approach is to install @ethercorps/sveltekit-og, configure its Vite plugin, create a +server.ts endpoint, render a Svelte component with ImageResponse, and choose between request-time generation and build-time prerendering.

This guide uses the package’s current v4 path for Svelte 5 projects. It covers a complete implementation, dynamic route parameters, prerendering, deployment constraints, supported template patterns, testing, failure diagnosis, and a hosted alternative when you do not want to maintain image rendering infrastructure.

1. Install SvelteKit OG

Install the package with your project’s package manager:

npm i @ethercorps/sveltekit-og

SvelteKit OG v4 requires Svelte 5 or later. Earlier package versions are unmaintained according to the project documentation. If your project is still on Svelte 4, upgrade Svelte before following the examples or pin a compatible older release while planning that migration.

2. Configure the Vite plugin

For SvelteKit 4.1.0 and later, the project documentation recommends the sveltekitOG() Vite plugin. Add it to vite.config.ts and restart the development server after changing the file.

import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { sveltekitOG } from '@ethercorps/sveltekit-og/vite';

export default defineConfig({
  plugins: [sveltekit(), sveltekitOG()]
});

The Rollup plugin is still documented for SvelteKit 4.0.0, but the project warns that this path is planned for deprecation in SvelteKit OG v5. Check the package documentation when upgrading SvelteKit or the OG package so the plugin matches your versions.

3. Create a reusable Open Graph template

Create a Svelte component that fills the requested canvas. A flexbox-oriented layout is the safest starting point because Satori, the renderer used by SvelteKit OG, supports a subset of HTML and CSS rather than a full browser layout engine.

If the component contains a <style> block, enable CSS injection with the documented option:

<!-- src/lib/OgCard.svelte -->
<svelte:options css='injected' />

<script lang='ts'>
  export let title: string;
  export let description = '';
  export let label = 'Documentation';
</script>

<div class='card'>
  <div class='label'>{label}</div>
  <h1>{title}</h1>
  {#if description}
    <p>{description}</p>
  {/if}
  <div class='brand'>example.com</div>
</div>

<style>
  .card {
    width: 1200px;
    height: 630px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 72px;
    background: linear-gradient(135deg, #111827, #312e81);
    color: white;
    font-family: sans-serif;
  }

  .label { font-size: 28px; color: #c4b5fd; }
  h1 { margin: 24px 0 16px; font-size: 72px; line-height: 1.05; }
  p { max-width: 900px; font-size: 32px; line-height: 1.3; color: #e5e7eb; }
  .brand { margin-top: auto; font-size: 24px; color: #d1d5db; }
</style>

The documented 1200 by 630 dimensions are a useful social-card example, not a universal requirement for every platform. Keep text short enough to survive cropping and test long titles, missing descriptions, and non-Latin characters. Advanced browser CSS, arbitrary external assets, and unsupported layout properties may not render as expected because this pipeline converts supported markup to SVG before rasterizing it.

4. Return an ImageResponse from +server.ts

Add an image endpoint under src/routes. The route imports the component and returns an ImageResponse from its GET handler.

A SvelteKit endpoint turns route data into an Open Graph image response.
A SvelteKit endpoint turns route data into an Open Graph image response.
/* src/routes/og/+server.ts */
import { ImageResponse } from '@ethercorps/sveltekit-og';
import OgCard from '$lib/OgCard.svelte';

export const GET = async () => {
  return new ImageResponse(
    OgCard,
    {
      width: 1200,
      height: 630,
      props: {
        title: 'How to Generate Open Graph Images in SvelteKit',
        description: 'Dynamic social cards rendered by a SvelteKit endpoint.',
        label: 'SvelteKit guide'
      }
    }
  );
};

Open /og in development and inspect the response as an image. The exact option shape can change between package releases, so compare your installed version with the current SvelteKit OG getting-started documentation at the ScreenshotNeo documentation for hosted capture options and with the package’s own documentation for renderer-specific APIs.

5. Generate an image for each page slug

For documentation or blog routes, pass a parameter to the image endpoint. A catch-all route can share the same slug as the page it represents:

/* src/routes/docs/[...slug]/og.png/+server.ts */
import { ImageResponse } from '@ethercorps/sveltekit-og';
import OgCard from '$lib/OgCard.svelte';
import { getDoc } from '$lib/content';

export const GET = async ({ params }) => {
  const doc = await getDoc(params.slug);

  if (!doc) {
    return new Response('Not found', { status: 404 });
  }

  return new ImageResponse(OgCard, {
    width: 1200,
    height: 630,
    props: {
      title: doc.title,
      description: doc.description,
      label: 'Docs'
    }
  });
};

Your page’s metadata can then point to /docs/the-slug/og.png. Keep the data lookup deterministic and handle missing content explicitly; otherwise a failed lookup can become a broken image response or an unhelpful server error.

6. Choose request-time generation or prerendering

Approach Use it when Tradeoff
Request-time Titles, personalization, or content must be resolved when requested Rendering work occurs at request time and depends on runtime support
Build-time The complete set of routes and image data is known during the build Images are static and cheap to serve, but updates require another build

For a single known image, opt into prerendering:

Choose prerendering for known routes or request-time generation for fresh data.
Choose prerendering for known routes or request-time generation for fresh data.
export const prerender = true;

For dynamic route variants, provide entries so SvelteKit can enumerate them. The documented pattern places an og.png route beside a catch-all documentation route and returns an entry for every known slug:

/* src/routes/docs/[...slug]/og.png/+page.ts */
export const entries = async () => {
  const docs = await loadAllDocs();
  return docs.map((doc) => ({ slug: doc.slug }));
};

export const prerender = true;

Generated images become static files. This removes per-request rendering, but a newly published page is not represented until the next build.

7. Deployment and runtime constraints

Test the endpoint with the adapter and runtime you will actually deploy. SvelteKit OG documents a Vercel setup and warns that an Edge function’s total bundle must fit under 1 MB, including Wasm, dependencies, and fonts. A custom font or large renderer dependency can consume that budget quickly.

Cloudflare Pages uses @sveltejs/adapter-cloudflare and SvelteKit request handlers. Do not assume that every adapter provides identical Wasm, font, filesystem, or Node API support. If the renderer fails only after deployment, inspect the adapter’s runtime and switch to a compatible execution target or prerender the images.

8. Fonts, assets, and template limits

  • Prefer local, known assets and keep the template self-contained.
  • Use flexbox and straightforward dimensions; browser-only CSS behavior is not guaranteed.
  • Load custom fonts only when the target runtime can include them within its bundle limits.
  • Guard optional data so undefined values do not become literal text in the image.
  • Use stable colors and explicit dimensions instead of relying on inherited browser defaults.

Satori converts supported markup and CSS to SVG, while Resvg rasterizes that SVG into an output such as PNG or JPEG. This explains why a component can be authored in Svelte while still having stricter layout behavior than a normal page.

9. Testing checklist

  1. Start the development server after changing Vite configuration.
  2. Request a fixed route such as /og and verify the content type and dimensions.
  3. Test a long title, an empty description, a missing slug, Unicode text, and a page with unusual punctuation.
  4. Build with the production adapter and request the deployed endpoint.
  5. Check the social platform’s debugger or preview tool after publishing metadata that references the image URL.
  6. For prerendered routes, confirm that every expected slug appears in the generated output.

10. Troubleshooting common errors

Symptom Likely cause Fix
Plugin is not recognized Vite plugin missing or dev server still running Add sveltekitOG() and restart the server.
Styles are missing Component CSS was not injected Add <svelte:options css='injected' /> and use supported CSS.
Works locally, fails on Edge Bundle, Wasm, font, or runtime incompatibility Inspect the adapter limits, reduce dependencies, or prerender.
Some slugs return 404 Entries do not include every route Return a complete entries list and rebuild.
Image is clipped Root element does not occupy the requested canvas Set explicit width and height and use box-sizing: border-box.
External image is blank Asset is unavailable to the renderer or uses unsupported loading behavior Use a reachable asset supported by the deployment runtime, or embed a local asset.
Social preview remains old Platform cache Verify the endpoint itself, then use the platform’s refresh or debugger mechanism.

11. Performance, reliability, and cost decisions

Build-time generation shifts rendering into the build and serves static files afterward. It is a strong fit for a finite documentation set. Request-time generation keeps content fresh but adds rendering work to each uncached request and makes runtime compatibility part of your reliability plan. These are architectural tradeoffs; the project documentation does not provide a universal rendering benchmark.

Keep templates small, avoid unnecessary fonts, and cache stable responses at your hosting layer when appropriate. If an image depends on frequently changing content, define an invalidation strategy. If it never changes, prerender it. Monitor failed image responses separately from normal page responses because a broken social image can remain unnoticed until a crawler requests it.

Or skip the browser setup

If you need an image of a rendered website rather than a Svelte-native SVG template, ScreenshotNeo provides a single screenshot request. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Each step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers.

Read the API details in the ScreenshotNeo docs. A minimal request looks like this:

curl -G 'https://api.screenshotneo.com/v1/shot' \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp
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)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo also supports full-page capture with lazy images loaded, CSS-element capture, dark mode, device presets, custom viewports, retina scale, PDF output, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable caching, signed links, asynchronous jobs, webhooks, bulk capture for 100 URLs per call, usage reporting, and an OpenAPI specification. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

There are 1,000 free shots each month with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.

12. FAQ

Can I use an HTML string instead of a Svelte component?

Yes. SvelteKit OG supports Svelte components and HTML/CSS templates. A component is usually easier to maintain when several routes share a design.

Do I have to use 1200 by 630?

No. That is the dimensions used by the documented example. Choose dimensions that fit your target social platforms and keep the root element aligned with them.

When should I prerender?

Prerender when route parameters and content can be enumerated during a build and do not need request-time personalization.

Why is Edge deployment especially sensitive?

The documented Vercel Edge limit is 1 MB for the complete function bundle, including Wasm, dependencies, and fonts. Renderer and font choices therefore affect whether the endpoint can deploy.

Can ScreenshotNeo generate a Svelte component’s OG card?

ScreenshotNeo captures rendered web pages. Use the SvelteKit OG endpoint when you want a server-rendered image template; use ScreenshotNeo when a clean screenshot of a page or endpoint is the better output.