A video editor uses the GPU, workers, WebCodecs and audio in the browser, so in Next.js it has to load on the client only. This guide sets that up with the App Router, handles the two headers the editor’s audio engine wants, and exports video straight from the browser. It uses Cutforge; the same pattern runs in production on a Next.js 16 site.
1. Install
npm install @cutforge/editor
2. A client component for the editor
// app/editor/VideoEditor.tsx
'use client';
import { Editor } from '@cutforge/editor';
import '@cutforge/editor/style.css';
export default function VideoEditor() {
return (
<Editor
license={process.env.NEXT_PUBLIC_CUTFORGE_LICENSE}
onExportComplete={({ blob, filename }) => upload(blob, filename)}
style={{ height: '100vh' }}
/>
);
}
async function upload(blob: Blob, filename: string) {
const res = await fetch(`/api/upload-url?name=${encodeURIComponent(filename)}`);
const { url } = await res.json();
await fetch(url, { method: 'PUT', body: blob, headers: { 'Content-Type': blob.type } });
}
Leave NEXT_PUBLIC_CUTFORGE_LICENSE unset to run the watermarked demo while you build.
3. Load it without server rendering
'use client' alone is not enough: client components still render once on the server, and the editor needs browser APIs from the start. Load it with next/dynamic and ssr: false. In the App Router that option is only allowed inside a client component, so add a small loader:
// app/editor/EditorLoader.tsx
'use client';
import dynamic from 'next/dynamic';
const VideoEditor = dynamic(() => import('./VideoEditor'), {
ssr: false,
loading: () => <p>Loading editor…</p>,
});
export default function EditorLoader() {
return <VideoEditor />;
}
// app/editor/page.tsx
import EditorLoader from './EditorLoader';
export default function Page() {
return <EditorLoader />;
}
A side benefit: the editor’s code only downloads when someone opens this page.
4. Headers for the fast audio path
The editor’s low-latency audio engine needs cross-origin isolation. Without it the editor still works and falls back to a simpler audio path. Set the headers on the editor route only, so the rest of your site is unaffected:
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
async headers() {
return [
{
source: '/editor',
headers: [
{ key: 'Cross-Origin-Opener-Policy', value: 'same-origin' },
{ key: 'Cross-Origin-Embedder-Policy', value: 'require-corp' },
],
},
];
},
};
export default nextConfig;
Three things that catch people out:
- Client-side navigation skips the headers. Isolation is decided when the document loads. If users reach
/editorthroughnext/linkfrom a page without these headers, the page is not isolated. Link to it with a plain<a href="/editor">so the browser does a full load. require-corpblocks cross-origin resources that don’t opt in, such as third-party images or scripts on that page. Serve them from your own origin or with aCross-Origin-Resource-Policyheader.- Static export (
output: 'export') ignoresheaders(). Set the two headers in your host’s configuration (nginx, Netlify, Cloudflare) instead.
5. Save projects
Pass project and onProjectChange to keep the editor’s project in React state, then store it as JSON in your database. The React guide shows the pattern.
Next steps
- Theming, white-label branding, localization, your own media library and import/export hooks: see the docs.
- How it compares with Rendley, IMG.LY and Remotion: video editor SDKs compared.