Server-Side Rendering
Functions
getRenderInfo(html: string): Promise<RenderInfo>Extract render metadata from HTML composition (Node.js safe)
RenderInfoEditframe packages provide SSR-safe entry points for server-side rendering with Next.js, Remix, and other React frameworks. Import from @editframe/elements/server or @editframe/react/server to access types and components without triggering browser-specific code.
Problem: Browser APIs in SSR
The main package entries (@editframe/elements and @editframe/react) import browser-specific code at the module level:
- Web Components (
customElements.define()) - DOM APIs (
HTMLElement,document,window) - Canvas, WebGL, WebCodecs APIs
This code will crash if imported during server-side rendering because Node.js doesn't provide these APIs.
Solution: SSR-Safe Entry Points
Use the /server subpath exports that only include types and SSR-safe utilities.
@editframe/elements/server
Import types only and the getRenderInfo utility:
import type {EFTimegroup,EFMedia,RenderToVideoOptions,RenderProgress,} from "@editframe/elements/server";import { getRenderInfo } from "@editframe/elements/server";
What's Included
- Types: TypeScript type definitions for all elements (zero runtime code)
- getRenderInfo: Function to extract render metadata from HTML (Node.js safe)
What's NOT Included
- Web Component class definitions
- DOM manipulation utilities
- Browser-specific rendering code
- Canvas or WebGL APIs
getRenderInfo Example
// server.ts (Node.js safe)import { getRenderInfo } from "@editframe/elements/server";const html = `<ef-timegroup mode="sequence" duration="30s"><ef-video src="intro.mp4"></ef-video><ef-video src="outro.mp4"></ef-video></ef-timegroup>`;const info = await getRenderInfo(html);console.log(info.durationMs); // 30000console.log(info.width); // 1920console.log(info.height); // 1080
@editframe/react/server
Import React components that render to HTML without triggering browser APIs:
import {Timegroup,Video,Audio,Image,Text,Captions,Surface,Waveform,PanZoom,} from "@editframe/react/server";
What's Included
- All composition React components (render to
<ef-*>tags) - Caption styling sub-components
- All types from
@editframe/elements/server
What's NOT Included
- GUI components (Preview, Workbench, Controls, Timeline, etc.)
- Hooks that depend on browser APIs (
useTimingInfo,usePlayback, etc.) - Browser rendering utilities
Next.js Integration
App Router (React Server Components)
// app/video/[id]/page.tsx (Server Component)import { Timegroup, Video, Audio } from "@editframe/react/server";export default function VideoPage({ params }: { params: { id: string } }) {return (<Timegroup mode="sequence" className="w-[1920px] h-[1080px]"><Video src={`/api/videos/${params.id}/intro.mp4`} className="size-full" /><Audio src={`/api/videos/${params.id}/music.mp3`} volume={0.3} /></Timegroup>);}
Pages Router (SSR)
Use dynamic imports with ssr: false for browser-only code:
// pages/editor.tsximport dynamic from "next/dynamic";// Safe: imports from /serverimport { Timegroup, Video } from "@editframe/react/server";// Unsafe: imports from main entry, requires client-side onlyconst Preview = dynamic(() => import("@editframe/react").then((mod) => mod.Preview),{ ssr: false });const Workbench = dynamic(() => import("@editframe/react").then((mod) => mod.Workbench),{ ssr: false });export default function EditorPage() {return (<div>{/* This renders on server */}<Timegroup mode="sequence" className="w-[1920px] h-[1080px]"><Video src="/videos/intro.mp4" className="size-full" /></Timegroup>{/* This only renders on client */}<Workbench rendering={false} /></div>);}
Remix Integration
Use the /server entry for loaders and actions:
// routes/video.$id.tsximport type { LoaderFunctionArgs } from "@remix-run/node";import { json } from "@remix-run/node";import { useLoaderData } from "@remix-run/react";import { Timegroup, Video, Audio } from "@editframe/react/server";export async function loader({ params }: LoaderFunctionArgs) {const videoData = await fetchVideoData(params.id);return json({ videoData });}export default function VideoRoute() {const { videoData } = useLoaderData<typeof loader>();return (<Timegroup mode="sequence" className="w-[1920px] h-[1080px]"><Video src={videoData.introUrl} className="size-full" /><Audio src={videoData.musicUrl} volume={0.3} /></Timegroup>);}
For client-only components, use ClientOnly:
import { ClientOnly } from "remix-utils/client-only";export default function EditorRoute() {return (<ClientOnly>{() => {// Import happens only on clientconst { Workbench } = require("@editframe/react");return <Workbench rendering={false} />;}}</ClientOnly>);}
Pre-rendering Static HTML
Generate static HTML for compositions:
// build.ts (Node.js)import { renderToString } from "react-dom/server";import { Timegroup, Video, Text } from "@editframe/react/server";import { writeFileSync } from "fs";const composition = (<Timegroup mode="fixed" duration="10s" className="w-[1920px] h-[1080px]"><Video src="/assets/background.mp4" className="size-full" /><Text className="absolute inset-0 flex items-center justify-center text-6xl">Hello World</Text></Timegroup>);const html = renderToString(composition);writeFileSync("./dist/composition.html", html);
This HTML can then be hydrated on the client with the full browser-side code.
Type Imports
When you only need types (not runtime code), use type-only imports for maximum safety:
// Safe in any environment (no runtime code)import type { EFTimegroup, RenderToVideoOptions } from "@editframe/elements/server";function createRenderConfig(): RenderToVideoOptions {return {fps: 30,codec: "h264",scale: 1,};}
Common Patterns
Hydration After SSR
Server:
// server.tsximport { renderToString } from "react-dom/server";import { Timegroup, Video } from "@editframe/react/server";const html = renderToString(<Timegroup mode="sequence" className="w-[1920px] h-[1080px]"><Video src="/video.mp4" className="size-full" /></Timegroup>);// Send html to client
Client:
// client.tsximport { hydrateRoot } from "react-dom/client";import { Timegroup, Video } from "@editframe/react"; // Full browser versionhydrateRoot(document.getElementById("root"),<Timegroup mode="sequence" className="w-[1920px] h-[1080px]"><Video src="/video.mp4" className="size-full" /></Timegroup>);
Conditional Imports
// utils.tsexport async function loadEditorComponents() {if (typeof window === "undefined") {throw new Error("Editor components require browser environment");}const { Workbench, Preview, Timeline } = await import("@editframe/react");return { Workbench, Preview, Timeline };}
Troubleshooting
Error: customElements is not defined
Problem: Importing from main entry point during SSR.
Solution: Use /server entry point:
// ❌ Wrongimport { Timegroup } from "@editframe/react";// ✅ Correctimport { Timegroup } from "@editframe/react/server";
Error: HTMLElement is not defined
Problem: Web Component class definition loaded during SSR.
Solution: Use dynamic import with ssr: false or conditional import.
Error: document is not defined
Problem: Browser-specific code running on server.
Solution: Check imports and ensure all browser code is client-only.
Package Structure
Both packages expose three entry points:
@editframe/elements
{"exports": {".": "./dist/index.js", // Browser: Full package (Web Components, DOM APIs)"./server": "./dist/server.js", // SSR: Types + getRenderInfo only"./node": "./dist/node.js" // Node: Same as /server (backward compat)}}
@editframe/react
{"exports": {".": "./dist/index.js", // Browser: Full package (all components + hooks)"./server": "./dist/server.js", // SSR: Composition components only"./r3f": "./dist/r3f/index.js" // Browser: React Three Fiber integration}}
Related
- entry-points.md - Complete guide to package entry points
- r3f.md - React Three Fiber integration
- getting-started.md - Installation and setup