Skip to content
react-wrapped

Server rendering

react-wrapped renders on a server. Nothing in it touches window while rendering, and this site is proof: every page here is static HTML made at build time. What the server cannot do is play: timelines are built in the browser. This guide says what ends up in the HTML, what waits, and how to set it up in a framework with server components.

What is in the server HTML

  • The whole stage with its palette as inline CSS variables, and every scene's markup. All scenes are mounted at once and the ones off screen are not displayed, so every word of the video is in the HTML.
  • The transport, the timeline's row, the settings panel and the scan panel with its blank year.
  • The gallery's tiles, each with its title and line. The pictures are not there yet.
  • The loading frame, when you render the wait.

What waits for the browser

  • The size of the frame. The player measures the room it has in a layout effect and scales the stage to it. Until then the frame is hidden, so it never shows at the wrong size.
  • The timelines. Every scene's GSAP timeline is built after mount, when its elements can be measured. Autoplay starts right after.
  • Stored state. useOpenedDrops reads storage before the first paint in the browser. On the server it has opened nothing and ready is false.
  • Pictures. The share card and the insight cards are drawn on a canvas when their tile is about to be seen.
  • The MP4. It needs the browser's video encoder.

Next.js and server components

The components use state and effects, and the packages do not mark themselves with "use client". So put the boundary in a file of your own. Fetch the numbers in a server component, pass them down as plain data, and build the scenes on the client side of the boundary: a scene holds functions, and functions cannot cross it.

app/wrapped/[handle]/page.tsx
import { MyWrapped } from "./my-wrapped";
import { loadYear } from "./load-year";

// A server component: it fetches, and hands over numbers.
export default async function Page({ params }: { params: Promise<{ handle: string }> }) {
  const { handle } = await params;
  const year = await loadYear(handle);
  return <MyWrapped year={year} />;
}
app/wrapped/[handle]/my-wrapped.tsx
"use client";

import { useMemo, useState } from "react";
import { DEFAULT_WRAPPED_CONFIG, type WrappedConfig, WrappedView } from "@react-wrapped/kit";

import { directYear, type Year } from "./direct-year";

export function MyWrapped({ year }: { year: Year }) {
  const [config, setConfig] = useState<WrappedConfig>(DEFAULT_WRAPPED_CONFIG);
  // Scenes are built here, from the plain numbers the server sent. Memoized: a new array rebuilds the video.
  const cut = useMemo(() => directYear(year), [year]);
  return (
    <WrappedView
      subject={cut.subject}
      seed={cut.seed}
      scenes={cut.scenes}
      config={config}
      onConfigChange={setConfig}
    />
  );
}

directYear is yours: the function that turns a year's numbers into a subject, a seed and scenes. Keep it in a file without the directive and it can be tested on its own.

The stylesheet, once

Import the stylesheet in the root layout, not in the component, so it is in the first HTML and loads once. With the single packages, import core first and each package's own after it.

app/layout.tsx
import "@react-wrapped/kit/styles.css";

The same on both sides

The server's HTML and the browser's first render have to match. The library keeps its side: nothing it renders depends on the clock or on Math.random, and every variation in a video comes from the seed. Keep yours:

  • Format dates and numbers the same way on both sides. Name the locale and the time zone, or format on the server and send strings.
  • Do not read the URL or storage while rendering. Start from defaults and read them in an effect, as the hook in the config in the URL does. A framework that hands the search to the server can use it from the first render.
  • A custom transition has to be defined on both sides before a stage renders. Import its file from the client component, not only from a browser entry.

Static sites

A statically built page works the same way: the HTML holds the markup, and the video starts when the script has loaded. For a crawler the text of every scene is in the page. For a link preview draw an image on the server from the same numbers, or host a share card the owner made.

Last updated . Read this page as Markdown.