September 4, 2026

Automate social images with SvelteKit server routes

The RenderFast Team

Anyone who posts on LinkedIn regularly knows the drill. Open a design tool, duplicate a slide, swap the headline, export a PNG, drag it into the composer. Repeat that five times a week and you’ve spent hours on layout instead of writing.

This tutorial builds a small internal tool that removes the design tool from the loop. You pick a template, fill in a form, get a live preview, and download a finished PNG ready to attach to your next post. The glue between your SvelteKit app and the RenderFast rendering API is a single +server.ts route, the same pattern you’d use for any authenticated API integration.

If you’d rather wire this up with SvelteKit’s newer remote functions instead of a classic API route, there’s a companion post linked at the end that does exactly that.

What you’ll build

A one-page app with:

  • A form for the text fields your template exposes (title, subtitle, whatever you named your layers)
  • A server route that calls the RenderFast /api/generate endpoint with your API key, which stays on the server the whole time
  • A preview of the rendered image and a download link

Nothing here touches a database. It’s a thin, safe proxy between your form and RenderFast’s render endpoint.

Prerequisites

  • Node.js 20 or newer
  • A RenderFast account with a template you can render (see the next section if you don’t have one yet)
  • An API key from App → API Keys in your RenderFast dashboard

You don’t need a database, a queue, or a cron job for this. The whole point of routing everything through a template is that the layout work is already done, your app just needs to fill in a few values and fetch the result.

Set up a template in RenderFast

Open Templates → New Template → Browse Starter Templates and clone one that fits a social post, something in the 1200x630 range works well. Give the layers you want to edit from your form semantic names like title and subtitle, since that’s how you’ll target them in the render request. Save the template and copy its Template ID from the editor URL.

Create the SvelteKit project

Scaffold a fresh project if you don’t already have one to bolt this onto:

npx sv create social-image-tool
cd social-image-tool
npm install

Add your RenderFast API key as a server-only environment variable:

echo "RENDERFAST_API_KEY=your_api_key_here" >> .env

Because the variable has no PUBLIC_ prefix, SvelteKit keeps it out of any client bundle. That matters here: this key can render images against your account, so it should never reach the browser.

Build the server route

Create src/routes/api/social-image/+server.ts. This route accepts a template ID and a set of layer overrides from the client, attaches your API key, and forwards the request to RenderFast:

// src/routes/api/social-image/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { RENDERFAST_API_KEY } from '$env/static/private';

const RENDERFAST_ENDPOINT = 'https://yourdomain.com/api/generate';

export const POST: RequestHandler = async ({ request }) => {
	const { templateId, layers } = await request.json();

	if (!templateId) {
		return json({ error: 'templateId is required' }, { status: 400 });
	}

	const response = await fetch(RENDERFAST_ENDPOINT, {
		method: 'POST',
		headers: {
			Authorization: `Bearer ${RENDERFAST_API_KEY}`,
			'Content-Type': 'application/json'
		},
		body: JSON.stringify({
			templateId,
			layers,
			format: 'png'
		})
	});

	const result = await response.json();

	if (!response.ok) {
		return json(result, { status: response.status });
	}

	// result: { success, imageUrl, cached, warnings }
	return json(result);
};

Swap yourdomain.com for the domain your RenderFast instance actually runs on. The layers object mirrors what you’d send directly to /api/generate: each key is a layer name from your template, and its value carries the fields that layer type accepts (text, fontSize, color for text layers, image_url for image layers, and so on). A response with warnings still returns a 200, it just means one of your overrides named a layer that doesn’t exist or carried a field that layer can’t use, worth checking during testing but not a failure.

This route is intentionally thin. It validates almost nothing beyond checking templateId is present, because RenderFast’s own API already returns a 400 with the offending field named when an override is malformed. Your route just needs to pass that error through, which the code above does.

