# 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 `` 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 'Custom Page
' > 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 `