1061 lines
29 KiB
Markdown

# GrapesJS Editor Integration
React wrapper component for GrapesJS WYSIWYG editor with forwardRef pattern, custom block registration, and keyboard shortcuts.
---
## Overview
The GrapesJS Editor component provides a production-ready integration of the GrapesJS page builder library into the Changemaker Lite admin interface. It handles initialization, plugin configuration, custom block registration, and save orchestration.
### Key Features
- **forwardRef Pattern**: Parent components trigger save via ref handle
- **Custom Block Library**: Register campaign-specific blocks from database
- **Plugin Ecosystem**: 10+ GrapesJS plugins pre-configured
- **Keyboard Shortcuts**: Ctrl+S (Cmd+S on Mac) to save
- **Error Boundary**: Graceful fallback on initialization failure
- **Mobile Detection**: Desktop-only warning for small screens
- **Video Block Support**: Placeholder generation for media library videos
---
## Architecture
```mermaid
graph TD
A[LandingPageEditor] -->|ref| B[GrapesJSEditor]
B -->|useImperativeHandle| C[triggerSave handle]
B --> D[grapesjs.init]
D --> E[Load Plugins]
E --> F[Register Custom Blocks]
F --> G[Load Initial Data]
G --> H[Canvas Ready]
A -->|handleSave| I[editorRef.current.triggerSave]
I --> J[Commands.run save-page]
J --> K[getProjectData + getHtml + getCss]
K --> L[onSave callback]
L --> M[API PUT /pages/:id]
style B fill:#9d4edd
style D fill:#3498db
style M fill:#2ecc71
```
**Flow:**
1. **Mount**: LandingPageEditor creates ref, renders GrapesJSEditor
2. **Init**: GrapesJSEditor calls `grapesjs.init()` → Loads plugins
3. **Blocks**: Registers custom blocks from PageBlock library
4. **Data**: Loads `initialData` (GrapesJS projectData JSON)
5. **Expose**: `useImperativeHandle` exposes `triggerSave()` method
6. **Save**: Parent calls `editorRef.current.triggerSave()` → Runs `save-page` command
7. **Callback**: GrapesJS extracts HTML/CSS → Calls `onSave()` → Parent saves to API
---
## Component API
### Props
```typescript
interface GrapesJSEditorProps {
initialData?: Record<string, unknown>;
onSave: (data: { projectData: Record<string, unknown>; html: string; css: string }) => void;
customBlocks?: PageBlock[];
}
```
**Fields:**
- **`initialData`** (optional): GrapesJS `projectData` JSON from previous save
- Contains components tree, styles, assets
- Empty object `{}` for new pages
- **`onSave`** (required): Callback when save triggered
- Receives `{ projectData, html, css }`
- Parent responsibility: Send to API
- **`customBlocks`** (optional): Array of PageBlock records from database
- Registered as draggable blocks in left panel
- See [Block Library](block-library.md) for schema
### Ref Handle
```typescript
interface GrapesJSEditorHandle {
triggerSave: () => void;
}
```
**Method:**
- **`triggerSave()`**: Programmatically trigger save command
- Extracts current editor state
- Calls `onSave` callback
- Used by parent's "Save" button or keyboard shortcut
### Usage Example
```typescript
import { useRef } from 'react';
import GrapesJSEditor, { GrapesJSEditorHandle } from '@/components/GrapesJSEditor';
function MyEditor() {
const editorRef = useRef<GrapesJSEditorHandle>(null);
const handleSave = async (data) => {
await api.put('/pages/123', {
blocks: data.projectData,
htmlOutput: data.html,
cssOutput: data.css,
});
};
const handleManualSave = () => {
editorRef.current?.triggerSave();
};
return (
<div>
<button onClick={handleManualSave}>Save</button>
<GrapesJSEditor
ref={editorRef}
initialData={page.blocks}
onSave={handleSave}
customBlocks={blocks}
/>
</div>
);
}
```
---
## GrapesJS Configuration
### Initialization Options
```typescript
const editor = grapesjs.init({
container: containerRef.current,
height: '100%',
width: 'auto',
storageManager: false, // No localStorage persistence (managed by API)
plugins: [
blocksBasicPlugin,
presetWebpagePlugin,
formsPlugin,
navbarPlugin,
countdownPlugin,
tabsPlugin,
typedPlugin,
customCodePlugin,
exportPlugin,
styleGradientPlugin,
touchPlugin,
],
pluginsOpts: {
[blocksBasicPlugin]: { flexGrid: true },
// ... other plugin options
},
canvas: {
styles: [
'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap',
],
},
});
```
**Key Settings:**
- **`storageManager: false`**: Disables auto-save to localStorage (we use API persistence)
- **`height: '100%'`**: Fills parent container (full-screen editor)
- **`canvas.styles`**: Injects Google Fonts into preview iframe
### Plugins Ecosystem
| Plugin | Purpose | Features |
|--------|---------|----------|
| `grapesjs-blocks-basic` | Basic blocks | Section, text, image, video, map, link, flexGrid |
| `grapesjs-preset-webpage` | Full page presets | Header, footer, hero templates |
| `grapesjs-plugin-forms` | Form components | Input, textarea, select, button, checkbox, radio |
| `grapesjs-navbar` | Navigation bars | Responsive navbar with dropdowns |
| `grapesjs-component-countdown` | Countdown timers | Event countdown with custom styling |
| `grapesjs-tabs` | Tab panels | Horizontal/vertical tab containers |
| `grapesjs-typed` | Typing animation | Typewriter text effect |
| `grapesjs-custom-code` | Embed raw HTML/JS | Custom code blocks (advanced users) |
| `grapesjs-plugin-export` | Export templates | ZIP download of HTML/CSS/assets |
| `grapesjs-style-gradient` | Gradient editor | Visual gradient picker for backgrounds |
| `grapesjs-touch` | Touch support | Mobile/tablet drag-and-drop (experimental) |
**Installation:**
```bash
cd admin && npm install \
grapesjs \
grapesjs-blocks-basic \
grapesjs-preset-webpage \
grapesjs-plugin-forms \
grapesjs-navbar \
grapesjs-component-countdown \
grapesjs-tabs \
grapesjs-typed \
grapesjs-custom-code \
grapesjs-plugin-export \
grapesjs-style-gradient \
grapesjs-touch
```
---
## Custom Blocks Registration
### Block Registration Flow
```mermaid
sequenceDiagram
participant API as API Database
participant Parent as LandingPageEditor
participant Editor as GrapesJSEditor
participant GJS as GrapesJS
Parent->>API: GET /api/page-blocks
API-->>Parent: PageBlock[]
Parent->>Editor: <GrapesJSEditor customBlocks={blocks} />
Editor->>Editor: useEffect(() => init)
Editor->>GJS: grapesjs.init()
GJS-->>Editor: editor instance
Editor->>Editor: Register custom blocks loop
loop For each block
Editor->>Editor: generateBlockHtml(type, defaults)
Editor->>GJS: BlockManager.add(id, config)
end
GJS-->>Editor: Blocks ready
```
### Block Generation Logic
```typescript
// From GrapesJSEditor.tsx
const blockManager = editor.Blocks;
for (const block of customBlocks) {
const defaults = block.defaults as Record<string, unknown>;
const html = generateBlockHtml(block.type, defaults);
blockManager.add(`custom-${block.type}`, {
label: block.label,
category: block.category || 'Campaign',
content: html,
});
}
```
**Example Block:**
```typescript
// From seed.ts
{
id: 'default-hero',
type: 'hero',
label: 'Hero Section',
category: 'Headers',
defaults: {
title: 'Welcome to Our Campaign',
subtitle: 'Join us in making a difference.',
ctaText: 'Get Involved',
ctaUrl: '#',
},
}
```
**Generated HTML:**
```html
<section style="padding: 80px 40px; text-align: center; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); color: #fff;">
<h1 style="font-size: 2.5rem; margin-bottom: 16px;">Welcome to Our Campaign</h1>
<p style="font-size: 1.25rem; opacity: 0.85; margin-bottom: 32px;">Join us in making a difference.</p>
<a href="#" style="display: inline-block; padding: 12px 32px; background: #9d4edd; color: #fff; text-decoration: none; border-radius: 6px; font-weight: 600;">Get Involved</a>
</section>
```
### Built-In Block Templates
**1. Hero Section**
```typescript
case 'hero':
return `
<section style="padding: 80px 40px; text-align: center; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); color: #fff;">
<h1 style="font-size: 2.5rem; margin-bottom: 16px;">${defaults.title || 'Hero Title'}</h1>
<p style="font-size: 1.25rem; opacity: 0.85; margin-bottom: 32px;">${defaults.subtitle || 'Subtitle text here'}</p>
<a href="${defaults.ctaUrl || '#'}" style="display: inline-block; padding: 12px 32px; background: #9d4edd; color: #fff; text-decoration: none; border-radius: 6px; font-weight: 600;">${defaults.ctaText || 'Get Started'}</a>
</section>`;
```
**2. Text Block**
```typescript
case 'text':
return `
<section style="padding: 60px 40px; max-width: 800px; margin: 0 auto;">
<h2 style="font-size: 1.75rem; margin-bottom: 16px;">${defaults.heading || 'Heading'}</h2>
<p style="font-size: 1rem; line-height: 1.7; opacity: 0.85;">${defaults.body || 'Body text goes here.'}</p>
</section>`;
```
**3. Features Grid**
```typescript
case 'features': {
const features = (defaults.features as Array<{ title: string; description: string }>) || [];
const featureHtml = features.map(f => `
<div style="flex: 1; min-width: 250px; padding: 24px; text-align: center;">
<h3 style="font-size: 1.25rem; margin-bottom: 8px;">${f.title}</h3>
<p style="opacity: 0.8;">${f.description}</p>
</div>`).join('');
return `
<section style="padding: 60px 40px;">
<div style="display: flex; flex-wrap: wrap; gap: 24px; justify-content: center;">
${featureHtml}
</div>
</section>`;
}
```
**4. Call to Action**
```typescript
case 'cta':
return `
<section style="padding: 60px 40px; text-align: center; background: linear-gradient(135deg, #9d4edd 0%, #7b2cbf 100%); color: #fff;">
<h2 style="font-size: 2rem; margin-bottom: 12px;">${defaults.heading || 'Call to Action'}</h2>
<p style="font-size: 1.1rem; margin-bottom: 24px; opacity: 0.9;">${defaults.description || 'Description here'}</p>
<a href="${defaults.buttonUrl || '#'}" style="display: inline-block; padding: 12px 32px; background: #fff; color: #9d4edd; text-decoration: none; border-radius: 6px; font-weight: 600;">${defaults.buttonText || 'Click Here'}</a>
</section>`;
```
**5. Video Block**
```typescript
case 'video': {
const videoId = defaults.videoId || 'PLACEHOLDER';
const playerType = defaults.playerType || 'standard';
return `
<section style="padding: 60px 40px;">
<div class="video-block"
data-video-id="${videoId}"
data-player-type="${playerType}"
data-autoplay="${defaults.autoplay || false}"
data-controls="${defaults.controls !== false}"
data-show-reactions="${defaults.showReactions !== false}"
style="max-width: 100%; margin: 0 auto;">
<div class="video-placeholder" style="aspect-ratio: 16/9; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center;">
<div style="text-align: center; color: #fff; padding: 24px;">
<svg style="width: 64px; height: 64px; margin-bottom: 16px; opacity: 0.9;" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" />
</svg>
<p style="margin: 0; font-size: 1.1rem; font-weight: 600;">Video Player</p>
<p style="margin: 8px 0 0; font-size: 0.9rem; opacity: 0.8;">ID: ${videoId}</p>
<p style="margin: 4px 0 0; font-size: 0.85rem; opacity: 0.7;">${playerType === 'advanced' ? 'Advanced Player (with reactions)' : 'Standard HTML5 Player'}</p>
<p style="margin: 12px 0 0; font-size: 0.75rem; opacity: 0.6; font-style: italic;">Video will render on published page</p>
</div>
</div>
</div>
</section>`;
}
```
---
## Save Command Integration
### Command Registration
```typescript
// In useEffect() after editor init
editor.Commands.add('save-page', {
run(ed: Editor) {
const projectData = ed.getProjectData() as Record<string, unknown>;
const html = ed.getHtml();
const css = ed.getCss() || '';
onSaveRef.current({ projectData, html, css });
},
});
```
**Why `onSaveRef`?**
- Avoids stale closure over `onSave` prop
- Parent can update callback without re-initializing editor
- Pattern: `const onSaveRef = useRef(onSave); onSaveRef.current = onSave;`
### Keyboard Shortcut
```typescript
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
editor.runCommand('save-page');
}
};
document.addEventListener('keydown', handleKeyDown);
// Cleanup
return () => {
document.removeEventListener('keydown', handleKeyDown);
editor.destroy();
};
```
**Shortcuts:**
- **Windows/Linux**: `Ctrl+S`
- **macOS**: `Cmd+S`
**Behavior:**
- Prevents browser's default "Save Page As..." dialog
- Triggers GrapesJS save command
- Calls `onSave` callback with current state
---
## forwardRef Pattern
### Implementation
```typescript
const GrapesJSEditor = forwardRef<GrapesJSEditorHandle, GrapesJSEditorProps>(
function GrapesJSEditor({ initialData, onSave, customBlocks }, ref) {
const editorRef = useRef<Editor | null>(null);
useImperativeHandle(ref, () => ({
triggerSave() {
editorRef.current?.runCommand('save-page');
},
}));
// ... rest of component
}
);
```
### Parent Usage
```typescript
// In LandingPageEditor.tsx
import { useRef } from 'react';
const editorRef = useRef<GrapesJSEditorHandle>(null);
const handleManualSave = () => {
editorRef.current?.triggerSave(); // Programmatic save
};
return (
<div>
<button onClick={handleManualSave}>Save</button>
<GrapesJSEditor ref={editorRef} onSave={handleSave} />
</div>
);
```
**Why forwardRef?**
- Decouples save trigger from GrapesJS internals
- Parent controls when to save (toolbar button, auto-save timer, etc.)
- Cleaner API than prop drilling `onManualSave` callback
---
## Error Handling
### Error Boundary State
```typescript
const [error, setError] = useState<string | null>(null);
try {
editor = grapesjs.init({ /* ... */ });
} catch (err) {
console.error('GrapesJS init error:', err);
setError('Failed to initialize the page editor. Please refresh the page.');
return;
}
if (error) {
return (
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#ff4d4f' }}>
{error}
</div>
);
}
```
**Failure Modes:**
1. **Missing plugin:** GrapesJS throws error during `init()`
2. **Browser incompatibility:** Old browser doesn't support ES6 modules
3. **Memory exhaustion:** Very large `initialData` crashes tab
**Recovery:**
- Error state shows user-friendly message
- No infinite re-render (error doesn't trigger re-init)
- User can refresh page or report issue
### Parent-Level Fallback
```typescript
// In LandingPageEditor.tsx
import { ErrorBoundary } from 'react-error-boundary';
<ErrorBoundary
fallback={<div>Editor failed to load. Please try CODE mode.</div>}
onReset={() => navigate('/app/pages')}
>
<GrapesJSEditor ref={editorRef} onSave={handleSave} />
</ErrorBoundary>
```
**Cascade:**
1. GrapesJS init error → Internal error state
2. React render error → ErrorBoundary catches
3. User sees fallback → Can switch to CODE mode
---
## Mobile Detection
### Desktop-Only Warning
**Location:** `LandingPageEditor.tsx` (parent component)
```typescript
import { Grid } from 'antd';
const screens = Grid.useBreakpoint();
const isMobile = !screens.md; // md = 768px
if (isMobile) {
return (
<Result
status="warning"
title="Desktop Required"
subTitle="The page editor requires a desktop or tablet device (minimum 768px width)."
extra={<Button onClick={() => navigate('/app/pages')}>Back to Pages</Button>}
/>
);
}
```
**Why desktop-only?**
- GrapesJS drag-and-drop requires precise mouse interactions
- Small screens can't fit 3-panel layout (blocks, canvas, properties)
- Touch support experimental (grapesjs-touch plugin unstable)
**Alternative for mobile admins:**
- Use CODE mode (Monaco editor works on mobile)
- Edit on desktop, preview on mobile
- Use responsive design testing tools
---
## Data Flow Patterns
### Initial Load
```mermaid
sequenceDiagram
participant DB as Database
participant API as API Service
participant Parent as LandingPageEditor
participant Editor as GrapesJSEditor
participant GJS as GrapesJS
Parent->>API: GET /api/pages/:id
API->>DB: SELECT blocks FROM landing_pages
DB-->>API: { blocks: {...} }
API-->>Parent: LandingPage JSON
Parent->>Editor: <GrapesJSEditor initialData={page.blocks} />
Editor->>GJS: editor.loadProjectData(initialData)
GJS-->>Editor: Canvas rendered
```
**Key Points:**
- `blocks` field contains full GrapesJS `projectData` (components tree, styles, assets)
- Empty object `{}` for new pages (GrapesJS shows blank canvas)
- Large JSON (50KB+) loads in ~200ms
### Save Flow
```mermaid
sequenceDiagram
participant User as User
participant Parent as LandingPageEditor
participant Editor as GrapesJSEditor
participant GJS as GrapesJS
participant API as API
User->>User: Press Ctrl+S
User->>Editor: KeyboardEvent
Editor->>GJS: runCommand('save-page')
GJS->>GJS: getProjectData()
GJS->>GJS: getHtml()
GJS->>GJS: getCss()
GJS-->>Editor: { projectData, html, css }
Editor->>Parent: onSave(data)
Parent->>API: PUT /api/pages/:id
API-->>Parent: 200 OK
Parent->>User: "Page saved" notification
```
**Critical Detail:**
- `getProjectData()` returns full editor state (for future edits)
- `getHtml()` returns rendered HTML (for public display)
- `getCss()` returns compiled CSS (for public display)
- All three saved to database (different use cases)
---
## Code Examples
### Complete Integration Example
```typescript
// admin/src/pages/LandingPageEditor.tsx
import { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button, message, Spin } from 'antd';
import { api } from '@/lib/api';
import GrapesJSEditor, { GrapesJSEditorHandle } from '@/components/GrapesJSEditor';
import type { LandingPage, PageBlock } from '@/types/api';
interface LandingPageEditorProps {
pageId: string;
onClose: () => void;
}
export default function LandingPageEditor({ pageId, onClose }: LandingPageEditorProps) {
const navigate = useNavigate();
const editorRef = useRef<GrapesJSEditorHandle>(null);
const [page, setPage] = useState<LandingPage | null>(null);
const [blocks, setBlocks] = useState<PageBlock[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const [pageRes, blocksRes] = await Promise.all([
api.get<LandingPage>(`/pages/${pageId}`),
api.get<PageBlock[]>('/page-blocks'),
]);
setPage(pageRes.data);
setBlocks(blocksRes.data);
} catch {
message.error('Failed to load page');
onClose();
} finally {
setLoading(false);
}
};
fetchData();
}, [pageId, onClose]);
const handleSave = async (data: { projectData: any; html: string; css: string }) => {
try {
await api.put(`/pages/${pageId}`, {
blocks: data.projectData,
htmlOutput: data.html,
cssOutput: data.css,
});
message.success('Page saved');
} catch {
message.error('Failed to save page');
}
};
const handleManualSave = () => {
editorRef.current?.triggerSave();
};
if (loading) return <Spin size="large" />;
if (!page) return null;
return (
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<div style={{ padding: '12px 16px', borderBottom: '1px solid #d9d9d9' }}>
<Button onClick={onClose}>Back</Button>
<Button type="primary" onClick={handleManualSave} style={{ marginLeft: 8 }}>
Save (Ctrl+S)
</Button>
</div>
<GrapesJSEditor
ref={editorRef}
initialData={page.blocks as Record<string, unknown>}
onSave={handleSave}
customBlocks={blocks}
/>
</div>
);
}
```
### Custom Block Registration
```typescript
// Add a custom "Campaign Stats" block
const campaignStatsBlock: PageBlock = {
id: 'custom-campaign-stats',
type: 'campaign-stats',
label: 'Campaign Stats',
category: 'Campaign',
sortOrder: 10,
schema: {
volunteers: { type: 'number', label: 'Volunteers' },
emails: { type: 'number', label: 'Emails Sent' },
events: { type: 'number', label: 'Events' },
},
defaults: {
volunteers: 1250,
emails: 5400,
events: 32,
},
};
// GrapesJSEditor will auto-register via generateBlockHtml()
<GrapesJSEditor customBlocks={[campaignStatsBlock, ...otherBlocks]} />
```
### Adding Custom HTML Generation
```typescript
// In GrapesJSEditor.tsx generateBlockHtml() function
case 'campaign-stats': {
const volunteers = defaults.volunteers || 0;
const emails = defaults.emails || 0;
const events = defaults.events || 0;
return `
<section style="padding: 60px 40px; background: #f8f9fa; text-align: center;">
<h2 style="margin-bottom: 32px; font-size: 2rem;">Our Impact</h2>
<div style="display: flex; gap: 48px; justify-content: center; flex-wrap: wrap;">
<div>
<div style="font-size: 3rem; font-weight: 700; color: #9d4edd;">${volunteers.toLocaleString()}</div>
<div style="font-size: 1rem; color: #666;">Volunteers</div>
</div>
<div>
<div style="font-size: 3rem; font-weight: 700; color: #9d4edd;">${emails.toLocaleString()}</div>
<div style="font-size: 1rem; color: #666;">Emails Sent</div>
</div>
<div>
<div style="font-size: 3rem; font-weight: 700; color: #9d4edd;">${events}</div>
<div style="font-size: 1rem; color: #666;">Events</div>
</div>
</div>
</section>`;
}
```
---
## Troubleshooting
### Problem: Blocks Not Appearing in Left Panel
**Symptoms:**
- Custom blocks array passed to GrapesJSEditor
- Left panel shows default blocks only
- No campaign-specific blocks
**Causes:**
1. `generateBlockHtml()` missing case for block type
2. Category name mismatch
3. Block registration timing issue
**Solutions:**
1. **Add case to generateBlockHtml():**
```typescript
case 'my-custom-block':
return `<section>My custom block HTML</section>`;
```
2. **Check category:**
```typescript
// Block category: "Campaign"
// GrapesJS shows blocks in collapsible "Campaign" section
// Case-sensitive match
```
3. **Verify registration timing:**
```typescript
// Registration happens in useEffect after init
console.log('Registering blocks:', customBlocks.length);
```
4. **Inspect BlockManager:**
```typescript
// In browser console (after editor loads)
window.editor.BlockManager.getAll().forEach(b => console.log(b.id));
// Should include 'custom-hero', 'custom-text', etc.
```
---
### Problem: Save Not Triggering
**Symptoms:**
- Press Ctrl+S → Nothing happens
- Manual save button doesn't work
- `onSave` callback never called
**Causes:**
1. Keyboard event listener not registered
2. forwardRef not working
3. `save-page` command not registered
**Solutions:**
1. **Check keyboard listener:**
```typescript
// In GrapesJSEditor useEffect
const handleKeyDown = (e: KeyboardEvent) => {
console.log('Key pressed:', e.key, 'Ctrl:', e.ctrlKey);
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
console.log('Save shortcut triggered');
e.preventDefault();
editor.runCommand('save-page');
}
};
```
2. **Verify ref handle:**
```typescript
// In parent component
console.log('Editor ref:', editorRef.current); // Should be { triggerSave: fn }
```
3. **Test command directly:**
```typescript
// In browser console (after editor loads)
window.editor.runCommand('save-page');
// Should trigger onSave callback
```
4. **Check onSaveRef pattern:**
```typescript
const onSaveRef = useRef(onSave);
onSaveRef.current = onSave; // Update on every render
```
---
### Problem: Editor Crashes on Large Pages
**Symptoms:**
- Loading page with 100+ components → Tab freezes
- GrapesJS UI unresponsive
- Save takes 10+ seconds
**Causes:**
- Too many components in single page
- Deep nesting (10+ levels)
- Heavy images without lazy loading
**Solutions:**
1. **Split into multiple pages:**
- Separate hero, features, testimonials into 3 pages
- Link pages via navigation
2. **Use CODE mode for complex layouts:**
- Write HTML directly → Faster than GrapesJS rendering
- Import via "Sync Overrides"
3. **Optimize images:**
- Use external CDN (not base64-encoded)
- Compress before upload
- Lazy load below fold
4. **Increase browser memory:**
- Chrome → `--max-old-space-size=4096`
- Edge → Similar flag
---
### Problem: Initial Data Not Loading
**Symptoms:**
- Editor opens with blank canvas
- `initialData` prop has data
- Console shows no errors
**Causes:**
1. `loadProjectData()` called before editor ready
2. Invalid JSON structure
3. Async timing issue
**Solutions:**
1. **Check editor ready state:**
```typescript
useEffect(() => {
if (!containerRef.current) return;
const editor = grapesjs.init({ /* ... */ });
// Wait for editor load event
editor.on('load', () => {
if (initialData && Object.keys(initialData).length > 0) {
editor.loadProjectData(initialData);
}
});
}, []);
```
2. **Validate JSON:**
```typescript
console.log('Loading data:', JSON.stringify(initialData, null, 2));
// Should have keys: assets, styles, pages
```
3. **Handle empty data:**
```typescript
if (initialData && Object.keys(initialData).length > 0) {
editor.loadProjectData(initialData);
} else {
console.log('Starting with blank canvas');
}
```
---
### Problem: Styles Not Applying in Canvas
**Symptoms:**
- Drag block to canvas → No background color
- Text has wrong font
- Layout broken
**Causes:**
1. Inline styles not supported
2. External stylesheet missing
3. Canvas iframe CSP issue
**Solutions:**
1. **Use inline styles in generateBlockHtml():**
```typescript
// Good
return `<section style="padding: 40px; background: #f00;">...</section>`;
// Bad (requires CSS injection)
return `<section class="hero">...</section>`;
```
2. **Inject fonts into canvas:**
```typescript
canvas: {
styles: [
'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap',
],
}
```
3. **Check iframe sandbox:**
```typescript
// GrapesJS canvas uses <iframe> — ensure no sandbox restrictions
// Default config works, but custom CSP may block
```
---
## Performance Optimization
### Lazy Loading
```typescript
// In LandingPageEditor.tsx
import { lazy, Suspense } from 'react';
const GrapesJSEditor = lazy(() => import('@/components/GrapesJSEditor'));
return (
<Suspense fallback={<Spin size="large" />}>
<GrapesJSEditor ref={editorRef} onSave={handleSave} />
</Suspense>
);
```
**Benefit:** Reduces initial bundle size by ~800KB (GrapesJS + plugins)
### Debounced Auto-Save
```typescript
import { useRef, useEffect } from 'react';
const autoSaveTimerRef = useRef<ReturnType<typeof setTimeout>>();
const handleEditorChange = () => {
clearTimeout(autoSaveTimerRef.current);
autoSaveTimerRef.current = setTimeout(() => {
editorRef.current?.triggerSave();
}, 5000); // Auto-save after 5s of inactivity
};
useEffect(() => {
// Listen to editor change events
const editor = window.editor; // Access via global (not recommended for prod)
editor?.on('component:update', handleEditorChange);
editor?.on('style:update', handleEditorChange);
return () => {
clearTimeout(autoSaveTimerRef.current);
editor?.off('component:update', handleEditorChange);
editor?.off('style:update', handleEditorChange);
};
}, []);
```
**Trade-off:** More API calls vs. reduced data loss risk
---
## Related Documentation
### Components
- **[LandingPageEditor](/v2/frontend/pages/LandingPageEditor)** — Full-screen editor wrapper
- **[LandingPagesPage](/v2/frontend/pages/LandingPagesPage)** — Table view with edit links
### Features
- **[Page Builder](page-builder.md)** — Complete page builder system
- **[Block Library](block-library.md)** — Custom blocks database
- **[MkDocs Export](mkdocs-export.md)** — Export to documentation site
### External
- **[GrapesJS Docs](https://grapesjs.com/docs/)** — Official documentation
- **[GrapesJS API](https://grapesjs.com/docs/api/)** — JavaScript API reference
- **[GrapesJS Plugins](https://grapesjs.com/docs/plugins/)** — Plugin ecosystem