Why GrapesJS fits Nuxt 3
GrapesJS needs the DOM, so in Nuxt 3 you initialise it in onMounted
(client-only) while Nitro server routes handle saving. This guide mounts the
editor, persists content, and exports HTML/CSS.
1. Mount the editor client-side
Create pages/editor.vue. Import GrapesJS dynamically inside
onMounted so it never runs during SSR.
<template>
<div>
<button @click="save">Save</button>
<div ref="el"><h1>Edit me in GrapesJS</h1></div>
</div>
</template>
<script setup>
import 'grapesjs/dist/css/grapes.min.css';
const el = ref(null);
let editor;
onMounted(async () => {
const grapesjs = (await import('grapesjs')).default;
editor = grapesjs.init({
container: el.value,
height: '100vh',
fromElement: true,
storageManager: false,
});
});
onBeforeUnmount(() => editor?.destroy());
async function save() {
await $fetch('/api/page', {
method: 'POST',
body: {
html: editor.getHtml(),
css: editor.getCss(),
project: editor.getProjectData(),
},
});
}
</script>
2. Add a Nitro server route
Create server/api/page.post.ts:
export default defineEventHandler(async (event) => {
const data = await readBody(event);
await savePage('home', data); // your DB write
return { status: 'ok' };
});
Add a matching server/api/page.get.ts to return the saved project,
then call editor.loadProjectData(saved) after init to reopen it.
3. Export the markup
const html = editor.getHtml();
const css = editor.getCss();
// `<style>${css}</style>${html}`
Common pitfalls in Nuxt 3
Nuxt's SSR is the usual culprit. A top-level import grapesjs from 'grapesjs' runs during server rendering and throws window is not defined — import it dynamically inside onMounted, or wrap the editor in <ClientOnly>. Persist through a Nitro server route in server/api with $fetch, keeping DB code on the server. If a dependency still complains during build, add it to build.transpile in nuxt.config. And call editor.destroy() in onBeforeUnmount so route changes don't leak editor instances.
Prerequisites
You'll need Node.js 18+ and a Nuxt 3 app. No Nuxt-specific GrapesJS package is
required — the editor is browser-only and Nitro server routes handle saving.
Familiarity with the Composition API, onMounted, and
server/api routes is enough.
Add custom blocks to the editor
Register draggable blocks with the Block Manager after init (inside onMounted):
editor.BlockManager.add('hero', {
label: 'Hero section',
category: 'Sections',
content: '<section class="hero"><h1>Headline</h1><p>Copy</p></section>',
});
Pull ready-made block libraries and presets from GJS.Market for a richer set.
Storage deep-dive: Nitro routes
Persist through Nitro server routes so DB code stays on the server:
// server/api/page.post.ts
export default defineEventHandler(async (event) => {
const data = await readBody(event);
await savePage('home', data);
return { status: 'ok' };
});
// server/api/page.get.ts returns the saved project; then
// editor.loadProjectData(saved) after init reopens it.
Performance tips
Import GrapesJS dynamically inside onMounted so it stays out of the
server render path and the main bundle, and call editor.destroy() in
onBeforeUnmount so route changes don't leak instances. If a dependency
complains at build time, add it to build.transpile in
nuxt.config.
Security considerations
Authenticate and authorise the Nitro save route before writing. Sanitise stored markup on output if non-admins can edit. Validate the payload size so a large project can't exhaust memory.
Troubleshooting common errors
“window is not defined” means GrapesJS ran during SSR — import it
dynamically inside onMounted or wrap the editor in
<ClientOnly>. A build error about a dependency
is usually fixed by adding it to build.transpile. An unstyled
or blank canvas means the stylesheet didn't load or the ref wasn't ready.
When to use GrapesJS with Nuxt 3
GrapesJS fits when your Nuxt app embeds a real visual page or email builder your users control, with your own storage and HTML output. For inline rich text, a lighter editor is enough; for full-page composition with layout, styling, and clean export, GrapesJS is the stronger, MIT-licensed, self-hosted choice.
Next steps
See the related GrapesJS + Vue and GrapesJS + Svelte guides, browse the GrapesJS marketplace, or start from the GJS.Market home page.
FAQ
Does GrapesJS work with Nuxt 3 SSR?
Yes — initialise it in onMounted or wrap it in
<ClientOnly> so it runs only in the browser.
How do I save GrapesJS content in Nuxt 3?
POST the project data with $fetch to a Nitro route in
server/api.
Why does my build fail with "window is not defined"?
GrapesJS is being imported at module top level and running during SSR. Import it
dynamically inside onMounted instead.