PageKit — the self-hosted GrapesJS site builder, sold as source. Get early access

Next.js + GrapesJS

Next.js Page Builder with GrapesJS

Build a visual drag-and-drop page builder inside your Next.js application with GrapesJS, custom components, persistent project data and your own publishing workflow.

Next.js App RouterReactDrag & DropVisual EditingHTML & CSSExtensible
your-app.com/editor
Next.js

Blocks

  • Hero
  • Feature grid
  • Pricing
  • Form
  • Footer

Your page

Hero section
Block panel, canvas, layers, style manager and responsive devices — the editor surface GrapesJS gives you before you write a line of it.

Powered by GrapesJS

Try the Editor

26k+

GitHub stars

1.4M+

npm downloads / month

BSD-3-Clause

Core licence

100+

Plugins on GJS.Market

GrapesJS figures verified 2026-09-03 against the npm registry and the GitHub API. The core library is BSD-3-Clause; the official React wrapper is MIT.

The build-vs-adopt question

Build a Next.js Page Builder Without Building the Editor Engine

GrapesJS provides the visual editing engine. Next.js provides the application architecture around it. Below is the same list read twice: what an editor is made of, and who ends up building each part once you adopt GrapesJS.

  • Drag & dropGrapesJS core
  • CanvasGrapesJS core
  • Component treeGrapesJS core
  • Selection & hoverGrapesJS core
  • BlocksGrapesJS core
  • StylingGrapesJS core
  • Responsive editingGrapesJS core
  • Undo / redoGrapesJS core
  • Asset managementGrapesJS core
  • SerializationGrapesJS core
  • CommandsGrapesJS core
  • Plugin systemGrapesJS core
  • StorageGrapesJS core
  • Multi-page projectsPlugin or extension
  • Template libraryPlugin or extension
  • Users & permissionsYour Next.js app
  • PublishingYour Next.js app
Who builds itYour Next.js appGrapesJS corePlugin or extension

Two of the seventeen are yours. The rest either ship with the editor or exist as a plugin you install.

A page builder looks like one feature and is in fact a small product. The visible part — a panel of blocks, a canvas, a style sidebar — sits on top of a dozen subsystems that all have to work before any of it feels usable.

Instead of rebuilding the editor engine, use GrapesJS and focus on your product.

Try a Next.js Page Builder

A real GrapesJS editor, running on this page. Drag a block in from the right, select anything on the canvas and restyle it, or switch the canvas to a phone width — this is the surface your users would get.

  • Drag & drop
  • Blocks
  • Layers
  • Style Manager
  • Responsive devices
  • Assets
  • Undo / redo

What happens when a user drags a block

  1. Next.js application
  2. Client Component
  3. GrapesJS
  4. Project data
  5. Next.js API
  6. Database
  7. Publish

Three responsibilities, three owners.

Next.js owns routing, authentication, data fetching and the deployed surface. GrapesJS owns everything inside the canvas. Your API owns what a project is, who may edit it and when it goes live. Every section below is one of those three boxes opened up.

Next.js handles the application. GrapesJS handles the visual editing. Your backend handles persistence and publishing. Nothing in that chain requires you to give up your existing architecture.

Architecture

How GrapesJS Fits into a Next.js Application

GrapesJS is a layer, not a framework. It renders a canvas into a DOM node you give it and exposes an API for everything in that canvas. It has no opinion about routing, no opinion about your database, and no runtime relationship with the rest of your app beyond the element it was handed.

NEXT.JS

  • App Router
  • Authentication
  • Users
  • Permissions
  • API
  • Database
  • Billing
  • Publishing

CLIENT EDITOR

GRAPESJS

  • Canvas
  • Components
  • Blocks
  • Style Manager
  • Assets
  • Commands
  • Storage

This is why the integration is small. The editor is a client-side widget with a rich API — the same shape as a code editor or a charting library, not a competing application framework. Everything that makes your product yours stays on the Next.js side of the line.

GrapesJS + React integration

GrapesJS does not replace Next.js. It adds the visual editing layer your application needs.

Quick start

Use GrapesJS with the Next.js App Router

