Next.js Page Builder — GrapesJS Integration Guide

Build a modern visual editor users can actually ship with

22k+
GitHub Stars
100+
Plugins
MIT
License
10yrs+
In Production
Next.js + GrapesJS blueprint

Ship a polished page builder without fighting SSR.

GrapesJS is browser-only, while Next.js renders on server by default. This guide gives you the safest architecture for client-only editor boot, stable save/publish APIs, and a UX users can learn quickly.

Quick implementation checklist

  • Run npm install grapesjs and load styles once globally
  • Keep editor initialization in a client-only component
  • Disable SSR using dynamic import with { ssr: false }
  • Add autosave to API route and version your content payloads
  • Render published HTML/CSS in a sanitized viewer route

Important: GrapesJS is browser-only

GrapesJS calls browser APIs (document, window, requestAnimationFrame) on initialisation. You must use dynamic() with { ssr: false } in the Pages Router, or the 'use client' directive with an isMounted guard in the App Router. Skipping this will cause a build-time or runtime error.

Client-Side Only Pattern

The recommended approach for both Pages Router and App Router is to split the editor into two files: a thin wrapper that uses dynamic to disable SSR, and a client-only component that initialises GrapesJS:

components/GrapesJSEditor.tsx
// components/GrapesJSEditor.tsx
import dynamic from 'next/dynamic';

const GrapesEditor = dynamic(
  () => import('./GrapesEditorClient'),
  { ssr: false, loading: () => <div>Loading editor...</div> }
);

export default function GrapesJSEditor() {
  return <GrapesEditor />;
}

// components/GrapesEditorClient.tsx
import { useEffect, useRef } from 'react';
import grapesjs from 'grapesjs';

export default function GrapesEditorClient() {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const editor = grapesjs.init({
      container: ref.current!,
      storageManager: false,
    });
    return () => editor.destroy();
  }, []);

  return <div ref={ref} style={{ height: '100vh' }} />;
}

App Router vs Pages Router

Pages Router (Next.js 12/13)

Use dynamic() from next/dynamic with { ssr: false }. All components are React Client Components by default, so the only requirement is disabling SSR for GrapesJS.

App Router (Next.js 13+)

Add 'use client' at the top of your editor component. You can still use dynamic() for the loading state, or add an isMounted state guard to prevent rendering until the component has mounted client-side.

Choose your launch scope

MVP in 1-2 days

Use default blocks, local persistence, and one publish endpoint. Best for internal tools or early customer trials.

Production launch in 1-2 weeks

Add block permissions, autosave, asset uploads, revision history, and role-based publishing workflows.

SaaS builder platform

Multi-tenant storage, template marketplace, usage quotas, audit logs, and workspace-level branding.

Step-by-Step Integration Guide

1

Install GrapesJS

Run npm install grapesjs in your Next.js project. TypeScript types are included.

2

Create a client-only editor component

Create GrapesEditorClient.tsx that initialises GrapesJS in a useEffect hook with a DOM ref.

3

Wrap with next/dynamic (ssr: false)

Use dynamic(() => import("./GrapesEditorClient"), { ssr: false }) to prevent server-side execution.

4

Connect storage to an API route

Use editor.getHTML() and editor.getCSS() to post content to a Next.js API route, Vercel KV, or Supabase.

Recommended Plugins for Next.js Projects

Storage$19

Storage REST API

Connect GrapesJS to Next.js API routes for persistence

Blocks$29

Blocks Library Pro

Production-ready block library for Next.js page builders

EmailFree

GrapesJS MJML Preset

Build responsive email templates in your Next.js app

ExportFree

Export Plugin

Export clean HTML/CSS from within Next.js API routes

Storage Integration Examples

Vercel KV

app/api/page/route.ts
// Save to Vercel KV
import { kv } from '@vercel/kv';

export async function POST(req: Request) {
  const { id, html, css } = await req.json();
  await kv.set(`page:${id}`, { html, css });
  return Response.json({ ok: true });
}

Supabase

lib/savePage.ts
// Save to Supabase
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_KEY!
);

export async function savePage(id: string, html: string, css: string) {
  await supabase.from('pages').upsert({ id, html, css });
}

Ready to launch your visual builder experience?

Start with battle-tested plugins, then layer your own workflows for teams, clients, and publishing approvals.

Explore plugin stack