Server-Side Rendering (SSR) with TokiForge
TokiForge is fully SSR-safe and works seamlessly with Next.js, Remix, Astro, and other server-rendered frameworks.
SSR Utilities
TokiForge provides a comprehensive SSRUtils class for server-side rendering that prevents FOUC (Flash of Unstyled Content) and enables hydration-safe theme switching.
Quick Start with SSRUtils
import { SSRUtils } from "@tokiforge/core";
// Or from framework packages (v2.4.0+):
// import { SSRUtils } from "@tokiforge/nextjs";
// import { SSRUtils } from "@tokiforge/remix";
// Get theme from cookies
const theme = SSRUtils.getThemeFromCookie(request.headers.get("Cookie"));
// Generate inline CSS and hydration script
const { style, script } = SSRUtils.generateSSRHead(themeConfig, {
theme: theme || "light",
minify: true,
includeHydrationScript: true,
});
// In your HTML
<html data-theme={theme}>
<head>
<style dangerouslySetInnerHTML={{ __html: style }} />
{script && <script dangerouslySetInnerHTML={{ __html: script }} />}
</head>
</html>;SSRUtils API
generateInlineCSS()
Generate inline CSS for a specific theme:
const css = SSRUtils.generateInlineCSS(themeConfig, {
theme: "dark",
selector: ":root",
prefix: "hf",
minify: true,
});generateCriticalCSS()
Generate CSS for multiple themes (enables theme switching without JavaScript):
const css = SSRUtils.generateCriticalCSS(themeConfig, {
themes: ["light", "dark"],
includeThemeSelectors: true, // Uses [data-theme="..."]
minify: true,
});getThemeFromCookie()
Extract theme from cookie string:
const theme = SSRUtils.getThemeFromCookie(
request.headers.get("Cookie"),
"tokiforge-theme"
);generateThemeCookie()
Create Set-Cookie header:
const cookie = SSRUtils.generateThemeCookie("dark", "tokiforge-theme", {
maxAge: 31536000,
path: "/",
sameSite: "Lax",
secure: true,
});
response.headers.set("Set-Cookie", cookie);generateHydrationScript()
Generate script to apply theme before React hydrates:
const script = SSRUtils.generateHydrationScript(
"tokiforge-theme",
"theme",
"light"
);generateSSRHead()
All-in-one helper for SSR. Options (v2.4.0):
theme� Theme name to rendercookieName� Cookie name for theme persistence (default:'tokiforge-theme')cookieMaxAge� Max age in seconds for the theme cookie (optional)includeHydrationScript� Include script to apply theme before hydrationminify� Minify output CSS/script
const { style, script } = SSRUtils.generateSSRHead(themeConfig, {
theme: "light",
cookieName: "tokiforge-theme",
cookieMaxAge: 31536000, // 1 year
includeHydrationScript: true,
minify: true,
});How TokiForge Handles SSR
TokiForge detects the server environment and:
- ? Skips DOM operations during rendering
- ? Safely initializes on client hydration
- ? Preserves theme state during SSR
- ? Handles dynamic imports safely
- ? Supports streaming responses
Next.js with TokiForge
Installation
npm install @tokiforge/core @tokiforge/react @tokiforge/nextjsBasic Setup
app/providers.tsx
"use client";
import { ThemeProvider, useTheme } from "@tokiforge/react";
import { themeConfig } from "@/config/tokens";
export function Providers({ children }: { children: React.ReactNode }) {
return <ThemeProvider config={themeConfig}>{children}</ThemeProvider>;
}app/layout.tsx
import { Providers } from "./providers";
import { themeConfig } from "@/config/tokens";
// Preload theme to prevent flash
export const metadata = {
title: "My App",
colorScheme: "light dark",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
try {
const saved = localStorage.getItem('theme');
const theme = saved || 'light';
document.documentElement.setAttribute('data-theme', theme);
} catch (e) {}
`,
}}
/>
</head>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Components with SSR
"use client";
import { useTheme } from "@tokiforge/react";
import type { DesignTokens } from "@/types/tokens";
export function ThemeSwitcher() {
const { theme, setTheme, availableThemes } = useTheme<DesignTokens>();
return (
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
{availableThemes.map((t) => (
<option key={t} value={t}>
{t}
</option>
))}
</select>
);
}Avoiding Hydration Mismatches
Use the suppressHydrationWarning attribute:
export default function RootLayout({ children }) {
return (
<html suppressHydrationWarning>
<body>{children}</body>
</html>
);
}Performance: Critical CSS
Inline critical theme CSS to prevent FOUC (Flash of Unstyled Content):
import { getCriticalCSS } from "@tokiforge/nextjs";
export default function RootLayout({ children }) {
const criticalCSS = getCriticalCSS(themeConfig, "light");
return (
<html>
<head>
<style dangerouslySetInnerHTML={{ __html: criticalCSS }} />
</head>
<body>{children}</body>
</html>
);
}Remix with TokiForge
Installation
npm install @tokiforge/core @tokiforge/react @tokiforge/remixRoot Route Setup
app/root.tsx
import { json } from "@remix-run/node";
import { Links, Meta, Outlet, Scripts, useLoaderData } from "@remix-run/react";
import { Providers } from "./components/providers";
import { themeConfig } from "./config/tokens";
export async function loader() {
return json({ themeConfig });
}
export default function App() {
const { themeConfig } = useLoaderData<typeof loader>();
return (
<html suppressHydrationWarning>
<head>
<Meta />
<Links />
<script
dangerouslySetInnerHTML={{
__html: `
try {
const saved = localStorage.getItem('theme');
const theme = saved || 'light';
document.documentElement.setAttribute('data-theme', theme);
} catch (e) {}
`,
}}
/>
</head>
<body>
<Providers config={themeConfig}>
<Outlet />
</Providers>
<Scripts />
</body>
</html>
);
}app/components/providers.tsx
import { ThemeProvider } from "@tokiforge/react";
import type { ThemeConfig } from "@tokiforge/core";
export function Providers({
config,
children,
}: {
config: ThemeConfig;
children: React.ReactNode;
}) {
return <ThemeProvider config={config}>{children}</ThemeProvider>;
}Action-Based Theme Switching
// app/routes/theme.tsx
import { json, redirect } from "@remix-run/node";
import type { ActionFunction } from "@remix-run/node";
export const action: ActionFunction = async ({ request }) => {
const formData = await request.formData();
const theme = formData.get("theme");
// Store theme preference (e.g., in session or database)
const cookie = `theme=${theme}; Max-Age=31536000; Path=/`;
return redirect("/", {
headers: {
"Set-Cookie": cookie,
},
});
};
// app/components/theme-switcher.tsx
import { Form } from "@remix-run/react";
export function ThemeSwitcher() {
return (
<Form action="/theme" method="post">
<select name="theme" onChange={(e) => e.currentTarget.form?.submit()}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</Form>
);
}Astro with TokiForge
Installation
npm install @tokiforge/core @tokiforge/astroSetup
src/layouts/Layout.astro
---
import { initializeTheme } from '@tokiforge/astro';
import { themeConfig } from '@/config/tokens';
const theme = initializeTheme(themeConfig);
---
<!doctype html>
<html data-theme={theme.currentTheme}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<style set:html={theme.getInlineCSS()} />
</head>
<body>
<slot />
</body>
</html>Client-Side Theme Switching
---
import ThemeSwitcher from '@/components/ThemeSwitcher.tsx';
---
<ThemeSwitcher client:load />SvelteKit with TokiForge
Installation
npm install @tokiforge/core @tokiforge/svelte @tokiforge/sveltekitSetup
src/routes/+layout.svelte
<script>
import { createThemeStore } from '@tokiforge/svelte';
import { themeConfig } from '$lib/config/tokens';
const { theme, setTheme, availableThemes } = createThemeStore(themeConfig);
</script>
<div data-theme={$theme}>
<select on:change={(e) => setTheme(e.target.value)}>
{#each $availableThemes as t}
<option value={t} selected={$theme === t}>{t}</option>
{/each}
</select>
<slot />
</div>Common SSR Patterns
Theme from User Preferences
// Get theme from request headers, session, or cookie
export async function loader({ request }: LoaderFunctionArgs) {
const cookie = request.headers.get("cookie");
const theme = parseThemeCookie(cookie) || "light";
return json({ theme });
}Defer Critical Rendering
// Next.js
export async function generateMetadata() {
return {
colorScheme: "light dark",
};
}
// Remix
export async function loader() {
return json(
{ data },
{
headers: {
"Cache-Control": "public, max-age=3600",
},
}
);
}Safe Hydration
export function useThemeHydration() {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
if (!isClient) {
return null; // Don't render theme switcher on server
}
return <ThemeSwitcher />;
}Performance Optimization
1. Minimize FOUC (Flash of Unstyled Content)
<!-- Inline critical theme CSS -->
<style>
:root {
--color-primary: #007aff;
--color-background: #ffffff;
}
</style>2. Cache Theme Tokens
// Cache tokens at build time
export const cached = cache(async () => {
const tokens = await generateTokens(themeConfig);
return tokens;
});3. Stream HTML for Faster TTFB
// Remix with streaming
export function RootRoute() {
return (
<html>
<body>
<Suspense fallback={<LoadingSpinner />}>
<ThemeProvider>
<App />
</ThemeProvider>
</Suspense>
</body>
</html>
);
}4. Optimize Bundle Size
// Dynamic import theme provider only on client
const ThemeProvider = dynamic(() => import("@tokiforge/react"), {
ssr: false,
});Troubleshooting SSR
Hydration Mismatch
Error: Hydration failed because the server rendered HTML didn't match the client.Solution: Add suppressHydrationWarning to root element:
<html suppressHydrationWarning>
<body>{children}</body>
</html>Theme Not Persisting
// Save theme in cookies/storage on server
export async function action({ request }: ActionFunction) {
const theme = await request.formData().get("theme");
return json(
{ success: true },
{
headers: {
"Set-Cookie": `theme=${theme}; Path=/; Max-Age=31536000`,
},
}
);
}FOUC (Flash of Unstyled Content)
Solution: Inline critical CSS in <head>:
<head>
<style dangerouslySetInnerHTML={{ __html: criticalCSS }} />
</head>Styles Not Loading
Ensure styles are included in server render:
import { getCSSVariables } from "@tokiforge/core";
const css = getCSSVariables(tokens);
return (
<html>
<head>
<style dangerouslySetInnerHTML={{ __html: css }} />
</head>
</html>
);Best Practices
- Always use
suppressHydrationWarningto prevent mismatch warnings - Inline critical theme CSS to prevent FOUC
- Store theme preference in cookies or database
- Test hydration with production builds
- Use dynamic imports for client-only components
- Cache tokens at build time when possible
- Stream HTML for better performance metrics
- Minify CSS in production builds