GrapesJS needs a browser. It measures elements, attaches listeners and mutates the DOM the moment it initialises, so the editor belongs inside a Client Component and its setup belongs inside an effect. That is the entire constraint — everything else is ordinary React.

npm install grapesjs
components/GrapesEditor.tsxtsx
'use client';

import { useEffect, useRef } from 'react';
import grapesjs, { type Editor } from 'grapesjs';
import 'grapesjs/dist/css/grapes.min.css';

export default function GrapesEditor() {
  const containerRef = useRef<HTMLDivElement>(null);
  const editorRef = useRef<Editor | null>(null);

  useEffect(() => {
    if (!containerRef.current) return;

    // Runs only in the browser: effects never execute during SSR.
    const editor = grapesjs.init({
      container: containerRef.current,
      height: '100vh',
      storageManager: false,
      blockManager: {
        blocks: [
          {
            id: 'section',
            label: 'Section',
            content: '<section class="py-16"><h2>Headline</h2></section>',
          },
          { id: 'text', label: 'Text', content: '<p>Edit me</p>' },
        ],
      },
    });

    editorRef.current = editor;

    // Strict Mode mounts twice in development; without this you get two editors.
    return () => {
      editor.destroy();
      editorRef.current = null;
    };
  }, []);

  return <div ref={containerRef} />;
}
app/editor/page.tsxtsx
import GrapesEditor from '@/components/GrapesEditor';

// A Server Component. It renders the Client Component; it never touches
// the editor instance, and no 'use client' is needed here.
export default function EditorPage() {
  return <GrapesEditor />;
}

Why the example looks like this

'use client'

Marks the module as a Client Component so React hooks are available and the code is sent to the browser.

useRef, not useState

The editor instance is not render data. Putting it in state schedules a re-render on every change for no benefit.

useEffect

Effects never run during server rendering, so initialisation is guaranteed to happen only once the DOM exists.

editor.destroy()

React Strict Mode mounts components twice in development. Without cleanup you get two editors stacked on one node.

That is a working editor. The rest of this page is about the four things you add next: where the project is stored, what users can drag, what happens when they publish, and which of those you do not have to write yourself.

Routing

App Router vs Pages Router

Both work. They differ in where the client boundary is drawn, and that difference is why so much GrapesJS + Next.js advice on the web fails on a modern project.
Recommended

App Router

A Client Component holds the editor; the route around it stays a Server Component. No dynamic import is required, because the editor is already only initialised in the browser.

tsx
// components/GrapesEditor.tsx
'use client';
// …useRef + useEffect + grapesjs.init()

// app/editor/page.tsx — stays a Server Component
import GrapesEditor from '@/components/GrapesEditor';
export default function Page() {
  return <GrapesEditor />;
}
  • The 'use client' directive marks the boundary — everything below it is sent to the browser.
  • The route file stays a Server Component and can await auth, params and data before rendering the editor.
  • A Client Component is still prerendered on the server by default. Effects are not, which is what keeps grapesjs.init() browser-only.
  • next/dynamic with ssr: false is rejected inside a Server Component — Next.js tells you to move it into a Client Component.
Legacy

Pages Router

Every page is a client entry point, so the usual recipe is a dynamic import with prerendering switched off.

pages/editor.tsxtsx
import dynamic from 'next/dynamic';

// In the Pages Router every page is a client entry point, so ssr: false
// is allowed here — and skips the prerender pass entirely.
const GrapesEditor = dynamic(() => import('@/components/GrapesEditor'), {
  ssr: false,
  loading: () => <p>Loading editor…</p>,
});

export default function EditorPage() {
  return <GrapesEditor />;
}
  • ssr: false is allowed here and skips the server render pass entirely.
  • The loading option gives you a placeholder while the editor chunk downloads.
  • The same GrapesEditor component works unchanged — only the way it is imported differs.

If you are migrating, do not carry the dynamic() wrapper across. In the App Router it either fails outright inside a Server Component or duplicates work that 'use client' already does.

Server components

GrapesJS and React Server Components

Server Components are where the work around the editor belongs. They fetch data, run server-side logic and provide the application shell. The Client Component initialises GrapesJS, owns its lifecycle and handles every browser interaction. Props cross the boundary; the editor instance never does.

