The svelte/server module provides server-side rendering (SSR) capabilities for Svelte applications. This module is only available on the server and when compiling with the server option.
Renders a Svelte component to HTML on the server.
function render<Comp, Props>(
component: Comp,
options?: {
props?: Props;
context?: Map<any, any>;
idPrefix?: string;
csp?: Csp;
}
): RenderOutputParameters:
getContext() at the component levelReturns: A RenderOutput object containing the rendered HTML and CSP hashes
Description:
The render() function performs server-side rendering of a Svelte component, generating HTML that can be sent to the client. The function returns a special object that can be used both synchronously and asynchronously, allowing you to handle components with or without async operations.
Usage Examples:
import { render } from 'svelte/server';
import App from './App.svelte';
const result = render(App, {
props: { name: 'World' }
});
console.log(result.body); // '<div>Hello World</div>'
console.log(result.head); // '<style>...</style>'import { render } from 'svelte/server';
import App from './App.svelte';
const context = new Map();
context.set('theme', 'dark');
context.set('user', { id: 123, name: 'Alice' });
const result = render(App, {
props: { title: 'Dashboard' },
context
});import { render } from 'svelte/server';
import App from './App.svelte';
// If your component has async operations (data fetching, etc.),
// you can await the render result
const result = await render(App, {
props: { userId: 123 }
});
const html = `
<!DOCTYPE html>
<html>
<head>
${result.head}
</head>
<body>
${result.body}
</body>
</html>
`;import { render } from 'svelte/server';
import App from './App.svelte';
import crypto from 'crypto';
// Generate a nonce for this request
const nonce = crypto.randomBytes(16).toString('base64');
const result = render(App, {
props: { data: myData },
csp: { nonce }
});
const html = `
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy"
content="script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'">
${result.head}
</head>
<body>
${result.body}
</body>
</html>
`;import { render } from 'svelte/server';
import App from './App.svelte';
const result = await render(App, {
props: { data: myData },
csp: { hash: true }
});
// Generate CSP header with hashes
const scriptHashes = result.hashes.script.join(' ');
const cspHeader = `script-src ${scriptHashes}`;
// Send CSP header in HTTP response
response.setHeader('Content-Security-Policy', cspHeader);import { createServer } from 'http';
import { render } from 'svelte/server';
import App from './App.svelte';
const server = createServer(async (req, res) => {
try {
const result = await render(App, {
props: {
path: req.url,
query: new URLSearchParams(req.url.split('?')[1])
}
});
res.writeHead(200, {
'Content-Type': 'text/html',
'Content-Length': Buffer.byteLength(result.body)
});
res.end(`
<!DOCTYPE html>
<html>
<head>
${result.head}
</head>
<body>
${result.body}
<script type="module" src="/app.js"></script>
</body>
</html>
`);
} catch (error) {
res.writeHead(500);
res.end('Internal Server Error');
}
});
server.listen(3000);import express from 'express';
import { render } from 'svelte/server';
import App from './App.svelte';
const app = express();
app.get('*', async (req, res) => {
try {
const result = await render(App, {
props: {
url: req.url,
user: req.user
}
});
res.send(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
${result.head}
</head>
<body>
<div id="app">${result.body}</div>
<script type="module" src="/client.js"></script>
</body>
</html>
`);
} catch (error) {
console.error('SSR error:', error);
res.status(500).send('Server Error');
}
});
app.listen(3000);After rendering on the server, you need to hydrate the component on the client:
Server (server.js):
import { render } from 'svelte/server';
import App from './App.svelte';
export async function handleRequest(req, res) {
const result = await render(App, {
props: { initialData: await fetchData() }
});
res.send(`
<!DOCTYPE html>
<html>
<head>${result.head}</head>
<body>
<div id="app">${result.body}</div>
<script type="module" src="/client.js"></script>
</body>
</html>
`);
}Client (client.js):
import { hydrate } from 'svelte';
import App from './App.svelte';
// Hydrate the server-rendered HTML
hydrate(App, {
target: document.getElementById('app'),
props: { initialData: window.__INITIAL_DATA__ }
});In SvelteKit, server-side rendering is handled automatically, but you can use render() for custom rendering scenarios:
// +page.server.js
import { render } from 'svelte/server';
import EmailTemplate from './EmailTemplate.svelte';
export async function POST({ request }) {
const data = await request.json();
// Render component to HTML string (e.g., for emails)
const result = render(EmailTemplate, {
props: {
userName: data.userName,
orderNumber: data.orderNumber
}
});
// Send email with rendered HTML
await sendEmail({
to: data.email,
subject: 'Order Confirmation',
html: result.body
});
return new Response('Email sent');
}Notes:
render() function returns a special object that works both synchronously and as a Promiseawait the resulthead and body properties that contain the rendered HTMLhtml property is deprecated; use body insteadonMount) do not executenonce and hash are not both specified in CSP optionsThe result of server-side rendering a component.
interface RenderOutput extends PromiseLike<SyncRenderOutput> {
head: string;
body: string;
html: string; // @deprecated use 'body' instead
hashes: {
script: Sha256Source[];
};
}Properties:
<head> element (styles, meta tags, etc.)body; use body instead'sha256-...'Description:
The RenderOutput type represents the result of calling render(). It's both a regular object with accessible properties and a Promise-like object that can be awaited for async rendering.
The object uses lazy evaluation: accessing head or body triggers synchronous rendering, while awaiting the object performs asynchronous rendering if the component has async operations.
Usage Examples:
import { render } from 'svelte/server';
import App from './App.svelte';
const result = render(App);
// Access properties directly
console.log(result.head); // '<style>.svelte-xyz{...}</style>'
console.log(result.body); // '<div class="svelte-xyz">...</div>'import { render } from 'svelte/server';
import AsyncApp from './AsyncApp.svelte';
// Await the result for components with async operations
const result = await render(AsyncApp, {
props: { userId: 123 }
});
console.log(result.body); // Fully resolved HTML with async dataimport { render } from 'svelte/server';
import App from './App.svelte';
const result = await render(App, { props: { title: 'My App' } });
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
${result.head}
</head>
<body>
<div id="root">${result.body}</div>
<script type="module" src="/bundle.js"></script>
</body>
</html>
`;import { render } from 'svelte/server';
import App from './App.svelte';
const result = await render(App, {
csp: { hash: true }
});
// Build CSP header from hashes
const cspScripts = result.hashes.script.join(' ');
const cspHeader = `script-src 'self' ${cspScripts}`;
console.log(cspHeader);
// 'script-src 'self' sha256-abc123... sha256-def456...'import { render } from 'svelte/server';
import App from './App.svelte';
const result = render(App);
// ✅ Recommended: Use 'body'
console.log(result.body);
// ❌ Deprecated: Avoid 'html'
// console.log(result.html); // This still works but is deprecatedNotes:
RenderOutput is both an object and Promise-like, allowing flexible usage patternshead or body properties triggers synchronous rendering on first accesshashes.script array is only populated when csp.hash is enabledhtml property is deprecated in favor of body for clearer semanticsConfiguration options for Content Security Policy when rendering.
interface Csp {
nonce?: string;
hash?: boolean;
}Properties:
Description:
The Csp interface configures Content Security Policy (CSP) behavior during server-side rendering. CSP helps prevent cross-site scripting (XSS) attacks by controlling which scripts can execute on your page.
Svelte provides two CSP strategies:
Important: You cannot use both nonce and hash simultaneously; render() will throw an error if both are specified.
Usage Examples:
The nonce strategy generates a unique token for each request and adds it to all inline scripts and styles:
import { render } from 'svelte/server';
import crypto from 'crypto';
import App from './App.svelte';
// Generate a cryptographically secure nonce
const nonce = crypto.randomBytes(16).toString('base64');
const result = render(App, {
props: { data: myData },
csp: { nonce }
});
// Build HTML with CSP meta tag
const html = `
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy"
content="script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'">
${result.head}
</head>
<body>
${result.body}
</body>
</html>
`;
// Or set CSP via HTTP header
response.setHeader(
'Content-Security-Policy',
`script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'`
);The hash strategy calculates SHA-256 hashes of inline content:
import { render } from 'svelte/server';
import App from './App.svelte';
const result = await render(App, {
props: { data: myData },
csp: { hash: true }
});
// Extract script hashes
const scriptHashes = result.hashes.script.join(' ');
// Build CSP header
const cspHeader = `script-src 'self' ${scriptHashes}; style-src 'self' 'unsafe-inline'`;
// Set CSP via HTTP header
response.setHeader('Content-Security-Policy', cspHeader);import express from 'express';
import { render } from 'svelte/server';
import crypto from 'crypto';
import App from './App.svelte';
const app = express();
app.get('*', async (req, res) => {
// Generate nonce for this request
const nonce = crypto.randomBytes(16).toString('base64');
const result = await render(App, {
props: { url: req.url },
csp: { nonce }
});
// Set CSP header
res.setHeader(
'Content-Security-Policy',
`default-src 'self'; script-src 'self' 'nonce-${nonce}'; style-src 'self' 'nonce-${nonce}'`
);
res.send(`
<!DOCTYPE html>
<html>
<head>${result.head}</head>
<body>${result.body}</body>
</html>
`);
});import { createServer } from 'http';
import { render } from 'svelte/server';
import App from './App.svelte';
const server = createServer(async (req, res) => {
const result = await render(App, {
csp: { hash: true }
});
// Build CSP with hashes
const scriptSources = ['self', ...result.hashes.script];
const csp = `script-src ${scriptSources.join(' ')}; object-src 'none'`;
res.writeHead(200, {
'Content-Type': 'text/html',
'Content-Security-Policy': csp
});
res.end(`
<!DOCTYPE html>
<html>
<head>${result.head}</head>
<body>${result.body}</body>
</html>
`);
});
server.listen(3000);In SvelteKit, you can configure CSP in your hooks:
// src/hooks.server.js
import { dev } from '$app/environment';
import crypto from 'crypto';
export async function handle({ event, resolve }) {
// Generate nonce for CSP
const nonce = crypto.randomBytes(16).toString('base64');
// Make nonce available to pages
event.locals.nonce = nonce;
const response = await resolve(event, {
transformPageChunk: ({ html }) => {
// Inject nonce into script tags if needed
return html;
}
});
// Set CSP header
if (!dev) {
response.headers.set(
'Content-Security-Policy',
`script-src 'self' 'nonce-${nonce}'; style-src 'self' 'nonce-${nonce}'`
);
}
return response;
}Use Nonce when:
Use Hash when:
Comparison:
// ❌ Invalid: Cannot use both
const result = render(App, {
csp: {
nonce: 'abc123',
hash: true // Error: Cannot specify both nonce and hash
}
});
// ✅ Valid: Nonce only
const result1 = render(App, {
csp: { nonce: 'abc123' }
});
// ✅ Valid: Hash only
const result2 = render(App, {
csp: { hash: true }
});
// ✅ Valid: No CSP
const result3 = render(App, {
csp: undefined
});Security Best Practices:
Generate secure nonces: Use cryptographically secure random values
const nonce = crypto.randomBytes(16).toString('base64');Use strict CSP policies: Avoid 'unsafe-inline' and 'unsafe-eval'
script-src 'self' 'nonce-xyz'; object-src 'none'Regenerate nonces per request: Never reuse nonces across requests
Include all necessary directives: Configure script-src, style-src, and other directives
Test your CSP: Monitor CSP violations in production
Content-Security-Policy-Report-Only: ...; report-uri /csp-reportNotes:
nonce and hash simultaneously throws an errorCsp type currently only includes nonce and hash, but may be extended in future versionsServer-side rendering in Svelte follows these steps:
render() function executes the component in a server environmentWhen rendering on the server, certain lifecycle behaviors differ from client-side:
Functions that DO run on server:
onDestroy - Cleanup operationsgetContext / setContext - Context managementFunctions that DON'T run on server:
onMount - Browser-only lifecyclebeforeUpdate / afterUpdate - No DOM updates on servertick() - Returns immediately without waitingwindow, document, etc. are not availableExample:
<script>
import { onMount, onDestroy, getContext } from 'svelte';
// ✅ Runs on both server and client
const theme = getContext('theme');
console.log('Component initializing');
// ✅ Runs on server (cleanup)
onDestroy(() => {
console.log('Cleanup');
});
// ❌ Only runs on client
onMount(() => {
console.log('Mounted to DOM');
return () => {
console.log('Unmounting from DOM');
};
});
// ✅ Runs on server, but needs checking
let isServer = typeof window === 'undefined';
if (!isServer) {
// Client-only code
window.addEventListener('resize', handleResize);
}
</script>Components can fetch data during SSR using async/await:
<script>
// This will be fetched during SSR
async function loadData() {
const response = await fetch('/api/data');
return response.json();
}
const data = $derived(await loadData());
</script>
{#await loadData()}
<p>Loading...</p>
{:then data}
<div>{data.title}</div>
{/await}Server:
// Await the render to ensure data is fetched
const result = await render(App);Svelte 5 supports streaming server-side rendering for better performance:
import { render } from 'svelte/server';
import { Readable } from 'stream';
import App from './App.svelte';
export async function handleRequest(req, res) {
// Start rendering
const result = render(App, { props: { url: req.url } });
res.writeHead(200, { 'Content-Type': 'text/html' });
// Stream the initial HTML
res.write(`
<!DOCTYPE html>
<html>
<head>${result.head}</head>
<body>
`);
// Await async content
const { body } = await result;
res.write(body);
res.end(`
</body>
</html>
`);
}Proper error handling is crucial for SSR:
import { render } from 'svelte/server';
import App from './App.svelte';
import ErrorPage from './ErrorPage.svelte';
export async function handleRequest(req, res) {
try {
const result = await render(App, {
props: { url: req.url }
});
res.status(200).send(`
<!DOCTYPE html>
<html>
<head>${result.head}</head>
<body>${result.body}</body>
</html>
`);
} catch (error) {
console.error('SSR Error:', error);
// Render error page
const errorResult = render(ErrorPage, {
props: {
message: error.message,
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
}
});
res.status(500).send(`
<!DOCTYPE html>
<html>
<head>${errorResult.head}</head>
<body>${errorResult.body}</body>
</html>
`);
}
}Detect whether code is running on server or client:
export const isServer = typeof window === 'undefined';
export const isBrowser = typeof window !== 'undefined';
// In component
if (isServer) {
// Server-only logic
} else {
// Client-only logic
}Always await async renders: If your component fetches data, await the render() result
const result = await render(App);Use context for server data: Pass server-side data through context
const context = new Map();
context.set('request', { url: req.url, headers: req.headers });
const result = render(App, { context });Guard browser-only code: Check for browser environment before using browser APIs
if (typeof window !== 'undefined') {
window.addEventListener('scroll', handleScroll);
}Implement proper CSP: Use nonce or hash-based CSP for security
const result = render(App, { csp: { nonce: generateNonce() } });Handle errors gracefully: Always catch and handle rendering errors
try {
const result = await render(App);
} catch (error) {
// Render error page or fallback
}Optimize for performance: Cache rendered content when possible
const cache = new Map();
const cacheKey = req.url;
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const result = await render(App);
cache.set(cacheKey, result);Use proper hydration: Ensure client-side hydration matches server HTML
// Client
import { hydrate } from 'svelte';
hydrate(App, { target: document.getElementById('app') });