HeliumTS is under active development. Expect bugs and breaking changes. If you find any issues, please report them in our GitHub
A stable release is planned for early 2026.
Server-Side Rendering (SSR)
HeliumTS supports server-side rendering for pages that need fully rendered HTML for search engines and social crawlers. Unlike SSG, SSR renders per request and can return personalized content.
Quick Start
Add the "use ssr" directive at the top of any page file:
1"use ssr";23export default function DashboardPage() {4 return <h1>Dashboard</h1>;5}
On each request, HeliumTS renders on the server, injects HTML into index.html, and sends complete markup to the browser. React then hydrates client-side.
Rendering Modes Comparison
| Feature | SSR | SSG | SPA (default) |
|---|---|---|---|
| HTML on first load | Per-request | Build-time | Empty root shell |
| Dynamic per-request data | Yes | No | Client-only |
| Server-side props | getServerSideProps | No | No |
| Auth-protected pages | Yes (skip guard on server) | No | Yes |
| Build step required | No | Yes | No |
| Suitable for SEO | Yes | Yes | Requires crawler JS execution |
Server-Side Props
To fetch data per request, export getServerSideProps from the same page file or from a sidecar page.server.ts file.
Inline Export (Not Recommended)
1"use ssr";23import type { GetServerSideProps } from "heliumts/server";45export const getServerSideProps: GetServerSideProps = async (req) => {6 const user = await db.users.findById(req.params.id);7 return { user };8};910export default function ProfilePage({ user }: { user: { name: string } }) {11 return <h1>Hello, {user.name}</h1>;12}
Sidecar File (Recommended)
Use a sidecar file to avoid mixing component and non-component exports in one module and keep Fast Refresh clean.
1src/pages/2├── profile.tsx3└── profile.server.ts
1// src/pages/profile.server.ts2import type { GetServerSideProps } from "heliumts/server";34export const getServerSideProps: GetServerSideProps = async (req, ctx) => {5 const user = await db.users.findById(req.params.id as string);6 return { user };7};
1// src/pages/profile.tsx2"use ssr";34export default function ProfilePage({ user }: { user: { name: string } }) {5 return <h1>Hello, {user.name}</h1>;6}
GetServerSideProps Type
1import type { GetServerSideProps } from "heliumts/server";
The handler receives req and ctx and may return null, undefined, a plain props object, or a redirect result.
reqincludes method, path, headers, query, and dynamic route params.ctxcontains server context like client IP, normalized headers, and raw incoming request.
Redirects from SSR
1import type { GetServerSideProps } from "heliumts/server";23export const getServerSideProps: GetServerSideProps = async (req, ctx) => {4 const accountStatus = await billing.getStatus(ctx.req.headers.authorization);56 if (accountStatus === "overdue") {7 return {8 redirect: {9 destination: "/billing/overdue",10 statusCode: 307,11 replace: true,12 },13 };14 }1516 return { accountStatus };17};
destination: required redirect target.statusCode: optional HTTP redirect status.permanent: shortcut for defaulting to 308 when no status is set.replace: client history behavior (default true).
Layouts with SSR
Layouts work the same way as with regular pages. If a layout returns null during SSR due to auth guards, the server output will be empty.
Use isSSR() to skip client-only guards on the server while still rendering the full provider tree.
1import { isSSR, useRouter } from "heliumts/client";2import { useEffect } from "react";34export default function AppLayout({ children }: { children: React.ReactNode }) {5 const { isPending, data } = useSession();6 const router = useRouter();78 useEffect(() => {9 if (!data?.session && !isPending) {10 router.push("/login");11 }12 }, [data, isPending, router]);1314 // Skip guard on SSR so providers still wrap the page.15 // Client-side hydration enforces redirect if needed.16 if (!isSSR()) {17 if (isPending) return null;18 if (!data?.session) return null;19 }2021 return (22 <SidebarProvider>23 <main>{children}</main>24 </SidebarProvider>25 );26}
isSSR Utility
1import { isSSR } from "heliumts/client";
Returns true on the server during SSR and false in the browser.
How SSR Works
- Directive scanning discovers pages with
"use ssr". - The server intercepts HTML responses for SSR routes.
- If present,
getServerSidePropsis executed and merged into props. - React renders layouts and page with
renderToString. - Helium injects rendered markup into
<div id="root">. - Serialized props are injected as
window.__HELIUM_SSR_DATA__. - Browser hydration attaches listeners without a full re-render.
Client-Side Navigation
After the first load, Helium fetches props via GET /__helium__/page-props?path=<url> and re-renders client-side without full-page reloads.
Sidecar File Conventions
Helium automatically discovers these sidecar extensions for each SSR page:
| Sidecar File | Example |
|---|---|
| page.server.ts | src/pages/profile.server.ts |
| page.server.tsx | src/pages/profile.server.tsx |
| page.server.js | src/pages/profile.server.js |
| page.server.mts | src/pages/profile.server.mts |
The sidecar must export getServerSideProps. Other exports are ignored.
Browser-Only Dependencies
If imported modules access window, document, or navigator during module evaluation, SSR fails and falls back to the SPA shell.
Load browser-only libraries in a client-only code path:
1"use ssr";23import { useEffect } from "react";45export default function MapPage() {6 useEffect(() => {7 import("leaflet").then(({ default: L }) => {8 L.map("map-container");9 });10 }, []);1112 return <div id="map-container" />;13}
Limitations
- SSR supports dynamic routes; params are available in
req.params. - Rendering is synchronous with
renderToString; streaming SSR is not supported yet. - Providers rendered during SSR must be safe on the server and should move browser side effects into
useEffect.