Where the boundary sits

  1. Server Component
  2. Page / data
  3. Client Component
  4. GrapesJS editor
app/projects/[projectId]/editor/page.tsxtsx
import GrapesEditor from '@/components/GrapesEditor';

export default async function EditorPage({
  params,
}: {
  params: Promise<{ projectId: string }>;
}) {
  const { projectId } = await params;

  // Server side: auth, permissions and data fetching stay here.
  const res = await fetch(`${process.env.API_URL}/projects/${projectId}`, {
    cache: 'no-store',
  });
  const initialProject = await res.json();

  // The Client Component receives plain, serialisable props.
  return <GrapesEditor projectId={projectId} initialProject={initialProject} />;
}
  • Authentication and permission checks run on the server, before the editor bundle is worth downloading.
  • The project's initial data is fetched server-side and passed down as a plain, serialisable prop.
  • The editor instance stays inside the Client Component. Nothing about it is serialisable, so nothing about it crosses the boundary.
  • Server Actions can be called from the client component for saves — they are just functions on the client side of the boundary.

Keep the editor client-side while using Server Components for the surrounding application.

SSR

Does GrapesJS Work with Next.js SSR?

Yes, but the editor itself should be initialized on the client because it relies on browser APIs and the DOM.

The useful version of that answer is more specific, because the two halves of it fail differently.

Reproduced locally

runtime
Node 20, no DOM
grapesjs
0.23.6
import
import('grapesjs') → resolves
init
grapesjs.init() → ReferenceError: document is not defined

2026-09-02

Importing the library in a DOM-less runtime is fine. Calling init() is not. So the rule is not "keep GrapesJS off the server" — it is "keep init() out of the render path", which an effect already guarantees.

  • The import is safe. Bundling GrapesJS into a module that the server also evaluates does not throw.
  • Initialisation is not. grapesjs.init() reads document, so it must run after mount — which is exactly what useEffect means.
  • 'use client' is not the same as "client only". Client Components are prerendered on the server; the effect is what does not run there.
  • dynamic(..., { ssr: false }) is therefore optional in the App Router. Reach for it to keep the editor chunk out of the initial payload, not to fix a crash.
  • Rendering a published page is a different problem entirely — see below. That page needs no editor runtime at all, so it can be statically generated like anything else.
Authoring vs serving

Your Editor Does Not Have to Be Your Published Page

This is the single most useful thing to get right early, and the easiest to get wrong. The editor is an authoring environment. The published page can use your own Next.js rendering architecture — static generation, streaming, revalidation, edge caching, all of it.

Authoring

  1. GrapesJS
  2. Project data
  3. Database

Serving

  1. Publish
  2. HTML + CSS
  3. Public page
Two pipelines, one artefact passing between them. Only the first needs GrapesJS.
app/p/[slug]/page.tsxtsx
// The public route. GrapesJS is not imported here, so the editor
// bundle never reaches a visitor.
export const revalidate = 3600;

export default async function PublishedPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const res = await fetch(`${process.env.API_URL}/published/${slug}`, {
    next: { revalidate: 3600 },
  });

  // Already sanitised on the way in — see the publish route below.
  const { html, css } = await res.json();

  return (
    <>
      <style dangerouslySetInnerHTML={{ __html: css }} />
      <div dangerouslySetInnerHTML={{ __html: html }} />
    </>
  );
}

GrapesJS never has to run on a page a visitor opens. Ship the editor to the handful of people who edit, and ship plain HTML and CSS to everyone else.

Persistence

Save GrapesJS Projects in Your Next.js Application

GrapesJS ships local and remote storage adapters and lets you register your own. A custom adapter is usually the right choice, because it puts every read and write behind a route you control and can authenticate.

  1. GrapesJS
  2. Project data
  3. Next.js API
  4. Your backend
  5. Database
The editor never talks to your database. It talks to one route, which talks to whatever you actually use.
components/GrapesEditor.tsx — storagets
const editor = grapesjs.init({
  container: containerRef.current,
  storageManager: {
    type: 'nextjs-api',
    autosave: true,
    stepsBeforeSave: 5,
  },
  plugins: [
    // Registered as a plugin so the adapter exists before the first load.
    (editor) => {
      editor.Storage.add('nextjs-api', {
        async load() {
          const res = await fetch(`/api/projects/${projectId}`);
          return res.ok ? res.json() : {};
        },
        async store(project) {
          await fetch(`/api/projects/${projectId}`, {
            method: 'PUT',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(project),
          });
        },
      });
    },
  ],
});
app/api/projects/[projectId]/route.tsts
import { NextResponse } from 'next/server';

