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";
2
3export 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

FeatureSSRSSGSPA (default)
HTML on first loadPer-requestBuild-timeEmpty root shell
Dynamic per-request dataYesNoClient-only
Server-side propsgetServerSidePropsNoNo
Auth-protected pagesYes (skip guard on server)NoYes
Build step requiredNoYesNo
Suitable for SEOYesYesRequires 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.

1"use ssr";
2
3import type { GetServerSideProps } from "heliumts/server";
4
5export const getServerSideProps: GetServerSideProps = async (req) => {
6 const user = await db.users.findById(req.params.id);
7 return { user };
8};
9
10export default function ProfilePage({ user }: { user: { name: string } }) {
11 return <h1>Hello, {user.name}</h1>;
12}

Use a sidecar file to avoid mixing component and non-component exports in one module and keep Fast Refresh clean.

1src/pages/
2├── profile.tsx
3└── profile.server.ts
1// src/pages/profile.server.ts
2import type { GetServerSideProps } from "heliumts/server";
3
4export 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.tsx
2"use ssr";
3
4export 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.

Redirects from SSR

1import type { GetServerSideProps } from "heliumts/server";
2
3export const getServerSideProps: GetServerSideProps = async (req, ctx) => {
4 const accountStatus = await billing.getStatus(ctx.req.headers.authorization);
5
6 if (accountStatus === "overdue") {
7 return {
8 redirect: {
9 destination: "/billing/overdue",
10 statusCode: 307,
11 replace: true,
12 },
13 };
14 }
15
16 return { accountStatus };
17};

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";
3
4export default function AppLayout({ children }: { children: React.ReactNode }) {
5 const { isPending, data } = useSession();
6 const router = useRouter();
7
8 useEffect(() => {
9 if (!data?.session && !isPending) {
10 router.push("/login");
11 }
12 }, [data, isPending, router]);
13
14 // 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 }
20
21 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

  1. Directive scanning discovers pages with "use ssr".
  2. The server intercepts HTML responses for SSR routes.
  3. If present, getServerSideProps is executed and merged into props.
  4. React renders layouts and page with renderToString.
  5. Helium injects rendered markup into <div id="root">.
  6. Serialized props are injected as window.__HELIUM_SSR_DATA__.
  7. 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 FileExample
page.server.tssrc/pages/profile.server.ts
page.server.tsxsrc/pages/profile.server.tsx
page.server.jssrc/pages/profile.server.js
page.server.mtssrc/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";
2
3import { useEffect } from "react";
4
5export default function MapPage() {
6 useEffect(() => {
7 import("leaflet").then(({ default: L }) => {
8 L.map("map-container");
9 });
10 }, []);
11
12 return <div id="map-container" />;
13}

Limitations

See Also

HeliumTS
Note:

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";
2
3export 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

FeatureSSRSSGSPA (default)
HTML on first loadPer-requestBuild-timeEmpty root shell
Dynamic per-request dataYesNoClient-only
Server-side propsgetServerSidePropsNoNo
Auth-protected pagesYes (skip guard on server)NoYes
Build step requiredNoYesNo
Suitable for SEOYesYesRequires 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.

1"use ssr";
2
3import type { GetServerSideProps } from "heliumts/server";
4
5export const getServerSideProps: GetServerSideProps = async (req) => {
6 const user = await db.users.findById(req.params.id);
7 return { user };
8};
9
10export default function ProfilePage({ user }: { user: { name: string } }) {
11 return <h1>Hello, {user.name}</h1>;
12}

Use a sidecar file to avoid mixing component and non-component exports in one module and keep Fast Refresh clean.

1src/pages/
2├── profile.tsx
3└── profile.server.ts
1// src/pages/profile.server.ts
2import type { GetServerSideProps } from "heliumts/server";
3
4export 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.tsx
2"use ssr";
3
4export 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.

  • req includes method, path, headers, query, and dynamic route params.
  • ctx contains server context like client IP, normalized headers, and raw incoming request.

Redirects from SSR

1import type { GetServerSideProps } from "heliumts/server";
2
3export const getServerSideProps: GetServerSideProps = async (req, ctx) => {
4 const accountStatus = await billing.getStatus(ctx.req.headers.authorization);
5
6 if (accountStatus === "overdue") {
7 return {
8 redirect: {
9 destination: "/billing/overdue",
10 statusCode: 307,
11 replace: true,
12 },
13 };
14 }
15
16 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";
3
4export default function AppLayout({ children }: { children: React.ReactNode }) {
5 const { isPending, data } = useSession();
6 const router = useRouter();
7
8 useEffect(() => {
9 if (!data?.session && !isPending) {
10 router.push("/login");
11 }
12 }, [data, isPending, router]);
13
14 // 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 }
20
21 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

  1. Directive scanning discovers pages with "use ssr".
  2. The server intercepts HTML responses for SSR routes.
  3. If present, getServerSideProps is executed and merged into props.
  4. React renders layouts and page with renderToString.
  5. Helium injects rendered markup into <div id="root">.
  6. Serialized props are injected as window.__HELIUM_SSR_DATA__.
  7. 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 FileExample
page.server.tssrc/pages/profile.server.ts
page.server.tsxsrc/pages/profile.server.tsx
page.server.jssrc/pages/profile.server.js
page.server.mtssrc/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";
2
3import { useEffect } from "react";
4
5export default function MapPage() {
6 useEffect(() => {
7 import("leaflet").then(({ default: L }) => {
8 L.map("map-container");
9 });
10 }, []);
11
12 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.

See Also