If you’d rather generate JPEG or WebP output instead of PNG, add a quality field alongside format in the request body, RenderFast accepts a value from 1 to 100 for either format and ignores it for PNG. That’s useful if you’re embedding the image somewhere that cares about file size, a lot of scheduling tools compress images anyway, so a smaller source file uploads faster.

Build the UI

Now the page that calls this route. It keeps the form fields in a couple of $state variables, posts to /api/social-image on submit, and shows the rendered image once it comes back:

<!-- src/routes/+page.svelte -->
<script lang="ts">
	const TEMPLATE_ID = 'your-template-id';

	let title = $state('');
	let subtitle = $state('');
	let imageUrl = $state('');
	let generating = $state(false);
	let error = $state('');

	async function generate() {
		generating = true;
		error = '';

		const response = await fetch('/api/social-image', {
			method: 'POST',
			headers: { 'Content-Type': 'application/json' },
			body: JSON.stringify({
				templateId: TEMPLATE_ID,
				layers: {
					title: { text: title },
					subtitle: { text: subtitle }
				}
			})
		});

		const result = await response.json();
		generating = false;

		if (!response.ok) {
			error = result.error ?? 'Something went wrong';
			return;
		}

		imageUrl = result.imageUrl;
	}
</script>

<main class="mx-auto max-w-lg space-y-6 p-8">
	<h1 class="text-xl font-semibold">Social image generator</h1>

	<div class="space-y-3">
		<label class="block">
			<span class="text-sm font-medium">Title</span>
			<input class="w-full rounded border p-2" bind:value={title} placeholder="My Blog Post Title" />
		</label>

		<label class="block">
			<span class="text-sm font-medium">Subtitle</span>
			<input class="w-full rounded border p-2" bind:value={subtitle} placeholder="Published today" />
		</label>

		<button
			class="rounded bg-black px-4 py-2 text-white disabled:opacity-50"
			onclick={generate}
			disabled={generating}
		>
			{generating ? 'Generating…' : 'Generate image'}
		</button>
	</div>

	{#if error}
		<p class="text-sm text-red-600">{error}</p>
	{/if}

	{#if imageUrl}
		<div class="space-y-2">
			<img src={imageUrl} alt="Generated preview" class="w-full rounded border" />
			<a href={imageUrl} download class="text-sm underline">Download PNG</a>
		</div>
	{/if}
</main>

Replace your-template-id with the Template ID you copied earlier. If you want to support more than one template, turn TEMPLATE_ID into a small array of { id, label, fields } objects and render a select above the form, the rest of the flow stays identical since the server route already accepts any templateId you send it.

Test the workflow

Start the dev server:

npm run dev

Open the page, type a title and subtitle, and click Generate image. The request goes to your own /api/social-image route, which attaches the API key and calls RenderFast, and the response comes back with an imageUrl you can preview and download directly. If you see a 400 with a named field in the error, double check that the layer names in your form match the layer names in the template editor exactly, that’s the most common mismatch.

Once it works locally, the same route works in production without changes since the API key never leaves the server.

One thing worth testing deliberately: submit the form with an empty title. Depending on how your template is built, an empty text override either renders a blank layer or falls back to whatever placeholder text you set in the template editor. Neither is wrong, but it’s worth knowing which one your template does before you rely on it for a real post.

Where to go from here

This is deliberately the smallest version that works. From here you could add more templates, run several renders in parallel for a batch of posts, or drop the resulting image straight into a scheduling tool instead of downloading it by hand. RenderFast doesn’t publish anywhere itself, it renders the image, what you do with the file afterward is up to you.

If you want to go further with the API, the quickstart guide covers creating templates and API keys from scratch, and the Generate API reference documents every layer override field. When you’re ready to wire this into your own account, create a RenderFast account and clone a starter template to get your first Template ID.

Prefer the other approach?

If you’d rather skip the manual fetch calls and API route boilerplate, the companion post covers the same tool built with SvelteKit’s remote functions: Automate social images with SvelteKit remote functions. It swaps the +server.ts route and client-side fetch for a typed command() you call directly from the component.