// Your own persistence layer — Postgres, MySQL, Mongo, S3, a headless CMS.
// GrapesJS never talks to it; it only ever talks to this route.
import { loadProject, saveProject } from '@/lib/projects';
import { requireProjectAccess } from '@/lib/auth';

export async function GET(
  _request: Request,
  { params }: { params: Promise<{ projectId: string }> },
) {
  const { projectId } = await params;
  await requireProjectAccess(projectId);

  return NextResponse.json(await loadProject(projectId));
}

export async function PUT(
  request: Request,
  { params }: { params: Promise<{ projectId: string }> },
) {
  const { projectId } = await params;
  await requireProjectAccess(projectId);

  const project = await request.json();
  if (typeof project !== 'object' || project === null) {
    return NextResponse.json({ error: 'Invalid project' }, { status: 400 });
  }

  await saveProject(projectId, project);
  return NextResponse.json({ ok: true });
}
  • Load: the adapter's load() runs on init and restores the project the user was last working on.
  • Save: store() receives the whole project as JSON. Return a rejected promise to surface a failure in the editor.
  • Autosave: autosave with stepsBeforeSave batches changes so you are not writing on every keystroke.
  • Drafts: keep the draft project and the published output in separate columns, so editing never mutates what visitors see.
  • Versioning: project data is a JSON document — an append-only table of versions costs one insert and buys rollback.
  • Publishing: a separate endpoint with its own permission check, not a flag on the save route.

Example: storing projects with Supabase

lib/projects.tsts
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!, // server only — never NEXT_PUBLIC_
);

export async function loadProject(projectId: string) {
  const { data } = await supabase
    .from('projects')
    .select('project')
    .eq('id', projectId)
    .single();

  return data?.project ?? {};
}

export async function saveProject(projectId: string, project: unknown) {
  await supabase
    .from('projects')
    .upsert({ id: projectId, project, updated_at: new Date().toISOString() });
}

This implements the loadProject and saveProject boundary the route above imports. Swap the body for Prisma, Drizzle, Mongo, DynamoDB or a REST call to an existing backend and nothing else on the page changes.

Example: connecting project storage to a Vercel/Next.js deployment

Publishing writes HTML and CSS your public route reads back. Revalidation, caching and the rendering strategy stay ordinary Next.js concerns — the editor is not in the request path.

See the published-page route

You can connect GrapesJS to any backend or database. Nothing in the editor knows or cares which one you picked.

Extending the canvas

Create Custom Components for Your Next.js Page Builder

Out of the box the canvas gives users generic HTML. Custom component types are how the editor starts producing your product's markup instead — the same sections your Next.js app already renders, with only the properties you decide to expose.

Registering a component typets
// Your design system, expressed as something users can drop in,
// edit and style — but not break.
editor.Components.addType('pricing-table', {
  isComponent: (el) =>
    el.tagName === 'SECTION' && el.classList.contains('pricing'),

  model: {
    defaults: {
      tagName: 'section',
      classes: ['pricing'],
      // Structure the user cannot accidentally dismantle.
      droppable: false,
      // Fields exposed in the Settings panel.
      traits: [
        { name: 'plan', label: 'Plan name', type: 'text' },
        { name: 'price', label: 'Price', type: 'text' },
        { name: 'billing', label: 'Billing period', type: 'select',
          options: [
            { id: 'month', label: 'Monthly' },
            { id: 'year', label: 'Yearly' },
          ] },
      ],
      components: [
        { type: 'text', tagName: 'h3', content: 'Starter' },
        { type: 'text', tagName: 'p', content: '$19 / month' },
        { type: 'link', content: 'Choose plan', attributes: { href: '#' } },
      ],
    },
  },
});

