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.
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 & drop — GrapesJS core
Canvas — GrapesJS core
Component tree — GrapesJS core
Selection & hover — GrapesJS core
Blocks — GrapesJS core
Styling — GrapesJS core
Responsive editing — GrapesJS core
Undo / redo — GrapesJS core
Asset management — GrapesJS core
Serialization — GrapesJS core
Commands — GrapesJS core
Plugin system — GrapesJS core
Storage — GrapesJS core
Multi-page projects — Plugin or extension
Template library — Plugin or extension
Users & permissions — Your Next.js app
Publishing — Your 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.
Outcomes
What Can You Build with a Next.js Page Builder?
The same editor engine backs very different products. What changes between them is the blocks you register, who is allowed to publish, and where the output goes.
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.
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 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
Server Component
↓
Page / data
↓
Client Component
↓
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
GrapesJS
↓
Project data
↓
Database
Serving
Publish
↓
HTML + CSS
↓
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.
GrapesJS
↓→
Project data
↓→
Next.js API
↓→
Your backend
↓→
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.
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.
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.
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);
},
},
GrapesJS Asset Manager
↓
Next.js API
↓
Your storage
↓
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
User edits
↓
Project data
↓
Database
Rendering
Project data
↓
HTML + CSS
↓
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
Edit
The user works in the editor
Changes accumulate in the project data. Nothing a visitor sees has moved yet.
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
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
Approval
Whoever owns the page signs it off
Optional, and worth adding the first time somebody publishes a broken pricing table.
5
Publish
Freeze the output
Generate HTML and CSS, sanitise, store as a new version, and point the live slug at it.
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.
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.
Capability
Build yourself
GrapesJS
Canvas
Build
Included
Drag & drop
Build
Included
Components
Build
Included
Blocks
Build
Included
Styling
Build
Included
Responsive editing
Build
Included
Assets
Build
Extensible
Storage
Build
Extensible
Commands
Build
Included
Plugins
Build ecosystem
Plugin 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.
1Week one
MVP
Prove the editing experience is right before you build anything around it.
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.