ScreenshotNeo

BlogHow-to

Generate Social Images for a SvelteKit Developer Blog

Generate a share image for every SvelteKit blog post with a reusable template, post data, and a server route. Choose build-time or request-time rendering based on your content and deployment.

By the ScreenshotNeo team4 October 202610 min read

Generate social images for a SvelteKit blog by creating a server route that renders a reusable image template with data for the requested post. The @ethercorps/sveltekit-og documentation describes rendering a Svelte component or raw HTML through Satori and Resvg. You can pre-render images for known posts at build time or generate them on request, depending on when your post data is available and whether your deployment runtime supports the renderer.

This guide builds a raw-HTML route, explains the component alternative, and covers build-time generation, deployment, metadata, crawler access, and common failure cases. The documented setup uses WebAssembly, so follow the current package instructions for your SvelteKit version and adapter.

1. Choose when and how to render each image

There are two decisions: what you use to author the template, and when the image is generated. They are independent: a template can be rendered at build time or requested later if your route and deployment support that pattern.

Decision Choose this when Consider
Svelte component You want to keep the template in Svelte syntax and organize reusable markup as a component. The renderer is not a full browser. Keep the component within the supported rendering model; if using a style block, enable CSS injection as the guide specifies.
Raw HTML A compact template with interpolated post data is enough. Escape values before inserting them into markup. The documented example returns a 1200 × 630 image; that is an example, not a universal platform standard.
Build time Post paths and metadata are known during the build. Pre-render one image for each known post. New or unlisted paths need a later build or a supported runtime route.
Request time Post data is looked up when a request arrives, or paths are not known in advance. Your server or deployment runtime must support the renderer and its WebAssembly setup.

The documentation describes both template approaches and pre-rendering. It does not provide a benchmark that establishes which renders faster, nor does it establish compatibility with every SvelteKit adapter. Choose based on your data model and verify the setup on your target runtime. Svelte component usage, raw HTML usage, pre-rendering.

2. Install and configure the renderer

Follow the current getting-started guide for the versions of SvelteKit, Vite, and the renderer you use. The documented setup installs the core package and adds a Vite plugin to handle WebAssembly bundling. Older Rollup configurations have a separate path; the guide says the Rollup plugin is expected to be deprecated in a future major release. Restart the development server after changing plugin configuration.

The exact package versions and configuration syntax can change, so use the version-specific instructions rather than copying an unpinned snippet from an older project. Check the framework and adapter configuration before debugging the route itself.

3. Add a server route that renders a post

Create a route such as src/routes/og/[slug]/+server.ts. The example below shows the route shape and a self-contained HTML template. Adapt the renderer import and options to the installed version’s usage guide; this example uses the documented HTML-string rendering approach and 1200 × 630 example dimensions.

// src/routes/og/[slug]/+server.ts
import { error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { Resvg } from '@resvg/resvg-js';
import { html } from 'satori-html';
import satori from 'satori';

// Replace this with your content loader or database query.
const posts: Record<string, { title: string; author: string }> = {
  'getting-started': { title: 'Getting started with SvelteKit', author: 'The Example Blog' },
  'dynamic-routes': { title: 'Understanding dynamic routes', author: 'The Example Blog' }
};

function escapeHtml(value: string): string {
  return value.replace(/[<>&"']/g, (char) => ({
    '<': '&amp;', '>': '&gt;', '&': '&amp;',
    '"': '&quot;', "'": '&#39;'
  })[char] ?? char);
}

export const GET: RequestHandler = async ({ params }) => {
  const post = posts[params.slug];
  if (!post) throw error(404, 'Post not found');

  const markup = html(`
    <div style="display:flex;width:1200px;height:630px;background:#101827;color:#f8fafc;padding:64px;font-family:Inter;flex-direction:column;justify-content:space-between">
      <div style="font-size:24px;color:#93c5fd">${escapeHtml(post.author)}</div>
      <div style="font-size:64px;font-weight:700;line-height:1.1">${escapeHtml(post.title)}</div>
      <div style="font-size:20px;color:#cbd5e1">Developer blog</div>
    </div>
  `);

  const svg = await satori(markup, {
    width: 1200,
    height: 630,
    fonts: [
      // Supply a font buffer in the format required by your installed Satori version.
      // Use a bundled or otherwise reliably available font; do not assume system fonts exist.
    ]
  });
  const png = new Resvg(svg).render().asPng();

  return new Response(png, {
    headers: {
      'Content-Type': 'image/png',
      'Cache-Control': 'public, max-age=0, s-maxage=86400'
    }
  });
};

Important: this is a route skeleton, not a drop-in package installation recipe. The renderer’s documented API and font requirements depend on the package version. Complete the font configuration and imports using its current setup and HTML usage pages before deploying. The template itself can be replaced by your site’s post loader; keep the missing-post case explicit and return an image content type for successful responses.

For production, source post data from the same content collection or database used by your blog. Avoid maintaining a second hand-written list that can drift from published posts. If titles can contain user-controlled or imported text, escape them before interpolation. A title can also be too long for the available space: define a deliberate line-wrap or truncation policy and check representative posts.

4. Put the image URL in each post’s metadata

Use an absolute, publicly reachable URL for the generated image. In SvelteKit, emit page metadata from the post page or shared layout using the actual post slug and your canonical site origin.

<svelte:head>
  <meta property="og:title" content={post.title} />
  <meta property="og:description" content={post.description} />
  <meta property="og:image" content={`${siteOrigin}/og/${post.slug}`} />
  <meta property="og:image:alt" content={`Social image for ${post.title}`} />
  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:image" content={`${siteOrigin}/og/${post.slug}`} />