Common custom components

  • Hero — headline, sub-headline, background and one call to action
  • Pricing table — plans, prices and billing period as editable fields
  • Product card — bound to a real product ID rather than typed-in text
  • Feature grid — a fixed layout with a variable number of children
  • Form — fields users can arrange, wired to your submission endpoint
  • Navigation — pulled from your routes so links cannot go stale
  • Application component — a chart, a booking widget, anything your SaaS already ships

Custom components are how the editor matches your design system and your business model. droppable: false stops a user dismantling a structure; traits decide exactly which properties they may change.

Build a Block Library

Blocks are what appears in the panel a user drags from. Registering one costs a few lines once the component type exists.

Registering a blockts
// A block is what the user drags. A component is what it becomes.
editor.Blocks.add('pricing-table', {
  label: 'Pricing table',
  category: 'Marketing',
  media: '<svg viewBox="0 0 24 24" width="24" height="24">' +
    '<rect x="3" y="4" width="18" height="16" rx="2" fill="currentColor"/></svg>',
  content: { type: 'pricing-table' },
});

Blocks are not components

  • A block is a starting point — a label, an icon and the content it inserts. It exists in the panel, not in the page.
  • A component is a structured element inside the editor, with its own model, traits and rules. It exists in the page once inserted.
  • One component type can back several blocks (a two-column and a three-column pricing table), and one block can insert a whole tree of components.
  • Reusable sections — hero, features, testimonials, footer
  • Marketing blocks — pricing tables, logo walls, calls to action
  • Layout blocks — columns, spacers, containers
  • Forms — contact, signup, multi-step
  • Ecommerce blocks — product grids, cart summaries, checkout prompts
  • Application blocks — whatever your product does that a page should be able to show

Manage Images and Assets

The Asset Manager handles selection and insertion. Where the files live, who may upload them and what is allowed through is your side of the contract.

Asset Manager — custom uploadts
assetManager: {
  // The library the user already has, loaded from your backend.
  assets: initialAssets,

  uploadFile: async (event) => {
    const files = event.dataTransfer
      ? event.dataTransfer.files
      : (event.target as HTMLInputElement).files;
    if (!files) return;

    const body = new FormData();
    Array.from(files).forEach((file) => body.append('files', file));

    // Your route authenticates the user and validates type and size
    // before anything is written to storage.
    const res = await fetch('/api/assets', { method: 'POST', body });
    const { urls } = await res.json();

    editor.AssetManager.add(urls);
  },
},
  1. GrapesJS Asset Manager
  2. Next.js API
  3. Your storage
  4. CDN
Uploads leave the browser once and land in a route you wrote.
  • Upload through your own route handler so the session cookie, rate limit and audit log all apply.
  • Allow external URLs for teams that already host images elsewhere.
  • Preload the user's existing library so the picker opens on their assets, not an empty panel.
  • Validate the MIME type and the size on the server. The editor's file input is a suggestion, not a control.
  • Serve from a CDN and store the CDN URL in the project, so published pages never hit your origin for media.
  • Paginate large libraries — the asset panel will happily try to render ten thousand thumbnails.
Data model

Project Data vs HTML and CSS

The editor produces two different things and they are not interchangeable. Storing the wrong one is the mistake that turns "edit your page" into "start over".

Project data

A JSON document describing components, styles, pages and assets. This is the editable source — the only artefact that can restore an editing session exactly as the user left it.

HTML

The rendered content of the canvas. Perfect for serving to visitors, lossy as a source: the component types, traits and editor state are gone.

CSS

The stylesheet the editor generated, including the rules for each breakpoint. Served alongside the HTML, regenerated whenever you publish.

Both artefacts, from one editorts
// Project data — the editable source. This is what you store.
const project = editor.getProjectData();

// HTML and CSS — the rendered output. Generate this when you publish.
const html = editor.getHtml();
const css = editor.getCss();

// Reopening an editing session needs the project data, not the HTML:
editor.loadProjectData(project);

Editing

  1. User edits
  2. Project data
  3. Database

Rendering

  1. Project data
  2. HTML + CSS
  3. Preview / publish

Store project data for editing. Generate HTML/CSS when you need to publish or render content.

Workflow

