How to Create Dynamic Open Graph Images with SvelteKit
Generate page-specific Open Graph images from a SvelteKit server route, then choose between runtime rendering and build-time prerendering.
To create dynamic Open Graph images with SvelteKit, add a +server.ts endpoint, render a Svelte card component with the page-specific data, and return the image response. This example uses the Sveltekit OG library’s ImageResponse API; image generation is not a built-in SvelteKit API. For a known set of pages whose data is available during the build, prerender the image routes. Use runtime generation when the image needs request-time data or the routes cannot be enumerated at build time.
1. Install and choose the rendering approach
Use Sveltekit OG’s documented ImageResponse API with a SvelteKit server route. Check the library’s current installation instructions and API against your SvelteKit version and deployment adapter before wiring it into production; the renderer and its dependencies must run in your target environment.
First decide when each image should be produced:
| Approach | Use it when | Plan for |
|---|---|---|
| Runtime generation | Content changes between builds, depends on request-time data, or has paths you cannot enumerate at build time. | Renderer compatibility with the deployed runtime, response caching, freshness, and generation failures. |
| Build-time prerendering | The page paths and all data needed for their images are known during the build. | Supplying the dynamic route entries and rebuilding when source content changes. |
Prerendering creates the known image outputs during the build and avoids generating them on their first request. Runtime generation is more flexible for changing or unenumerated data. These are implementation tradeoffs, not a universal performance ranking; measure your own app and hosting setup if performance or cost is a deciding factor.
2. Build the card component
Keep the card design in a Svelte component and pass page data in as props. The example below expects a title, description, and absolute image URL. Adapt the markup and styling to the supported features of the renderer you choose; do not assume every browser CSS feature or browser asset-loading behavior is available during server-side image rendering.
<!-- src/lib/OgCard.svelte -->
<script lang="ts">
export let title: string;
export let description: string;
export let imageUrl: string | undefined = undefined;
</script>
<div style="display:flex; width:1200px; height:630px; padding:64px; box-sizing:border-box; background:#101827; color:#f8fafc; font-family:Inter, sans-serif;">
<div style="display:flex; flex-direction:column; justify-content:center; width:100%;">
<div style="font-size:24px; color:#93c5fd; margin-bottom:20px;">Your site name</div>
<div style="font-size:58px; line-height:1.08; font-weight:700; margin-bottom:22px;">{title}</div>
<div style="font-size:27px; line-height:1.3; color:#cbd5e1;">{description}</div>
{#if imageUrl}
<img src={imageUrl} alt="" style="display:none;" />
{/if}
</div>
</div>
This minimal card keeps the optional image unused in the layout, so remove imageUrl and its markup if your design has no image. If you do display an image, supply it as data or use a public absolute URL the server renderer can access. A path relative to a browser page is not automatically available to a server-side renderer.
3. Return a dynamic image from a SvelteKit route
Create a route such as src/routes/og/[slug].png/+server.ts. Load the record for the slug, reject missing records, and pass the values to ImageResponse. This example shows the route and response shape; connect getPostBySlug to your own content source.
// src/routes/og/[slug].png/+server.ts
import { ImageResponse } from 'sveltekit-og';
import OgCard from '$lib/OgCard.svelte';
import { getPostBySlug } from '$lib/server/posts';
export async function GET({ params }) {
const slug = params.slug;
// Validate the route value before querying a database or content store.
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
return new Response('Invalid slug', { status: 400 });
}
const post = await getPostBySlug(slug);
if (!post) {
return new Response('Post not found', { status: 404 });
}
return new ImageResponse(
OgCard,
{
props: {
title: post.title,
description: post.description,
imageUrl: post.ogImageUrl
},
width: 1200,
height: 630
}
);
}
The ImageResponse constructor and Svelte component pattern are documented by Sveltekit OG. Its API documentation uses 1200 by 630 pixels as an example dimension; treat that as an example to verify against the destinations and design requirements for your pages, not a universal platform requirement. See the library documentation for current constructor options and usage details.
Load fonts and assets for the server renderer
Custom fonts need to be supplied as binary data, such as an ArrayBuffer. Sveltekit OG documents font helpers for loading and resolving font data. Follow the library’s current font-loading example for your runtime and pass the font data through the supported ImageResponse options. The exact loading code depends on where the font lives and what your deployment runtime supports.
For logos or other local image assets, provide image data directly, such as a data URL, or make the asset reachable through a public absolute URL. Check asset loading, font rendering, and the card’s output in the actual rendering library and deployment environment.
4. Point the page metadata at the generated image
The image endpoint only contributes to a page’s share preview if the HTML metadata references its publicly reachable, absolute URL. Set page-specific metadata in the SvelteKit page or layout that owns the content. Keep the image URL and page URL canonical and consistent with your site’s public origin.
<!-- Example in a Svelte page component with loaded `post` data -->
<svelte:head>
<title>{post.title}</title>
<meta property="og:title" content={post.title} />
<meta property="og:type" content="article" />
<meta property="og:url" content={`https://example.com/blog/${post.slug}`} />
<meta property="og:description" content={post.description} />
<meta property="og:image" content={`https://example.com/og/${post.slug}.png`} />
</svelte:head>
Replace example.com with the production origin. Do not emit a relative og:image value: a social preview fetcher needs an absolute image URL it can request. Validate the rendered HTML and make sure the endpoint is publicly accessible from outside your logged-in session.
5. Prerender a finite set of image routes
If all image paths and their data are available during the build, Sveltekit OG documents setting export const prerender = true and defining entries for dynamic paths. The exact entry-loading mechanism depends on your route and content source. For example, if your post slugs are available from a build-time content module:
// src/routes/og/[slug].png/+server.ts
import { ImageResponse } from 'sveltekit-og';
import OgCard from '$lib/OgCard.svelte';
import { getPostBySlug, listPostSlugs } from '$lib/server/posts';
export const prerender = true;
export async function entries() {
return (await listPostSlugs()).map((slug) => ({ slug }));
}
export async function GET({ params }) {
const post = await getPostBySlug(params.slug);
if (!post) return new Response('Post not found', { status: 404 });
return new ImageResponse(OgCard, {
props: {
title: post.title,
description: post.description,
imageUrl: post.ogImageUrl
},
width: 1200,
height: 630
});
}
Here listPostSlugs and getPostBySlug stand for your own build-accessible content functions. Confirm that the route entries are returned in the shape expected by your SvelteKit version. When a post is added, removed, or edited, rebuild the prerendered output so the generated card stays in sync. SvelteKit’s adapter documentation explains how build output is adapted for deployment; it does not establish a compatibility result for every image renderer or hosting provider.
6. Verify the complete share-preview path
- Start the app with the deployment mode you intend to use.
- Request an existing image URL directly and confirm it returns an image rather than an HTML error page.
- Request a malformed slug and a valid but missing slug; confirm the route returns the intended 400 and 404 responses.
- Open the page HTML and inspect its
og:imagevalue. Confirm it is absolute, publicly reachable, and matches the slug. - Check the generated card for long titles, absent descriptions, optional assets, and the fonts used by the production build.
- For prerendering, verify that every required route is emitted and rebuild after content changes.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The image URL returns 404. | The slug is missing, route entries were not generated, or the page points at the wrong path. | Check the route pattern and production URL. For prerendering, enumerate every required slug and ensure its data exists at build time. |
| The endpoint returns an HTML error instead of an image. | The route threw while loading data or rendering, or the deployed runtime cannot run the renderer. | Inspect server logs, handle missing records, and check the renderer and its dependencies against the SvelteKit adapter and target runtime. |
| The social preview has no image. | The page metadata is missing, uses a relative URL, or points to an inaccessible endpoint. | Inspect the page’s rendered og:image tag, use the production absolute URL, and request it without authentication. |
| The font falls back or text layout changes. | Font bytes were not supplied or the renderer cannot resolve the font asset. | Load the font as binary data using the renderer’s supported helper and verify it in the deployed environment. |
| A logo or image is absent. | The renderer cannot resolve a browser-relative asset path. | Pass image data, such as a data URL, or use a public absolute URL accessible to the server renderer. |
| Some styles do not appear as expected. | The selected image renderer does not support the CSS or browser behavior used by the component. | Simplify the card styling to supported features and validate output with the selected library rather than assuming full browser rendering. |
| A prerendered image shows old content. | Static output was generated before the page data changed. | Rebuild and redeploy, or choose runtime generation and define a cache and revalidation policy that fits the content. |
8. Performance, reliability, and cost
Prerendering moves image work to the build for the routes you enumerate, while runtime generation makes the result depend on a request-time rendering path. The research sources do not provide a host-by-host compatibility matrix, benchmark, or universal cache policy, so check the library, fonts, assets, adapter, and target runtime together. For mutable content, choose how freshness, caching, and invalidation should work before release. Measure build time and request behavior in your app if they affect your hosting cost or latency; there is no general performance figure to apply here.
Validate data before rendering so missing or malformed content does not produce a plausible but incorrect card. Keep the data-loading path bounded and handle absent records explicitly. Test the production adapter because local development alone does not establish that the renderer and all its dependencies will work after deployment.
Or skip the browser setup
If your goal is to capture a page as an image or PDF rather than generate a branded, data-driven Open Graph card, ScreenshotNeo offers a one-call website screenshot API. It does not replace the SvelteKit card component and metadata flow above; it can capture a rendered page URL.
Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.
cURL:
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 API documentation for request options, then sign up for 1,000 free screenshots a month with no card.
FAQ
Is dynamic image generation built into SvelteKit?
This guide uses the Sveltekit OG library’s ImageResponse API in a SvelteKit server route. The implementation claims here are specific to that library, not a native SvelteKit image-generation API.
Can I use a Svelte component as the image design?
The documented API accepts a Svelte component or raw HTML. Pass the page-specific values as props and confirm that the component’s styling and assets work in the renderer.
Should I generate images at request time or during the build?
Use build-time prerendering when you can enumerate paths and data during the build. Use runtime generation when output depends on request-time data or cannot be enumerated then.
Can the route use a relative image or font path?
Do not assume browser-relative paths work in server rendering. Supply font bytes and make local images available as data or through a public absolute URL.


