# Page Builder Complete WYSIWYG landing page builder with GrapesJS editor, slug-based public routing, and MkDocs Material theme integration. --- ## Overview The Page Builder system provides a comprehensive solution for creating custom landing pages without coding. Administrators can use a visual drag-and-drop interface or write raw HTML/CSS directly. ### Key Features - **Dual-Mode Editing**: Switch between VISUAL (GrapesJS drag-and-drop) and CODE (raw HTML editor) - **Slug-Based Routing**: Public pages accessible at `/p/:slug` (e.g., `/p/about-us`) - **MkDocs Export**: Publish pages to MkDocs documentation site with Material theme integration - **SEO Meta Tags**: Configure title, description, and Open Graph images - **Custom Blocks**: Reusable components (hero, features, CTA, testimonials, contact forms) - **Video Integration**: Embed media library videos with standard or advanced players - **Mobile Detection**: Editor warns users on small screens (desktop-only editing) ### Architecture Overview ```mermaid graph LR A[Admin] --> B[LandingPagesPage] B --> C[Create Page Modal] C --> D[LandingPageEditor] D --> E[GrapesJS Editor] E --> F[Save API] F --> G[(LandingPage Model)] G --> H[Public Route] H --> I[/p/:slug] D --> J[Publish Toggle] J --> K[MkDocs Export] K --> L[overrides/*.html] K --> M[docs/*.md stub] style E fill:#9d4edd style G fill:#3498db style K fill:#2ecc71 ``` **Flow:** 1. Admin creates page via LandingPagesPage 2. Editor loads with GrapesJS (VISUAL mode) or Monaco (CODE mode) 3. Admin drags blocks, configures properties, saves (Ctrl+S) 4. API stores `projectData` (GrapesJS JSON), `htmlOutput`, `cssOutput` 5. On publish: API exports `.html` override + `.md` stub to MkDocs 6. Public users access page at `/p/:slug` (React route renders HTML) --- ## Database Models ### LandingPage **Table:** `landing_pages` **Key Fields:** | Field | Type | Description | |-------|------|-------------| | `id` | String (UUID) | Primary key | | `slug` | String | Unique URL-safe identifier (auto-generated from title) | | `title` | String | Page title (internal + fallback SEO) | | `description` | String? | Page description (internal) | | `editorMode` | Enum | `VISUAL` (GrapesJS) or `CODE` (raw HTML) | | `blocks` | JSON | GrapesJS `projectData` (components tree) | | `htmlOutput` | String? | Rendered HTML (cached output from editor) | | `cssOutput` | String? | Rendered CSS (cached output from editor) | | `mkdocsPath` | String? | Override file path (e.g., `about.html`) | | `mkdocsStubPath` | String? | Stub Markdown path (e.g., `about.md`) | | `mkdocsExportMode` | Enum | `THEMED` (extends main.html) or `STANDALONE` (full HTML) | | `mkdocsHideNav` | Boolean | Hide navigation sidebar in MkDocs | | `mkdocsHideToc` | Boolean | Hide table of contents in MkDocs | | `mkdocsSkipExport` | Boolean | Don't export to MkDocs (only accessible via /p/:slug) | | `published` | Boolean | Public visibility (false = draft) | | `seoTitle` | String? | Custom SEO title (overrides `title`) | | `seoDescription` | String? | Meta description for search engines | | `seoImage` | String? | Open Graph image URL | | `createdAt` | DateTime | Creation timestamp | | `updatedAt` | DateTime | Last modification timestamp | **Indexes:** - `slug` (unique) - `published` (filter index) **Relationships:** - None (standalone model) ### PageBlock See [Block Library](block-library.md) documentation. --- ## API Endpoints ### Admin Routes **Prefix:** `/api/pages` **Authentication:** Requires admin role (SUPER_ADMIN, INFLUENCE_ADMIN, or MAP_ADMIN) #### List Pages ```http GET /api/pages?page=1&limit=20&search=campaign&published=true ``` **Query Parameters:** - `page` (number, default: 1) — Page number - `limit` (number, default: 20, max: 100) — Results per page - `search` (string?) — Search title, description, or slug (case-insensitive) - `published` (string?) — Filter by status: `"true"`, `"false"`, or omit for all **Response:** ```json { "pages": [ { "id": "abc123", "slug": "about-us", "title": "About Our Campaign", "description": "Learn more about our mission.", "editorMode": "VISUAL", "blocks": { /* GrapesJS JSON */ }, "htmlOutput": "
...
", "cssOutput": "section { padding: 40px; }", "mkdocsPath": "about.html", "mkdocsStubPath": "about.md", "mkdocsExportMode": "THEMED", "mkdocsHideNav": false, "mkdocsHideToc": true, "mkdocsSkipExport": false, "published": true, "seoTitle": "About Us | Campaign 2026", "seoDescription": "Join our movement for change.", "seoImage": "https://example.com/og-image.jpg", "createdAt": "2026-01-15T10:00:00Z", "updatedAt": "2026-02-13T14:30:00Z" } ], "pagination": { "page": 1, "limit": 20, "total": 5, "totalPages": 1 } } ``` #### Get Page ```http GET /api/pages/:id ``` **Response:** Single `LandingPage` object (same structure as list item above) **Errors:** - `404 PAGE_NOT_FOUND` — Page doesn't exist #### Create Page ```http POST /api/pages Content-Type: application/json { "title": "New Landing Page", "description": "Page description", "editorMode": "VISUAL" } ``` **Request Body:** - `title` (string, required) — Page title (slug auto-generated) - `description` (string?) — Internal description - `editorMode` (enum?, default: `VISUAL`) — `VISUAL` or `CODE` - `mkdocsPath` (string?) — Custom override path (defaults to `{slug}.html`) **Response:** Created `LandingPage` object (201 status) **Errors:** - `400 INVALID_MKDOCS_PATH` — Invalid path (traversal attempt, missing .html extension) **Behavior:** - Slug auto-generated from title (lowercased, spaces→hyphens, alphanumeric only) - Slug collision handling (appends `-2`, `-3`, etc.) - `blocks` initialized as empty JSON object - `published` defaults to `false` #### Update Page ```http PUT /api/pages/:id Content-Type: application/json { "blocks": { /* GrapesJS projectData */ }, "htmlOutput": "
...
", "cssOutput": "section { padding: 40px; }", "published": true } ``` **Request Body:** (all fields optional) - `title` (string?) — New title (regenerates slug if changed) - `description` (string?) - `blocks` (JSON?) — GrapesJS `projectData` - `htmlOutput` (string?) — Rendered HTML - `cssOutput` (string?) — Rendered CSS - `published` (boolean?) — Publish status - `mkdocsPath` (string?) — Custom override path - `mkdocsExportMode` (enum?) — `THEMED` or `STANDALONE` - `mkdocsHideNav` (boolean?) - `mkdocsHideToc` (boolean?) - `mkdocsSkipExport` (boolean?) - `seoTitle` (string?) - `seoDescription` (string?) - `seoImage` (string?) **Response:** Updated `LandingPage` object **Errors:** - `404 PAGE_NOT_FOUND` — Page doesn't exist - `400 INVALID_MKDOCS_PATH` — Invalid path **Side Effects:** - **On publish (published=true, mkdocsSkipExport=false):** Exports to MkDocs (writes `.html` + `.md` stub) - **On unpublish or mkdocsSkipExport=true:** Removes MkDocs files - **On title change:** Regenerates slug, updates `mkdocsPath` if it was auto-generated, cleans up old exports #### Delete Page ```http DELETE /api/pages/:id ``` **Response:** 204 No Content **Errors:** - `404 PAGE_NOT_FOUND` — Page doesn't exist **Side Effects:** - Removes MkDocs exports (`.html` override + `.md` stub) if they exist #### Sync Overrides ```http POST /api/pages/sync ``` **Purpose:** Import untracked `.html` files from `mkdocs/docs/overrides/` as CODE-mode pages. Useful for migrating hand-crafted HTML templates. **Response:** ```json { "imported": 2, "updated": 1, "stubs": 3 } ``` **Behavior:** 1. Scans `mkdocs/docs/overrides/` recursively for `.html` files 2. For untracked files: Creates new CODE-mode page (published=true) 3. For tracked CODE-mode pages: Updates `htmlOutput` from disk (disk wins) 4. For tracked VISUAL-mode pages: Skips (managed by GrapesJS) 5. Backfills missing `.md` stubs for published pages **Use Cases:** - Migrate legacy hand-coded landing pages - Import templates from designers - Sync after manual file system edits #### Validate Exports ```http POST /api/pages/validate ``` **Purpose:** Verify MkDocs exports exist on disk, repair if missing. **Response:** ```json { "validated": 10, "repaired": 2, "errors": [ { "pageId": "xyz789", "slug": "broken-page", "error": "EACCES: permission denied" } ] } ``` **Behavior:** 1. Queries all published, non-skipped pages with `mkdocsPath` 2. Checks if `.html` override and `.md` stub exist 3. Re-exports if either missing 4. Updates `mkdocsStubPath` if changed 5. Returns error list for manual intervention **Use Cases:** - Recover from accidental file deletion - Fix export state after container restarts - Audit before MkDocs rebuild ### Public Routes **Prefix:** `/api/pages` **Authentication:** None (public access) #### View Published Page ```http GET /api/pages/:slug/view ``` **Example:** ```http GET /api/pages/about-us/view ``` **Response:** ```json { "id": "abc123", "slug": "about-us", "title": "About Our Campaign", "htmlOutput": "
...
", "cssOutput": "section { padding: 40px; }", "seoTitle": "About Us | Campaign 2026", "seoDescription": "Join our movement for change.", "seoImage": "https://example.com/og-image.jpg", "createdAt": "2026-01-15T10:00:00Z", "updatedAt": "2026-02-13T14:30:00Z" } ``` **Errors:** - `404 PAGE_NOT_FOUND` — Page doesn't exist or is unpublished **Security:** - Only returns published pages (`published=true`) - Omits editor-only fields (`blocks`, `mkdocsPath`, etc.) --- ## Configuration ### Environment Variables ```bash # MkDocs integration MKDOCS_DOCS_PATH=/mkdocs/docs # Override path: ${MKDOCS_DOCS_PATH}/overrides/ # Stub path: ${MKDOCS_DOCS_PATH}/ (root of docs) ``` **Docker Volume:** ```yaml volumes: - ./mkdocs:/mkdocs:rw ``` **Note:** API container needs write access to export files. ### Site Settings **Feature Flag:** `ENABLE_LANDING_PAGES` **Location:** Admin → Settings → Features → Landing Pages **Default:** `true` **Effect:** Shows/hides "Pages" menu item in admin sidebar --- ## Admin Workflow ### Creating a Page 1. **Navigate:** Admin sidebar → Pages 2. **Click:** "Create Page" button 3. **Fill form:** - Title: `"About Us"` (slug auto-generated: `about-us`) - Description: `"Learn about our campaign"` (optional) - Editor Mode: `VISUAL` (default) or `CODE` 4. **Submit:** "Create & Edit" button 5. **Result:** Redirected to full-screen editor ### Visual Editing (VISUAL Mode) 1. **Editor opens:** GrapesJS interface with 3 panels: - Left: Block library (drag-and-drop components) - Center: Canvas (preview + inline editing) - Right: Properties panel (configure selected component) 2. **Add blocks:** Drag "Hero Section" from left panel to canvas 3. **Configure:** Click hero → Edit title/subtitle/CTA in right panel 4. **Save:** Press `Ctrl+S` (or `Cmd+S` on Mac) → API saves `projectData`, `htmlOutput`, `cssOutput` 5. **Close:** Click "X" or "Back to Pages" → Returns to table ### Code Editing (CODE Mode) 1. **Editor opens:** Split-view Monaco editors: - Left: HTML editor - Right: CSS editor (optional) 2. **Edit HTML:** Write raw HTML with Jinja2 template syntax (for MkDocs) 3. **Save:** Press `Ctrl+S` → API saves `htmlOutput`, `cssOutput` 4. **Close:** Click "Back to Pages" ### Publishing a Page **Option 1: From Table** 1. Locate page in table 2. Click "Publish" button in Actions column 3. Status tag changes: Draft → Published 4. Page accessible at `/p/{slug}` **Option 2: From Settings Modal** 1. Click gear icon (Settings) in Actions column 2. Settings modal opens 3. (Field not shown in modal — use table toggle) **Side Effects (on publish):** - If `mkdocsSkipExport=false`: Exports `.html` + `.md` to MkDocs - If `mkdocsSkipExport=true`: Only accessible via `/p/:slug` (no MkDocs export) ### Configuring SEO 1. Click gear icon (Settings) in Actions column 2. Fill SEO section: - **SEO Title:** Custom title for `` and Open Graph (defaults to `title`) - **SEO Description:** Meta description for search engines - **SEO Image:** Full URL to Open Graph image (e.g., `https://cdn.example.com/og.jpg`) 3. Click "Save" 4. Re-export to MkDocs if already published ### MkDocs Integration Settings **Access:** Page Settings modal → MkDocs Integration section **Fields:** 1. **Skip MkDocs Export** (checkbox) - When enabled: Page NOT exported to MkDocs site - Use case: Pages meant only for `/p/:slug` (not documentation) - Default: `false` (export enabled) 2. **Override Path** (text input) - Custom filename for override (e.g., `custom-about.html`) - Default: Auto-generated from slug (`{slug}.html`) - Validation: Must end with `.html`, no path traversal 3. **Full page MkDocs** (checkbox) - When enabled: Exports as STANDALONE (full `<!DOCTYPE html>` document) - When disabled: Exports as THEMED (wraps in `{% extends "main.html" %}`) - Default: `false` (THEMED) - Use case: Standalone pages with no MkDocs chrome (like `lander.html`) 4. **Hide navigation sidebar** (checkbox, only for THEMED mode) - Adds `hide: [navigation]` to `.md` stub front matter - Hides left sidebar on page - Default: `false` 5. **Hide table of contents** (checkbox, only for THEMED mode) - Adds `hide: [toc]` to `.md` stub front matter - Hides right sidebar on page - Default: `false` **Workflow:** 1. Edit page settings 2. Configure MkDocs options 3. Save settings 4. If published: API auto-exports with new settings 5. Rebuild MkDocs: Admin → Pages → "Build Site" button ### Syncing Overrides **Purpose:** Import hand-coded `.html` files from disk **Workflow:** 1. Place `.html` files in `mkdocs/docs/overrides/` (on Docker host) 2. Admin → Pages → "Sync Overrides" button 3. API scans directory, imports new files as CODE-mode pages 4. Table refreshes, new pages appear 5. Edit pages normally, publish as needed **Example:** ```bash # On Docker host echo '<h1>Custom Page</h1>' > mkdocs/docs/overrides/custom.html # In admin panel # Click "Sync Overrides" → 1 imported ``` ### Validating Exports **Purpose:** Verify MkDocs files exist, repair if missing **Workflow:** 1. Admin → Pages → "Validate Exports" button 2. API checks all published pages: - `.html` override exists? - `.md` stub exists? 3. Re-exports if either missing 4. Shows result: `Validated 10 pages: 2 repaired` **Use Cases:** - After container restart (volume mount issues) - After manual file deletion - Before rebuilding MkDocs site --- ## Public Workflow ### Viewing a Published Page 1. **User navigates:** `https://yoursite.com/p/about-us` 2. **React router:** Matches `/p/:slug` route → Loads `LandingPage.tsx` 3. **API call:** `GET /api/pages/about-us/view` 4. **Response:** Returns `htmlOutput`, `cssOutput`, SEO fields 5. **Render:** - Sets `document.title = seoTitle || title` - Updates meta description, Open Graph image - Injects `cssOutput` as `<style>` tag - Renders `htmlOutput` via `dangerouslySetInnerHTML` 6. **Video hydration:** Scans for `.video-block` divs, replaces placeholders with React VideoPlayer components ### SEO Meta Tags **Applied automatically on page load:** ```html <html> <head> <title>About Us | Campaign 2026
...
``` ### Video Embedding **Editor Placeholder:** ```html
``` **Runtime Hydration:** 1. `LandingPage.tsx` mounts → Scans for `.video-block` elements 2. Reads `data-*` attributes 3. Creates React root for each block 4. Renders `AdvancedVideoPlayer` or `VideoPlayer` component 5. Replaces placeholder with live player **Supported Attributes:** - `data-video-id` (required) — Media library video ID - `data-player-type` (`"standard"` or `"advanced"`, default: `"standard"`) - `data-width` (CSS value, default: `"100%"`) - `data-height` (CSS value, default: `"auto"`) - `data-autoplay` (`"true"` or `"false"`, default: `"false"`) - `data-controls` (`"true"` or `"false"`, default: `"true"`) - `data-show-reactions` (`"true"` or `"false"`, default: `"true"`, advanced player only) --- ## Code Examples ### Creating a Page (TypeScript) ```typescript import { api } from '@/lib/api'; async function createAboutPage() { const { data } = await api.post('/pages', { title: 'About Us', description: 'Learn about our campaign', editorMode: 'VISUAL', }); console.log('Created page:', data.slug); // "about-us" return data.id; } ``` ### Saving Editor State (GrapesJS) ```typescript // In LandingPageEditor component import { useRef } from 'react'; import GrapesJSEditor, { GrapesJSEditorHandle } from '@/components/GrapesJSEditor'; const editorRef = useRef(null); const handleSave = () => { editorRef.current?.triggerSave(); // Calls registered save command }; const handleEditorSave = async (data: { projectData: any; html: string; css: string }) => { await api.put(`/pages/${pageId}`, { blocks: data.projectData, htmlOutput: data.html, cssOutput: data.css, }); message.success('Page saved'); }; return ( ); ``` ### Fetching Published Page (Public Route) ```typescript import axios from 'axios'; async function loadLandingPage(slug: string) { try { const { data } = await axios.get(`/api/pages/${slug}/view`); // Set SEO document.title = data.seoTitle || data.title; // Inject CSS const style = document.createElement('style'); style.textContent = data.cssOutput || ''; document.head.appendChild(style); // Render HTML return data.htmlOutput; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 404) { throw new Error('Page not found or unpublished'); } throw error; } } ``` ### MkDocs Export Logic (Backend) ```typescript // From pages.service.ts function wrapInMaterialOverride(html: string, css: string | null): string { const styleBlock = css ? `` : ''; return `{% extends "main.html" %} {% block content %} ${styleBlock} ${html} {% endblock %} `; } async function exportToMkDocs(opts: ExportOptions): Promise { const { mkdocsPath, html, css, exportMode, title, seoTitle, seoDescription } = opts; // Write override template const filePath = path.join(MKDOCS_OVERRIDES, mkdocsPath); const content = exportMode === 'STANDALONE' ? wrapInStandaloneDocument(html, css, seoTitle || title, seoDescription) : wrapInMaterialOverride(html, css); await fs.writeFile(filePath, content, 'utf-8'); // Write .md stub const stubPath = mkdocsPath.replace(/\.html$/, '.md'); const stubContent = `--- template: ${mkdocsPath} title: "${seoTitle || title}" --- `; await fs.writeFile(path.join(MKDOCS_DOCS_ROOT, stubPath), stubContent, 'utf-8'); return stubPath; } ``` --- ## Troubleshooting ### Problem: GrapesJS Editor Not Loading **Symptoms:** - Blank screen in editor - Console error: `Cannot read property 'init' of undefined` **Causes:** - GrapesJS package not installed - CSS import missing - Plugin incompatibility **Solutions:** 1. **Verify installation:** ```bash cd admin && npm list grapesjs # Should show: grapesjs@0.21.x ``` 2. **Check CSS import:** ```typescript // In GrapesJSEditor.tsx import 'grapesjs/dist/css/grapes.min.css'; ``` 3. **Check browser console:** - Look for `grapesjs` variable in global scope - Verify all plugins loaded successfully 4. **Clear cache:** ```bash # In browser DevTools # Right-click Reload → Empty Cache and Hard Reload ``` --- ### Problem: Published Page Not Rendering **Symptoms:** - 404 error at `/p/my-page` - Page exists in database, `published=true` **Causes:** - React route not registered - Slug mismatch - Public route mounted incorrectly **Solutions:** 1. **Verify route registration:** ```typescript // In admin/src/App.tsx } /> ``` 2. **Check slug in URL:** - Slug is case-sensitive: `/p/About-Us` ≠ `/p/about-us` - Use lowercase, hyphenated: `/p/about-us` 3. **Test API directly:** ```bash curl http://localhost:4000/api/pages/about-us/view # Should return JSON, not 404 ``` 4. **Check published status:** ```sql SELECT slug, published FROM landing_pages WHERE slug = 'about-us'; -- published should be true ``` --- ### Problem: Mobile Warning Shows on Desktop **Symptoms:** - "Desktop Required" warning displays on 1920px screen - Editor won't load **Causes:** - Browser window width < 768px - Breakpoint detection failure - DevTools docked (reduces viewport width) **Solutions:** 1. **Check actual viewport width:** ```javascript // In browser console console.log(window.innerWidth); // Should be > 768 for desktop ``` 2. **Undock DevTools:** - Press F12 → Click ⋮ (three dots) → Dock to right/bottom → Undock - Increases available viewport width 3. **Verify breakpoint hook:** ```typescript // In PageEditorPage.tsx const screens = Grid.useBreakpoint(); const isMobile = !screens.md; // md = 768px ``` 4. **Test responsive mode:** - F12 → Toggle device toolbar (Ctrl+Shift+M) - Select "Responsive" → Set width to 1024px --- ### Problem: MkDocs Export Not Found **Symptoms:** - MkDocs site shows 404 for `/pages/about-us/` - Override file missing from `mkdocs/docs/overrides/` **Causes:** - Page not published - `mkdocsSkipExport=true` - Export path incorrect - MkDocs not rebuilt **Solutions:** 1. **Verify publish status:** ```sql SELECT slug, published, mkdocs_skip_export FROM landing_pages WHERE slug = 'about-us'; -- Both should be true/false appropriately ``` 2. **Check export path:** ```bash ls -la mkdocs/docs/overrides/about.html # Should exist if published and not skipped ``` 3. **Validate exports:** - Admin → Pages → "Validate Exports" button - Check repair count 4. **Rebuild MkDocs:** ```bash docker compose exec mkdocs mkdocs build # Or in admin: Pages → "Build Site" ``` 5. **Check template path in stub:** ```bash cat mkdocs/docs/about.md # Should show: template: about.html (NOT overrides/about.html) ``` --- ### Problem: Slug Collision on Create **Symptoms:** - Create page with title "About Us" → slug becomes `about-us-2` - Expected `about-us` but already taken **Causes:** - Existing page with same slug (possibly unpublished) - Soft-deleted page (if soft delete implemented) **Solutions:** 1. **Check existing pages:** ```sql SELECT id, title, slug, published FROM landing_pages WHERE slug LIKE 'about-us%'; ``` 2. **Delete duplicate:** - If old page is unwanted: Admin → Pages → Delete - New page can reuse slug 3. **Use unique title:** - Rename new page: "About Us 2026" → slug `about-us-2026` 4. **Manual slug override:** - After create: Edit page → Settings → Override Path → `about-us-custom.html` --- ### Problem: Video Block Not Hydrating **Symptoms:** - Video placeholder shows on published page - No player renders - Console error: `Invalid video ID: PLACEHOLDER` **Causes:** - `data-video-id="PLACEHOLDER"` not replaced - Video ID not numeric - Hydration script not running **Solutions:** 1. **Check video ID in editor:** - Open GrapesJS editor → Select video block - Properties panel → Video ID field should be numeric (e.g., `123`) - Not `PLACEHOLDER` 2. **Verify HTML output:** ```html
...
...
``` 3. **Check hydration script:** ```typescript // In LandingPage.tsx useEffect(() => { // Should scan for .video-block elements const videoBlocks = contentRef.current?.querySelectorAll('.video-block'); console.log('Found video blocks:', videoBlocks?.length); }, [page]); ``` 4. **Test video ID validity:** ```bash curl http://localhost:4100/api/media/videos/42 # Should return video metadata, not 404 ``` --- ## Performance Considerations ### Editor Initialization **GrapesJS startup:** ~500ms on modern desktop **Optimization strategies:** - Lazy load GrapesJS: `const GrapesJS = lazy(() => import('./GrapesJSEditor'))` - Show loading spinner during init - Preload on hover over "Edit" button ### Large Pages **Complexity threshold:** 100+ components **Symptoms:** - Laggy drag-and-drop - Slow save operations - Canvas rendering delay **Mitigations:** - Break into multiple pages (split hero + sections) - Use CODE mode for complex layouts - Minimize nested components ### htmlOutput Storage **Database overhead:** `htmlOutput` can be 50KB+ for complex pages **Considerations:** - Indexed by `published` for public queries (fast) - Not indexed by content (no full-text search on HTML) - Consider external storage for very large pages (future enhancement) ### Public Page Rendering **React hydration:** Video blocks hydrate after initial render (~100ms delay) **Performance tips:** - Use `dangerouslySetInnerHTML` for immediate HTML paint - Defer video hydration to `setTimeout(..., 100)` - Preload video metadata for above-fold players --- ## Security Considerations ### Admin-Authored HTML **Risk:** XSS via malicious HTML in editor **Mitigation:** - **Accepted risk:** Only admins can create/edit pages (trusted users) - **No user-supplied content:** Public users cannot edit landing pages - **Authentication required:** All write endpoints require admin role **Comment in code:** ```typescript // HTML/CSS is admin-authored via GrapesJS editor (not user-submitted content). // Only authenticated admins can create/edit pages, so XSS risk is accepted. return
; ``` ### Slug Validation **Attack vector:** Path traversal via slug injection **Protection:** ```typescript function generateSlug(title: string): string { return title .toLowerCase() .replace(/[^a-z0-9]+/g, '-') // Alphanumeric + hyphens only .replace(/^-+|-+$/g, '') // Trim leading/trailing hyphens .slice(0, 80); // Max 80 chars } ``` **Safe slugs:** `about-us`, `campaign-2026`, `contact` **Rejected:** `../etc/passwd`, ``, `../../admin` ### MkDocs Path Validation **Attack vector:** Write arbitrary files via path traversal in `mkdocsPath` **Protection:** ```typescript function validateMkdocsPath(mkdocsPath: string): void { if (mkdocsPath.includes('\0')) throw new Error('Null byte detected'); const normalized = path.normalize(mkdocsPath); if (normalized.includes('..') || path.isAbsolute(normalized)) { throw new Error('Path traversal not allowed'); } if (mkdocsPath.includes('%2e') || mkdocsPath.includes('%2E')) { throw new Error('Encoded path traversal not allowed'); } if (!mkdocsPath.endsWith('.html')) { throw new Error('Path must end with .html'); } } ``` **Safe paths:** `about.html`, `pages/contact.html` **Rejected:** `../../../etc/passwd.html`, `/etc/shadow.html`, `%2e%2e/admin.html` ### Published Flag Enforcement **Attack vector:** Access draft pages via public route **Protection:** ```typescript // In pagesService.findBySlugPublic() if (!page || !page.published) { throw new AppError(404, 'Page not found', 'PAGE_NOT_FOUND'); } ``` **Behavior:** - Unpublished pages return 404 on public route - Admin routes bypass check (can view drafts) --- ## Related Documentation ### Frontend Components - **[LandingPageEditor](/v2/frontend/pages/LandingPageEditor)** — Full-screen editor wrapper - **[LandingPagesPage](/v2/frontend/pages/LandingPagesPage)** — Table view + CRUD - **[GrapesJSEditor](/v2/frontend/components/GrapesJSEditor)** — GrapesJS wrapper with forwardRef - **[PublicLandingPage](/v2/frontend/pages/public/LandingPage)** — Public page renderer ### Backend Modules - **[pages-admin.routes](/v2/backend/modules/pages/pages-admin.routes)** — Admin CRUD endpoints - **[pages-public.routes](/v2/backend/modules/pages/pages-public.routes)** — Public view endpoint - **[pages.service](/v2/backend/modules/pages/pages.service)** — Business logic + MkDocs export - **[pages.schemas](/v2/backend/modules/pages/pages.schemas)** — Zod validation schemas ### Database - **[LandingPage Model](/v2/database/models/pages)** — Schema + relationships - **[PageBlock Model](/v2/database/models/pages)** — Block library schema ### Feature Documentation - **[GrapesJS Editor Integration](grapes-editor.md)** — forwardRef pattern + custom blocks - **[Block Library](block-library.md)** — Reusable components system - **[MkDocs Export](mkdocs-export.md)** — Material theme integration ### External Resources - [GrapesJS Documentation](https://grapesjs.com/docs/) — Official editor docs - [GrapesJS Plugins](https://grapesjs.com/docs/plugins/) — Available plugins - [MkDocs Material](https://squidfunk.github.io/mkdocs-material/) — Theme docs - [Jinja2 Templates](https://jinja.palletsprojects.com/) — Template syntax