From Draft to Published Page

  1. 1
    Edit

    The user works in the editor

    Changes accumulate in the project data. Nothing a visitor sees has moved yet.

  2. 2
    Save draft

    Autosave writes the project

    Your storage adapter posts the project JSON to your API on a debounce or a step count.

  3. 3
    Preview

    Render the draft the way a visitor would

    A private route that generates HTML and CSS from the draft project — the same code path publishing uses, without the write.

  4. 4
    Approval

    Whoever owns the page signs it off

    Optional, and worth adding the first time somebody publishes a broken pricing table.

  5. 5
    Publish

    Freeze the output

    Generate HTML and CSS, sanitise, store as a new version, and point the live slug at it.

  6. 6
    Rollback

    Point the slug at an earlier version

    Cheap if every publish was an insert rather than an update. Expensive to add later.

app/api/publish/route.tsts
import { NextResponse } from 'next/server';
import sanitizeHtml from 'sanitize-html';

import { publishPage } from '@/lib/projects';
import { requireProjectAccess } from '@/lib/auth';

export async function POST(request: Request) {
  const { projectId, html, css } = await request.json();

  // Permissions are enforced here, not by hiding a button in the editor.
  const user = await requireProjectAccess(projectId);

  if (typeof html !== 'string' || typeof css !== 'string') {
    return NextResponse.json({ error: 'Invalid payload' }, { status: 400 });
  }

  // Sanitise on the way in, once — not on every render.
  const page = await publishPage({
    projectId,
    html: sanitizeHtml(html),
    css,
    publishedBy: user.id,
  });

  return NextResponse.json({ url: `/p/${page.slug}` });
}
Catalogue

Extend Your Next.js Page Builder with Plugins

GrapesJS provides the editor engine. Plugins add the specialised functionality a specific product needs — and most of what a page builder still lacks after the first week already exists as one.

Browse GrapesJS Plugins
Marketplace

Plugins for a Next.js Build

Live listings from the GJS.Market catalogue, grouped by the question a Next.js team asks after the editor mounts.

Scope control

Don't Build Every Feature Yourself

Plugins do not eliminate development work. They change what kind of work it is, and that difference compounds over the life of the product.
Written in-house

Build it yourself

