Server-Side Rendering (SSR)
Paraglide JS provides first-class SSR support through the paraglideMiddleware(). The middleware handles locale detection, URL delocalization, and request isolation automatically.
[!TIP] For middleware setup, framework examples, and troubleshooting, see the Middleware Guide.
Basic Setup
import { paraglideMiddleware } from "./paraglide/server.js";
app.get("*", async (request) => {
return paraglideMiddleware(request, async ({ request, locale }) => {
return new Response(html(request));
});
});Video: How Paraglide's SSR middleware works with locale detection and request isolation.
How Locale Context Works
The middleware uses AsyncLocalStorage (opens in a new tab) (a way to pass context through async calls without explicit parameters) to maintain locale state across async operations. This means getLocale() returns the correct locale even in deeply nested async code:
// Works correctly - locale is preserved through async boundaries
async function fetchUserData() {
const locale = getLocale(); // Returns request-specific locale
return fetch(`/api/users?lang=${locale}`);
}Each concurrent request has its own isolated locale context, preventing race conditions where one request's locale could leak into another.
Hydration
Client-side hydration works automatically. The middleware sets the locale on the server, and the client runtime reads it from the URL or a cookie on hydration.
[!NOTE] Server and client must agree on the locale source. If the server reads locale from the URL but the client reads from
localStorage, hydration mismatches can occur.localStorageis not available during the initial SSR document request, so use a server-visible strategy such ascookiewhen the persisted locale must affect the first response.
Streaming SSR (React 18, Solid)
Streaming SSR frameworks work with Paraglide because AsyncLocalStorage preserves context across stream chunks. The locale set at the start of the request remains available throughout the entire streaming response.
// React 18 streaming example
app.get("*", async (request) => {
return paraglideMiddleware(request, async ({ request, locale }) => {
const stream = await renderToReadableStream(<App />);
return new Response(stream, {
headers: { "Content-Type": "text/html" },
});
});
});[!NOTE] Modern edge runtimes support AsyncLocalStorage. Keep the default on Vercel Edge and on Cloudflare Workers when Node.js compatibility is available. If you target a runtime that still lacks
AsyncLocalStorageornode:async_hooks, see AsyncLocalStorage in the Middleware Guide for the fallback option.
Fallback Behavior
If no strategy resolves a locale (e.g., URL pattern doesn't match, no cookie set), the middleware falls back to your baseLocale. To ensure a locale is always resolved, add baseLocale as the last strategy:
strategy: ["url", "cookie", "baseLocale"];Troubleshooting
Wrong locale on first request: The client may briefly show the wrong locale before hydration completes. This is normal when the server and client resolve from the same source. If it persists, check that server and client use the same strategy order, and avoid relying on localStorage alone for SSR-first locale detection.
Locale not persisting across pages: If using cookie-based persistence, ensure the cookie is being set. Check the Set-Cookie header in your response and verify the cookie domain/path settings.
Hydration mismatch errors: Server and client disagree on the locale. Common causes:
- Different strategy order on server vs client
- Server reads URL, client reads localStorage
- Cached HTML served with stale locale
See the Middleware Guide for more debugging tips.
See Also
- Middleware Guide - Framework examples, troubleshooting, AsyncLocalStorage
- Static Site Generation - Build-time generation of localized pages
- Strategy Configuration - Configure locale detection strategies
- i18n Routing - URL patterns, translated pathnames, domain-based routing