</svelte:head>

Use a configured canonical origin rather than deriving a public URL from an untrusted request host. Set metadata for each post, and ensure the image route does not require a logged-in session or browser-only cookies. Social crawlers need to fetch the image URL directly.

The 1200 × 630 dimensions above follow an implementation example; do not assume they are the current universal requirement for every network. Check the current documentation for the platforms you target and inspect the deployed preview using their own debugging tools.

5. Pre-render known post images when practical

If all post slugs and metadata exist during the build, generate the known images as part of the build and serve them as static files. The library documents pre-rendering images for known routes using build-time page data, while retaining runtime generation for an unlisted path in its described setup.

In practice, collect the canonical set of published post paths, feed each post’s data to the same template, and write the resulting image to a stable public path. Keep the route or fallback behavior aligned with your content policy: a removed post should not silently produce a stale image, and a newly published post must not point to a file that was never generated. Build-time rendering requires the paths and data to be available during the build.

Use request-time generation when the data is only available at request time or when a build for every content change is unsuitable. That choice depends on a working server or supported runtime. The documentation does not supply comparative latency, cost, or reliability measurements, so base the decision on your deployment constraints rather than assumed performance.

6. Check deployment, caching, and crawler access

The rendering pipeline uses WebAssembly. The project publishes adapter-specific instructions for Vercel and Cloudflare. Follow the page for your adapter and runtime, and verify the built deployment: local development success alone does not establish that WebAssembly assets bundle or execute correctly in production.

For request-time images, caching can avoid recomputing an image for repeated requests when the post data has not changed. Pick a cache lifetime that matches how often titles and artwork are edited, and plan how a changed post invalidates or outlives a cached result. The sample response header is an illustration of a cache policy, not a universal setting.

Vercel’s OG-image guidance recommends edge caching computed images and allowing social-media providers to fetch image API routes in robots.txt. Treat that as Vercel-specific guidance, and verify crawler access with your host and target platforms. A crawler blocked by robots rules, authentication, firewall rules, or a non-public route cannot retrieve the image. Vercel OG image generation guidance.

7. Troubleshoot common failures

Symptom Likely cause Fix
Build fails while loading a WebAssembly module The bundler plugin or adapter-specific setup is missing, outdated, or incompatible with the current configuration. Recheck the current getting-started and adapter instructions, ensure the Vite plugin is configured as documented, and restart the dev server after plugin changes.
Works locally but fails after deployment The target adapter or runtime is not configured for the renderer’s WebAssembly pipeline. Use the renderer’s adapter-specific setup where available and inspect the deployed function/build logs. Do not assume every adapter is supported.
Image route returns 404 The slug is absent, differs in case, or is not included in the build-time path list. Use the canonical slug source, return a deliberate 404 for missing posts, and regenerate static outputs when publishing new content.
Social preview is missing while the page looks correct The metadata URL may be relative, private, blocked, or inaccessible to a crawler. Use an absolute public URL, check robots and host access rules, and fetch the image route without a browser session.
Generated image has missing or incorrect font glyphs The rendering environment may not have the expected font available, or the provided font data may not include the glyph. Bundle a font in the documented format and test titles with the punctuation and scripts your posts use.
Long title overflows or looks cramped Variable post titles exceed the template’s intended layout. Test long and short titles, tune font size and line height, and define a wrap or truncation approach that preserves meaning.
Image shows stale post data A cached image remains after the source metadata changed. Adjust cache lifetime or invalidate the relevant cache when content changes; ensure pre-rendered images are rebuilt.

8. Reliability, performance, and cost considerations

  • Data consistency: generate from the canonical post record and use the same slug for the page, metadata, and image route.
  • Rendering reliability: verify WebAssembly bundling on the production adapter. Keep a deployment check that requests a known image URL after publishing.
  • Rendering work: pre-rendering avoids doing the render on each image request for known posts; request-time generation computes images when requested. Actual latency and resource cost depend on your implementation and host; the cited docs provide no benchmark.
  • Caching: cache stable output, but match expiry or invalidation to how quickly post content changes.
  • Costs: build-time work consumes build resources; request-time work consumes runtime resources, and caching may reduce repeated work. Check your host’s current pricing and limits; no provider cost comparison is established here.
  • Format and platform behavior: verify deployed response dimensions, content type, and preview rendering on the platforms you use. Platform-specific requirements were not verified for this guide.

9. Or skip the browser setup

If your immediate need is a screenshot of a public page, ScreenshotNeo provides a website screenshot API and MCP server. It takes a URL and returns a PNG, JPEG, WebP, or PDF. Its API is for capturing rendered web pages; it does not replace the reusable, post-data-driven OG template built above.

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. 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.

Sign up for ScreenshotNeo and get 1,000 free screenshots a month with no card.

10. FAQ

Can I generate an image for a post that does not exist yet?

Only if your route has data for it. Otherwise return a 404 or use a deliberate fallback; avoid generating a misleading image from an unknown slug.

Should the image route use a browser screenshot?

The documented SvelteKit OG approach renders a template through Satori and Resvg. A full browser is not required for that flow, and arbitrary browser DOM behavior should not be assumed.

Can I use one image for every social network?

You can point multiple metadata tags at one image, but confirm the current dimensions and format expectations for each platform you target.

Is pre-rendering always faster or cheaper?

The sources provide no benchmark or universal cost comparison. Pre-render when your known routes and build workflow suit it; verify resource limits for your own host.

Primary references