Every capability you write becomes a permanent line item.

    From the catalogue

    Install a plugin

    Somebody has already solved the generic half of your editor.

      Use the GrapesJS core for the editor and add only the capabilities your product actually needs.

      The decision

      Build a Next.js Page Builder From Scratch or Use GrapesJS?

      Capability by capability, what you would be signing up to write. "Extensible" means the library provides the interface and the default, and expects you to point it at your own backend.

      CapabilityBuild yourselfGrapesJS
      CanvasBuildIncluded
      Drag & dropBuildIncluded
      ComponentsBuildIncluded
      BlocksBuildIncluded
      StylingBuildIncluded
      Responsive editingBuildIncluded
      AssetsBuildExtensible
      StorageBuildExtensible
      CommandsBuildIncluded
      PluginsBuild ecosystemPlugin architecture

      Verified against the published GrapesJS API on 2026-09-02.

      Next.js gives you the application framework. GrapesJS gives you the visual editing engine.

      Commercial products

      Build a SaaS Page Builder with Next.js

      A SaaS page builder is your application with an editor inside it. Almost everything that makes it a business — accounts, teams, limits, billing, domains — is Next.js work you would be doing anyway. The editor is one route.

      NEXT.JS

      • Authentication
      • Organizations
      • Users
      • Permissions
      • Billing
      • Editor

      GRAPESJS

      PROJECT API

      • Database
      • Publishing
      • Multi-user: several people editing different projects, and eventually the same one.
      • Permissions: who may edit, who may publish, who may only look. Enforced server-side.
      • Organisations and teams: projects belong to an account, not to a person.
      • Billing: plan limits expressed as counts of projects, pages, seats or published domains.
      • Publishing: the step where your product takes responsibility for what goes live.
      • White-label: your panels, your icons, your colours — the editor should not look bolted on.

      The editor is one route in your application. Everything around it is what you are actually selling.

      Before you ship

      Performance and Security Considerations

      Two lists worth reading before the first real user opens the editor, not after.

      Performance

      Keep the editor out of the fast path

      GrapesJS is a substantial client-side library. That is fine on an authoring route and expensive everywhere else.

      • Lazy-load GrapesJS so its chunk is fetched with the editor route, not the app shell.
      • Initialise only when the editor is actually needed — not on a dashboard that merely links to it.
      • Keep the editor instance in a ref, out of React state.
      • Avoid re-rendering the component that owns the editor; the GrapesJS lifecycle is not React's.
      • Lazy-load heavy plugins rather than registering all of them at init.
      • Paginate large asset collections instead of loading the whole library into the panel.

      The editor is an authoring tool. You usually don't need the full editor runtime on every published page.

      Security

      Treat editor output as user input

      Because it is. Anything the canvas produced arrived over the network from a browser you do not control.

      • Sanitise generated or user-provided HTML before storing it, wherever it will be rendered as markup.
      • Validate uploaded assets server-side: MIME type, size, and what you are willing to serve back.
      • Authenticate every storage endpoint. A project ID in a URL is not authorisation.
      • Enforce permissions server-side; a hidden publish button is a UI preference, not a control.
      • Validate the shape of project data before writing it, and again before rendering from it.
      • Protect publishing endpoints separately from saving — they have different blast radii.
      • Set a Content Security Policy for the routes that render published markup.

      Never trust client-side editor state. The editor is a convenience for the author, not a boundary.

      Troubleshooting

      Common Next.js + GrapesJS Mistakes

      Almost every integration failure reported for this stack is one of the following seven.

      • Mistake

        Initializing GrapesJS inside a Server Component

        Server Components have no hooks, no effects and no DOM. The import may resolve, but there is nowhere for the editor to mount.

        Fix

        Use a Client Component — put 'use client' at the top of the module that owns the editor.

      • Mistake

        Initializing before the DOM exists

        Calling init() during render, or against a ref that is still null, fails because the container element has not been created yet.

        Fix

        Initialize after mount, inside useEffect, and guard on the ref being present.

      • Mistake

        Running GrapesJS during SSR

        Any call to init() that reaches the server throws ReferenceError: document is not defined. The import alone is harmless.

        Fix

        Keep editor initialization client-side. An effect is enough; dynamic imports are an optimisation, not the fix.

      • Mistake

        Putting the editor instance into React state

        The editor is a large mutable object that changes constantly. Storing it in state schedules re-renders that achieve nothing.

        Fix

        Use a ref. Keep state for things the UI actually renders, like a saving indicator.

      • Mistake

        Re-rendering the editor unnecessarily

        A parent re-render that recreates props or remounts the container tears down and rebuilds the whole canvas.

        Fix

        Keep the GrapesJS lifecycle separate from normal React rendering — mount once, then drive it through its own API.

      • Mistake

        Storing only HTML

        HTML is the output, not the source. Reopening a project from HTML loses component types, traits and editor state, so the next edit starts from a flattened page.

        Fix

        Persist project data if users need to continue editing. Generate HTML on publish.

      • Mistake

        Running GrapesJS on every public page

        Shipping the editor runtime to visitors who cannot edit anything costs bundle size, memory and Core Web Vitals for no return.

        Fix

        Separate editor runtime from published content — render published pages as plain HTML and CSS.

      Roadmap

      Start Small, Then Scale

      Three scopes, each adding to the one before it. Most teams overshoot on the first and are surprised by the third.

      1. 1Week one

        MVP

        Prove the editing experience is right before you build anything around it.

        • GrapesJS mounted in a client component
        • A handful of basic blocks
        • Local or single-project storage
        • Simple publishing to one route
        Start here
      2. 2First real users

        Production

        Everything that turns a demo into something a customer can rely on.

        • Authentication on every editor route
        • Backend storage behind your own API
        • Asset uploads and a media library
        • Custom components matching your design system
        • Permissions enforced server-side
        • Versioning and a publishing workflow
        See the storage section
      3. 3Commercial

        SaaS

        The product layer around the editor, which is what you actually charge for.

        • Organisations and teams
        • Billing and plan limits
        • White-label editor UI
        • A plugin set curated for your users
        • A template library
        • Analytics on published pages
        SaaS page builder

      The path from prototype to commercial product runs through your application, not through the editor.

      FAQ

      Frequently Asked Questions

      Can I use GrapesJS with Next.js?

      Yes. GrapesJS is a client-side library that mounts into a DOM element, so it runs inside any React application, including Next.js. Install grapesjs, initialise it in an effect inside a Client Component, and destroy it on unmount.

      Does GrapesJS work with the Next.js App Router?

      Yes, and the App Router is the recommended integration. Put 'use client' at the top of the component that owns the editor and initialise GrapesJS in useEffect. The route file itself can stay a Server Component.

      Does GrapesJS work with React Server Components?

      It works alongside them. The editor itself cannot be a Server Component — it needs hooks, effects and the DOM. Server Components handle auth, data fetching and the page shell, then pass serialisable props into the Client Component that owns the editor.

      Does GrapesJS support Next.js SSR?

      The component wrapping the editor can be server-rendered; the editor itself must initialise in the browser. grapesjs.init() reads document and throws in a Node runtime, so keep that call inside an effect. Published pages need no editor runtime at all and can be statically generated.

      Why does GrapesJS need a Client Component?

      Because it interacts with the DOM directly — measuring elements, attaching listeners and rendering an iframe canvas — and because it needs React hooks to manage its lifecycle. Neither is available in a Server Component.

      How do I initialize GrapesJS in Next.js?

      Create a Client Component with a ref on a container div, call grapesjs.init({ container }) inside useEffect with an empty dependency array, keep the returned editor in a second ref, and call editor.destroy() in the cleanup function.

      Should I use dynamic(..., { ssr: false })?

      In the Pages Router, yes — it is the standard way to skip prerendering. In the App Router it is optional and cannot be used inside a Server Component: Next.js rejects ssr: false there and asks you to move it into a Client Component. Use it in the App Router only to keep the editor chunk out of the initial payload.

      How do I save GrapesJS projects in Next.js?

      Register a custom storage adapter with editor.Storage.add(), whose load() and store() call a Next.js route handler. The route authenticates the request and reads or writes the project JSON in your database. Turn on autosave with stepsBeforeSave so writes are batched.

      Can I use Supabase with GrapesJS and Next.js?

      Yes, and there is an example on this page. Supabase is one implementation of the load/save boundary — Postgres, MySQL, Mongo, S3 or an existing REST API work the same way, because the editor only ever talks to your route.

      Can I build a SaaS page builder with Next.js and GrapesJS?

      Yes. GrapesJS is BSD-3-Clause licensed and self-hosted, with no per-seat fee and no hosted service in the loop, so you can embed it in a commercial product. The work is mostly the SaaS layer — organisations, permissions, billing, publishing — which is ordinary Next.js work.

      Can I create custom blocks?

      Yes. editor.Blocks.add() registers a block with a label, a category, an icon and the content it inserts. Blocks can insert raw HTML or instantiate one of your own component types.

      Can I create custom components?

      Yes. editor.Components.addType() defines a component type with its own model, traits, rules and child structure — the mechanism for exposing your design system inside the canvas while stopping users from breaking it.

      Can I export HTML and CSS?

      Yes. editor.getHtml() and editor.getCss() return the canvas output at any time, and getProjectData() returns the editable JSON source. Store the project data for editing; generate the HTML and CSS when you publish.

      Can I use GrapesJS plugins with Next.js?

      Yes. Plugins are functions that receive the editor instance, so they are registered the same way in Next.js as anywhere else — through the plugins option at init, or by calling the editor API from your onEditor handler.

      Can I build a CMS editor with Next.js?

      Yes, and it is one of the most common uses. Map GrapesJS project data onto your existing content model, restrict the block set to components your Next.js templates can render, and let editors work visually without touching the repository.

      Can I self-host a Next.js page builder?

      Yes. GrapesJS is an npm package with no hosted backend, licence server or telemetry, so the entire builder — editor, storage and published pages — runs wherever your Next.js application runs.

      Build Your Next.js Page Builder with GrapesJS

      Use Next.js for your application architecture and GrapesJS for the visual editing engine. Start with the core editor and extend your product with the plugins, blocks and integrations you need.

      Build the application with Next.js. Add visual editing with GrapesJS. Extend it with GJS.Market.