27 KiB
PageEditorPage
Overview
File: admin/src/pages/PageEditorPage.tsx
Route: /app/pages/:id/edit
Role Requirements: Any authenticated user (uses authenticate middleware)
Purpose: Provides a full-screen dual-mode editor for landing pages with visual WYSIWYG editing (GrapesJS) and raw HTML code editing (Monaco Editor). This page is the primary interface for creating and editing landing pages, supporting both drag-and-drop visual design for non-technical users and direct HTML/CSS editing for developers. The editor operates in full-screen mode without the AppLayout wrapper to maximize editing space.
Key Features:
- Dual editor modes (Visual/Code) toggled per page
- Full-screen editing interface (no AppLayout)
- GrapesJS visual editor with custom blocks
- Monaco Editor for raw HTML editing
- Real-time save with Ctrl+S keyboard shortcut
- Live preview for published pages
- Publish/unpublish toggle
- Mobile device detection with warning screen
- Auto-save on Ctrl+S in code mode
- Editor state managed via useRef for performance
Layout: Full-screen (no AppLayout wrapper)
Dependencies:
- Ant Design v5 (Button, Switch, Space, Typography, Tag, Spin, Grid, Result)
- Monaco Editor (@monaco-editor/react)
- GrapesJS (via GrapesJSEditor component wrapper)
- react-router-dom (useParams, useNavigate)
Features
1. Dual Editor Modes
Visual Mode (GrapesJS):
- Drag-and-drop interface
- Custom block library (loaded from API)
- Component tree navigation
- Style manager (CSS properties)
- Trait manager (component attributes)
- Asset manager (images, files)
- Canvas preview (desktop/tablet/mobile)
- Undo/redo
- Full-screen toggle
- Export HTML/CSS
Code Mode (Monaco Editor):
- Syntax highlighting for HTML
- Line numbers
- Word wrap enabled
- Auto-formatting
- Dark theme
- Minimap disabled for cleaner view
- Automatic layout adjustment
- Ctrl+S keyboard shortcut for save
- Direct HTML editing (no CSS/JS extraction)
Mode Selection:
- Set when creating page in LandingPagesPage
editorModefield: "VISUAL" or "CODE"- Cannot switch modes within editor (navigate back to pages list to change)
- Mode displayed as colored tag in toolbar
2. Toolbar Controls
Left Section:
- Back Button - Navigate to pages list
- Page Title - Current page name
- Slug Display - Public URL preview (/p/:slug)
- Mode Tag - Visual (green) or Code (blue)
Right Section:
- Published Toggle - Switch to enable/disable public access
- Live Tag - Visible when published
- Preview Button - Opens public page in new tab (only when published)
- Save Button - Manual save trigger (primary action)
3. Auto-Save & Keyboard Shortcuts
Code Mode Shortcuts:
- Ctrl+S / Cmd+S - Save page (prevents browser default)
- Keyboard event handler registered on mount
- Handler cleaned up on unmount
Visual Mode Save:
- Save button triggers
editorRef.current?.triggerSave() - GrapesJS editor handles internal save via forwardRef
4. Mobile Device Detection
Mobile Warning:
- Detects screen width < 768px (md breakpoint)
- Shows Result component with "Desktop Required" message
- "Back to Pages" button for navigation
- Prevents editor loading on mobile devices
- Different message for visual vs code mode
5. Loading & Error States
Loading State:
- Full-screen centered spinner
- Displayed while fetching page data + blocks
- Minimum height: 100vh
Error Handling:
- Failed fetch shows error message
- Auto-navigates back to pages list
- Prevents editor render on missing page
User Workflow
Opening a Page for Editing
-
Navigate to Pages List:
- Go to
/app/pages(LandingPagesPage) - View table of all landing pages
- Go to
-
Select Page to Edit:
- Click "Edit" button in page row
- Opens editor in full-screen mode
- URL changes to
/app/pages/:id/edit
-
Wait for Editor Load:
- Loading spinner appears
- Page data fetched from API
- Visual mode: block library also loaded
- Editor renders based on mode
Editing in Visual Mode
-
Use GrapesJS Interface:
- Add Components: Drag blocks from left sidebar onto canvas
- Move Components: Click and drag to reposition
- Edit Text: Double-click text to edit inline
- Style Components: Select component, use Style Manager in right panel
- Change Attributes: Use Trait Manager for component properties
- Upload Images: Use Asset Manager to add media
-
Canvas Controls:
- Toggle device preview (desktop/tablet/mobile)
- Toggle fullscreen mode
- Toggle borders/padding visualization
-
Save Changes:
- Click "Save" button in toolbar (or Ctrl+S)
- Editor extracts:
- Project data (component tree JSON)
- Rendered HTML output
- Compiled CSS styles
- All three sent to API via PUT request
- Success message: "Page saved"
Editing in Code Mode
-
Edit HTML Directly:
- Monaco editor displays current HTML output
- Edit HTML structure, inline styles, content
- Syntax highlighting for HTML tags
-
Save Changes:
- Press Ctrl+S (or Cmd+S on Mac)
- Or click "Save" button in toolbar
- Raw HTML content sent to API
- Success message: "Page saved"
-
Limitations:
- No visual preview within editor
- Must publish and use Preview button to see changes
- Changes don't update GrapesJS project data (one-way sync)
Publishing a Page
-
Toggle Published Switch:
- Switch in top-right toolbar
- Green = published, Gray = unpublished
- API updates
publishedfield immediately
-
When Published:
- "Live" tag appears next to switch
- "Preview" button becomes visible
- Page accessible at
/p/:slugURL
-
Preview Published Page:
- Click "Preview" button (eye icon)
- Opens new browser tab to
/p/:slug - Shows rendered page as public users see it
Component Breakdown
Main Component Structure
export default function PageEditorPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const screens = Grid.useBreakpoint();
const isMobile = !screens.md;
const { token } = theme.useToken();
// State
const [page, setPage] = useState<LandingPage | null>(null);
const [blocks, setBlocks] = useState<PageBlock[]>([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [codeContent, setCodeContent] = useState('');
const editorRef = useRef<GrapesJSEditorHandle>(null);
// Derived state
const isCodeMode = page?.editorMode === 'CODE';
// Fetch page + blocks (Visual mode only)
useEffect(() => {
const fetchData = async () => {
try {
if (isCodeMode) {
const pageRes = await api.get<LandingPage>(`/pages/${id}`);
setPage(pageRes.data);
setCodeContent(pageRes.data.htmlOutput || '');
} else {
const [pageRes, blocksRes] = await Promise.all([
api.get<LandingPage>(`/pages/${id}`),
api.get<PageBlock[]>('/page-blocks'),
]);
setPage(pageRes.data);
setBlocks(blocksRes.data);
setCodeContent(pageRes.data.htmlOutput || '');
}
} catch {
message.error('Failed to load page');
navigate('/app/pages');
} finally {
setLoading(false);
}
};
fetchData();
}, [id]);
// Ctrl+S keyboard shortcut (code mode only)
useEffect(() => {
if (!isCodeMode) return;
const handler = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
handleSaveCode();
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [isCodeMode, handleSaveCode]);
// Conditional render based on state
if (loading) return <Spin />;
if (!page) return null;
if (isMobile) return <MobileWarning />;
return (
<div style={{ height: '100vh' }}>
<Toolbar />
{isCodeMode ? <MonacoEditor /> : <GrapesJSEditor />}
</div>
);
}
Toolbar Component
Structure:
- Full-width sticky header
- Dark background (
colorBgBase) - Border bottom separator
- Two-column layout (Space components)
Left Section:
<Space>
<Button type="text" icon={<ArrowLeftOutlined />} onClick={goBack} />
<Text strong>{page.title}</Text>
<Text>/p/{page.slug}</Text>
<Tag color={isCodeMode ? 'blue' : 'green'}>
{isCodeMode ? 'Code' : 'Visual'}
</Tag>
</Space>
Right Section:
<Space>
<Space size={4}>
<Text>Published</Text>
<Switch checked={page.published} onChange={handleTogglePublished} />
</Space>
{page.published && <Tag color="green">Live</Tag>}
{page.published && (
<Button icon={<EyeOutlined />} onClick={() => window.open(`/p/${page.slug}`)}>
Preview
</Button>
)}
<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={handleSave}>
Save
</Button>
</Space>
GrapesJS Editor Integration
Component:
<GrapesJSEditor
ref={editorRef}
initialData={page.blocks as Record<string, unknown>}
onSave={handleSaveVisual}
customBlocks={blocks}
/>
Save Callback:
const handleSaveVisual = useCallback(async (data: {
projectData: Record<string, unknown>;
html: string;
css: string;
}) => {
setSaving(true);
try {
const { data: updated } = await api.put<LandingPage>(`/pages/${page.id}`, {
blocks: data.projectData,
htmlOutput: data.html,
cssOutput: data.css,
});
setPage(updated);
message.success('Page saved');
} catch {
message.error('Failed to save page');
} finally {
setSaving(false);
}
}, [page]);
Trigger Save (from parent):
editorRef.current?.triggerSave();
Monaco Editor Integration
Component:
<Editor
height="100%"
defaultLanguage="html"
theme="vs-dark"
value={codeContent}
onChange={(value) => setCodeContent(value ?? '')}
options={{
wordWrap: 'on',
minimap: { enabled: false },
fontSize: 14,
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
Save Handler:
const handleSaveCode = useCallback(async () => {
setSaving(true);
try {
const { data: updated } = await api.put<LandingPage>(`/pages/${page.id}`, {
htmlOutput: codeContent,
});
setPage(updated);
message.success('Page saved');
} catch {
message.error('Failed to save page');
} finally {
setSaving(false);
}
}, [page, codeContent]);
Mobile Warning Component
Conditional Render:
if (isMobile) {
return (
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
padding: 24,
background: token.colorBgBase,
}}>
<Result
status="info"
title="Desktop Required"
subTitle={`The ${isCodeMode ? 'code' : 'page'} editor requires a desktop browser. Please switch to a larger screen to edit this page.`}
extra={
<Button type="primary" onClick={() => navigate('/app/pages')}>
Back to Pages
</Button>
}
/>
</div>
);
}
Breakpoint Detection:
- Uses
Grid.useBreakpoint()hook isMobile = !screens.md(screen width < 768px)- Early return prevents editor initialization
State Management
Local Component State (useState)
// Page data
const [page, setPage] = useState<LandingPage | null>(null);
// Block library (Visual mode only)
const [blocks, setBlocks] = useState<PageBlock[]>([]);
// Loading state
const [loading, setLoading] = useState(true);
// Save in progress state
const [saving, setSaving] = useState(false);
// Monaco editor content (Code mode)
const [codeContent, setCodeContent] = useState('');
Refs (useRef)
// GrapesJS editor handle
const editorRef = useRef<GrapesJSEditorHandle>(null);
Why useRef?
- GrapesJS editor controlled externally
- Parent triggers save via
editorRef.current?.triggerSave() - No re-renders when editor state changes
- Performance optimization for large canvas
Derived State
// Computed from page data
const isCodeMode = page?.editorMode === 'CODE';
// Responsive breakpoint
const screens = Grid.useBreakpoint();
const isMobile = !screens.md;
// Theme tokens
const { token } = theme.useToken();
State Flow
-
Component Mounts:
loadingset to trueuseEffecttriggers fetch based on mode- Visual mode: parallel fetch page + blocks
- Code mode: fetch page only
- Sets
page,blocks,codeContent - Sets
loadingto false
-
User Edits Content:
- Visual mode: GrapesJS manages internal state
- Code mode: Monaco onChange updates
codeContent
-
User Saves:
- Visual mode:
editorRef.current?.triggerSave()→handleSaveVisualcallback - Code mode:
handleSaveCodedirectly - Sets
savingto true - API PUT request
- Updates
pagewith response - Sets
savingto false
- Visual mode:
-
User Toggles Published:
- API PUT request with
publishedfield - Updates
pagewith response - UI updates (Live tag, Preview button)
- API PUT request with
API Integration
Endpoints Used
- GET /api/pages/:id - Fetch page data
- GET /api/page-blocks - Fetch custom block library (Visual mode only)
- PUT /api/pages/:id - Update page (save, publish)
API Calls
1. Fetch Page + Blocks (Visual Mode)
const [pageRes, blocksRes] = await Promise.all([
api.get<LandingPage>(`/pages/${id}`),
api.get<PageBlock[]>('/page-blocks'),
]);
setPage(pageRes.data);
setBlocks(blocksRes.data);
setCodeContent(pageRes.data.htmlOutput || '');
LandingPage Response:
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"title": "Campaign Launch",
"slug": "campaign-launch",
"editorMode": "VISUAL",
"blocks": {
"pages": [...],
"styles": [...],
"components": [...]
},
"htmlOutput": "<html>...</html>",
"cssOutput": ".container { ... }",
"published": false,
"createdAt": "2025-02-10T12:00:00Z",
"updatedAt": "2025-02-10T14:30:00Z"
}
PageBlock Response:
[
{
"id": "block-hero",
"label": "Hero Section",
"category": "sections",
"content": "<div class='hero'>...</div>",
"media": "<svg>...</svg>",
"attributes": { "class": "gjs-block" }
},
...
]
2. Fetch Page Only (Code Mode)
const pageRes = await api.get<LandingPage>(`/pages/${id}`);
setPage(pageRes.data);
setCodeContent(pageRes.data.htmlOutput || '');
3. Save Visual Mode Changes
const { data: updated } = await api.put<LandingPage>(`/pages/${page.id}`, {
blocks: data.projectData,
htmlOutput: data.html,
cssOutput: data.css,
});
setPage(updated);
Request Body:
{
"blocks": {
"pages": [...],
"styles": [...],
"components": [...]
},
"htmlOutput": "<html>...</html>",
"cssOutput": ".container { ... }"
}
4. Save Code Mode Changes
const { data: updated } = await api.put<LandingPage>(`/pages/${page.id}`, {
htmlOutput: codeContent,
});
setPage(updated);
Request Body:
{
"htmlOutput": "<!DOCTYPE html>\n<html>...</html>"
}
5. Toggle Published
const { data: updated } = await api.put<LandingPage>(`/pages/${page.id}`, {
published: !page.published,
});
setPage(updated);
message.success(updated.published ? 'Page published' : 'Page unpublished');
Request Body:
{
"published": true
}
Code Examples
Complete Save Visual Mode Flow
const handleSaveVisual = useCallback(async (data: {
projectData: Record<string, unknown>;
html: string;
css: string;
}) => {
if (!page) return;
setSaving(true);
try {
const { data: updated } = await api.put<LandingPage>(`/pages/${page.id}`, {
blocks: data.projectData,
htmlOutput: data.html,
cssOutput: data.css,
});
setPage(updated);
message.success('Page saved');
} catch {
message.error('Failed to save page');
} finally {
setSaving(false);
}
}, [page]);
Keyboard Shortcut Handler
useEffect(() => {
if (!isCodeMode) return; // Only in code mode
const handler = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault(); // Prevent browser save dialog
handleSaveCode();
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler); // Cleanup
}, [isCodeMode, handleSaveCode]);
Conditional API Fetch Pattern
useEffect(() => {
const fetchData = async () => {
try {
if (isCodeMode) {
// Code mode: page only
const pageRes = await api.get<LandingPage>(`/pages/${id}`);
setPage(pageRes.data);
setCodeContent(pageRes.data.htmlOutput || '');
} else {
// Visual mode: page + blocks in parallel
const [pageRes, blocksRes] = await Promise.all([
api.get<LandingPage>(`/pages/${id}`),
api.get<PageBlock[]>('/page-blocks'),
]);
setPage(pageRes.data);
setBlocks(blocksRes.data);
setCodeContent(pageRes.data.htmlOutput || '');
}
} catch {
message.error('Failed to load page');
navigate('/app/pages');
} finally {
setLoading(false);
}
};
fetchData();
}, [id]); // Only re-fetch if page ID changes
Editor Ref Save Trigger
// Parent component
<Button
type="primary"
icon={<SaveOutlined />}
loading={saving}
onClick={() => {
if (isCodeMode) {
handleSaveCode();
} else {
editorRef.current?.triggerSave(); // Trigger GrapesJS save
}
}}
>
Save
</Button>
// GrapesJSEditor component
<GrapesJSEditor
ref={editorRef}
initialData={page.blocks}
onSave={handleSaveVisual} // Callback receives extracted data
customBlocks={blocks}
/>
Performance Considerations
1. Parallel API Requests (Visual Mode)
const [pageRes, blocksRes] = await Promise.all([
api.get<LandingPage>(`/pages/${id}`),
api.get<PageBlock[]>('/page-blocks'),
]);
Benefit: Reduces loading time by ~50% (2 sequential requests → 1 parallel batch).
2. Conditional Block Loading
if (isCodeMode) {
// Skip blocks fetch in code mode
const pageRes = await api.get<LandingPage>(`/pages/${id}`);
} else {
// Load blocks only for visual mode
const [pageRes, blocksRes] = await Promise.all([...]);
}
Benefit: Saves unnecessary API call in code mode (blocks not used).
3. useRef for Editor Handle
const editorRef = useRef<GrapesJSEditorHandle>(null);
Why useRef?
- GrapesJS editor has large internal state (component tree, styles, assets)
useRefprevents re-renders when editor state changes- Parent only needs to trigger save, not track editor state
- Performance critical for large page designs
4. useCallback for Save Handlers
const handleSaveVisual = useCallback(async (data) => {
// ...
}, [page]); // Only recreate when page changes
const handleSaveCode = useCallback(async () => {
// ...
}, [page, codeContent]); // Only recreate when deps change
Benefit: Prevents unnecessary function recreation on every render.
5. Early Mobile Detection
if (isMobile) {
return <MobileWarning />; // No editor initialization
}
Benefit: Skips heavy editor initialization on mobile devices (saves memory + CPU).
6. Automatic Monaco Layout
<Editor
options={{
automaticLayout: true, // Auto-adjust on window resize
minimap: { enabled: false }, // Disable minimap to save CPU
scrollBeyondLastLine: false, // Reduce DOM size
}}
/>
Benefit: Reduces Monaco memory footprint by disabling minimap (can use 100MB+ on large files).
Responsive Design
Mobile Detection
Breakpoint:
- Uses
Grid.useBreakpoint()hook - Mobile if
!screens.md(screen width < 768px)
Mobile Warning Screen:
if (isMobile) {
return (
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
padding: 24,
background: token.colorBgBase,
}}>
<Result
status="info"
title="Desktop Required"
subTitle={`The ${isCodeMode ? 'code' : 'page'} editor requires a desktop browser...`}
extra={
<Button type="primary" onClick={() => navigate('/app/pages')}>
Back to Pages
</Button>
}
/>
</div>
);
}
Why Mobile Warning?
- GrapesJS requires large screen for drag-and-drop UI (canvas + panels + toolbar)
- Monaco editor impractical on mobile keyboards
- Touch gestures conflict with editor interactions
- Better UX to redirect users to desktop device
Full-Screen Layout
No AppLayout Wrapper:
- Page routed outside AppLayout component
- Uses full viewport height (
100vh) - No sidebar navigation
- Maximizes editing canvas space
Layout Structure:
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
<Toolbar /> {/* Fixed header */}
<Editor /> {/* Flex-grow to fill remaining space */}
</div>
Accessibility
Keyboard Navigation
-
Tab Key:
- Cycles through toolbar buttons (Back, Save, Preview, Toggle)
- Enters editor focus (Monaco or GrapesJS canvas)
-
Ctrl+S / Cmd+S:
- Save shortcut (code mode only)
- Prevents browser default "Save Page As" dialog
-
GrapesJS Keyboard Shortcuts:
- Ctrl+Z: Undo
- Ctrl+Shift+Z: Redo
- Delete: Remove selected component
- Ctrl+C/V: Copy/paste components
-
Monaco Editor Shortcuts:
- Ctrl+S: Save (custom handler)
- Ctrl+F: Find
- Ctrl+H: Find and replace
- Ctrl+/: Toggle comment
- Alt+Up/Down: Move line up/down
ARIA Labels
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/app/pages')}
aria-label="Back to pages list"
/>
Screen Reader Announcements:
- Button labels announced via aria-label
- Switch state announced ("Published" / "Unpublished")
- Tag colors announced by screen readers
Focus Management
Toolbar Focus Order:
- Back button
- Published switch
- Preview button (if visible)
- Save button
Editor Focus:
- Monaco: automatic focus management via Monaco API
- GrapesJS: focus enters canvas on click
Troubleshooting
Problem: Editor Not Loading
Symptoms:
- Blank screen after loading spinner disappears
- Console errors related to GrapesJS or Monaco
Solutions:
-
Check page data in API response:
curl -H "Authorization: Bearer <token>" \ http://localhost:4000/api/pages/<page-id>- Verify
blocks,htmlOutput,editorModefields exist
- Verify
-
Check browser console:
- Open DevTools Console (F12)
- Look for JavaScript errors
- Common errors:
- "Cannot read property 'pages' of undefined" →
blocksfield missing/corrupt - "Monaco Editor failed to load" → CDN blocked or slow network
- "Cannot read property 'pages' of undefined" →
-
Clear browser cache:
- Monaco and GrapesJS cache resources
- Ctrl+Shift+R (hard refresh)
-
Check network tab:
- Verify API requests complete successfully
- Verify block library loads (Visual mode)
Problem: Save Button Not Working
Symptoms:
- Click Save button, no success message
- Loading spinner appears but never completes
- Console shows 400/500 errors
Solutions:
-
Check API request in Network tab:
- Look for PUT
/api/pages/:idrequest - Check request payload (should have
htmlOutput,blocks,cssOutput) - Check response status code
- Look for PUT
-
Visual Mode - Invalid blocks data:
- GrapesJS may generate invalid JSON
- Check console for serialization errors
- Try creating new page instead of editing corrupt one
-
Code Mode - Invalid HTML:
- API may validate HTML structure
- Check for missing closing tags
- Check for script injection attempts (blocked by CSP)
-
Network timeout:
- Large pages (>1MB HTML) may timeout
- Increase Axios timeout in
admin/src/lib/api.ts - Optimize HTML output (minify, remove unused CSS)
Problem: Ctrl+S Not Saving (Code Mode)
Symptoms:
- Press Ctrl+S, nothing happens
- Browser "Save Page As" dialog appears instead
Solutions:
-
Check browser focus:
- Ensure Monaco editor is focused (click inside editor)
- Keyboard handler requires window focus
-
Check browser extensions:
- Extensions may intercept Ctrl+S
- Test in incognito mode
- Disable extensions one by one
-
Mac users: Use Cmd+S instead of Ctrl+S
- Handler supports both
e.ctrlKeyande.metaKey
- Handler supports both
-
Manual save as fallback:
- Click "Save" button in toolbar
- Same effect as Ctrl+S
Problem: Published Page Not Accessible
Symptoms:
- Toggle "Published" switch to ON
- Navigate to
/p/:slug, get 404 error
Solutions:
-
Check slug uniqueness:
- Slug must be unique across all pages
- Check for URL conflicts with existing routes
-
Check page published status:
curl -H "Authorization: Bearer <token>" \ http://localhost:4000/api/pages/<page-id>- Verify
"published": truein response
- Verify
-
Check public route registration:
- Open
admin/src/App.tsx - Verify public route exists:
<Route path="/p/:slug" element={<LandingPage />} />
- Open
-
Check nginx routing:
- Public pages served through nginx
- Verify nginx reverse proxy configuration
-
Hard refresh public page:
- Ctrl+Shift+R to bypass cache
- Browser may cache 404 response
Problem: GrapesJS Not Loading Custom Blocks
Symptoms:
- Visual editor loads but block panel is empty
- Only default blocks visible (Text, Image, etc.)
Solutions:
-
Check blocks API response:
curl -H "Authorization: Bearer <token>" \ http://localhost:4000/api/page-blocks- Should return array of blocks with
label,content,media
- Should return array of blocks with
-
Check blocks passed to GrapesJS:
- Add console.log in PageEditorPage:
console.log('Custom blocks:', blocks); - Verify array not empty
- Add console.log in PageEditorPage:
-
Check GrapesJS block registration:
- Open
admin/src/components/GrapesJSEditor.tsx - Verify blocks registered in
editor.BlockManager.add()
- Open
-
Clear GrapesJS localStorage:
- GrapesJS caches project data
- Open DevTools → Application → Local Storage
- Delete keys starting with
gjsProject-
Related Documentation
- LandingPagesPage - Page list + create new page
- Landing Pages Feature - Full feature documentation
- Pages API - API endpoints
- GrapesJSEditor Component - Editor wrapper
- Block Library - Custom block system
- Public Landing Pages - Rendered page component
- MkDocs Export - Export to documentation site