# Block Library Reusable page component system with JSON schema definitions, default values, and campaign-specific customization. --- ## Overview The Block Library provides a database-driven system for managing reusable page components (blocks) in the GrapesJS editor. Administrators can use pre-configured blocks or create custom ones tailored to their campaign needs. ### Key Features - **Database-Driven**: Blocks stored in PostgreSQL (PageBlock model) - **JSON Schema**: Define configurable properties for each block type - **Default Values**: Pre-populate blocks with campaign-specific content - **Category Organization**: Group blocks (Headers, Content, Actions, etc.) - **Sort Order**: Control block position in editor panel - **6 Default Blocks**: Hero, Text, Features, CTA, Testimonials, Contact Form - **Custom Blocks**: Create campaign-specific blocks via admin API --- ## Architecture ```mermaid graph LR A[(PageBlock Table)] -->|GET /api/page-blocks| B[API Service] B --> C[LandingPageEditor] C --> D[GrapesJSEditor] D --> E[BlockManager] E --> F[Left Panel] G[Admin] -->|POST /api/page-blocks| B G -->|Define Schema| H[JSON Schema] G -->|Set Defaults| I[Default Values] H --> A I --> A style A fill:#3498db style E fill:#9d4edd style F fill:#2ecc71 ``` **Flow:** 1. **Seed**: Default blocks created in `api/prisma/seed.ts` 2. **Fetch**: Editor loads all blocks via `GET /api/page-blocks` 3. **Register**: GrapesJSEditor registers each block with BlockManager 4. **Render**: Blocks appear in left panel (grouped by category) 5. **Customize**: Admin creates custom blocks via API (future enhancement) --- ## Database Model ### PageBlock Table **Schema:** ```typescript model PageBlock { id String @id @default(uuid()) type String @unique // Block type identifier (e.g., 'hero', 'text') label String // Display name in editor ("Hero Section") category String? // Group blocks ("Headers", "Content", "Actions") sortOrder Int @default(0) // Position in left panel schema Json // JSON schema for configurable properties defaults Json // Default values for schema fields thumbnail String? // Preview image URL (future enhancement) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([category, sortOrder]) } ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `id` | String (UUID) | Primary key | | `type` | String | Unique identifier (e.g., `"hero"`, `"features"`) | | `label` | String | Human-readable name shown in editor | | `category` | String? | Group blocks in collapsible sections | | `sortOrder` | Int | Order within category (lower = higher in list) | | `schema` | JSON | Property definitions (field name, type, label) | | `defaults` | JSON | Default values for each schema field | | `thumbnail` | String? | Preview image URL (not implemented) | **Indexes:** - `type` (unique) - `category + sortOrder` (composite, for sorted listing) --- ## Default Blocks ### 1. Hero Section **Type:** `hero` **Category:** Headers **Schema:** ```json { "title": { "type": "string", "label": "Title" }, "subtitle": { "type": "string", "label": "Subtitle" }, "backgroundImage": { "type": "string", "label": "Background Image URL" }, "ctaText": { "type": "string", "label": "Button Text" }, "ctaUrl": { "type": "string", "label": "Button URL" } } ``` **Defaults:** ```json { "title": "Welcome to Our Campaign", "subtitle": "Join us in making a difference in your community.", "backgroundImage": "", "ctaText": "Get Involved", "ctaUrl": "#" } ``` **Rendered HTML:** ```html

Welcome to Our Campaign

Join us in making a difference in your community.

Get Involved
``` --- ### 2. Text Block **Type:** `text` **Category:** Content **Schema:** ```json { "heading": { "type": "string", "label": "Heading" }, "body": { "type": "text", "label": "Body Text" } } ``` **Defaults:** ```json { "heading": "About Us", "body": "Tell your story here. Explain your mission, values, and what drives your campaign forward." } ``` **Rendered HTML:** ```html

About Us

Tell your story here. Explain your mission, values, and what drives your campaign forward.

``` --- ### 3. Features Grid **Type:** `features` **Category:** Content **Schema:** ```json { "features": { "type": "array", "label": "Features", "items": { "title": "string", "description": "string", "icon": "string" } } } ``` **Defaults:** ```json { "features": [ { "title": "Community Action", "description": "Organize local events and initiatives.", "icon": "" }, { "title": "Advocacy", "description": "Email your representatives directly.", "icon": "" }, { "title": "Volunteer", "description": "Sign up for shifts and make a difference.", "icon": "" } ] } ``` **Rendered HTML:** ```html

Community Action

Organize local events and initiatives.

Advocacy

Email your representatives directly.

Volunteer

Sign up for shifts and make a difference.

``` --- ### 4. Call to Action **Type:** `cta` **Category:** Actions **Schema:** ```json { "heading": { "type": "string", "label": "Heading" }, "description": { "type": "string", "label": "Description" }, "buttonText": { "type": "string", "label": "Button Text" }, "buttonUrl": { "type": "string", "label": "Button URL" } } ``` **Defaults:** ```json { "heading": "Ready to Take Action?", "description": "Join thousands of community members making their voices heard.", "buttonText": "Join Now", "buttonUrl": "#" } ``` **Rendered HTML:** ```html

Ready to Take Action?

Join thousands of community members making their voices heard.

Join Now
``` --- ### 5. Testimonials **Type:** `testimonials` **Category:** Content **Schema:** ```json { "quotes": { "type": "array", "label": "Quotes", "items": { "text": "string", "author": "string", "role": "string" } } } ``` **Defaults:** ```json { "quotes": [ { "text": "This platform made it so easy to contact my representatives.", "author": "Jane D.", "role": "Community Member" }, { "text": "I signed up for a volunteer shift and it changed my perspective.", "author": "Mark S.", "role": "Volunteer" } ] } ``` **Rendered HTML:** ```html

"This platform made it so easy to contact my representatives."

Jane D.

Community Member

"I signed up for a volunteer shift and it changed my perspective."

Mark S.

Volunteer

``` --- ### 6. Contact Form **Type:** `contact-form` **Category:** Actions **Schema:** ```json { "heading": { "type": "string", "label": "Heading" }, "fields": { "type": "array", "label": "Fields", "items": { "name": "string", "type": "string", "required": "boolean" } } } ``` **Defaults:** ```json { "heading": "Get in Touch", "fields": [ { "name": "name", "type": "text", "required": true }, { "name": "email", "type": "email", "required": true }, { "name": "message", "type": "textarea", "required": true } ] } ``` **Rendered HTML:** ```html

Get in Touch

``` **Note:** Form submission not wired (static HTML). Use grapesjs-plugin-forms for backend integration. --- ## API Endpoints ### Admin Routes **Prefix:** `/api/page-blocks` **Authentication:** Requires admin role (SUPER_ADMIN, INFLUENCE_ADMIN, or MAP_ADMIN) #### List Blocks ```http GET /api/page-blocks?category=Headers ``` **Query Parameters:** - `category` (string?) — Filter by category **Response:** ```json [ { "id": "default-hero", "type": "hero", "label": "Hero Section", "category": "Headers", "sortOrder": 1, "schema": { "title": { "type": "string", "label": "Title" }, "subtitle": { "type": "string", "label": "Subtitle" } }, "defaults": { "title": "Welcome to Our Campaign", "subtitle": "Join us in making a difference." }, "thumbnail": null, "createdAt": "2026-01-10T00:00:00Z", "updatedAt": "2026-01-10T00:00:00Z" } ] ``` **Sorting:** - Results ordered by `category ASC, sortOrder ASC` - Blocks in same category appear in sortOrder sequence #### Get Block ```http GET /api/page-blocks/:id ``` **Response:** Single `PageBlock` object **Errors:** - `404 BLOCK_NOT_FOUND` — Block doesn't exist #### Create Block ```http POST /api/page-blocks Content-Type: application/json { "type": "campaign-stats", "label": "Campaign Stats", "category": "Campaign", "sortOrder": 10, "schema": { "volunteers": { "type": "number", "label": "Volunteers" }, "emails": { "type": "number", "label": "Emails Sent" } }, "defaults": { "volunteers": 1250, "emails": 5400 } } ``` **Request Body:** - `type` (string, required) — Unique type identifier (alphanumeric + hyphens) - `label` (string, required) — Display name - `category` (string?) — Group name (default: `null`) - `sortOrder` (number?, default: 0) — Position in list - `schema` (JSON, required) — Property definitions - `defaults` (JSON, required) — Default values matching schema - `thumbnail` (string?) — Preview image URL **Response:** Created `PageBlock` object (201 status) **Errors:** - `400 VALIDATION_ERROR` — Invalid schema or type collision #### Update Block ```http PUT /api/page-blocks/:id Content-Type: application/json { "label": "Updated Label", "defaults": { "volunteers": 2000 } } ``` **Request Body:** (all fields optional except constraints) - `type` (string?) — Cannot change after creation (immutable) - `label` (string?) - `category` (string?) - `sortOrder` (number?) - `schema` (JSON?) - `defaults` (JSON?) **Response:** Updated `PageBlock` object **Errors:** - `404 BLOCK_NOT_FOUND` — Block doesn't exist - `400 VALIDATION_ERROR` — Invalid schema or defaults #### Delete Block ```http DELETE /api/page-blocks/:id ``` **Response:** 204 No Content **Errors:** - `404 BLOCK_NOT_FOUND` — Block doesn't exist **Side Effects:** - Pages using this block will still render (HTML is cached) - Block removed from editor panel for new pages --- ## Schema Format ### Property Types **Supported Types:** | Type | Description | Example | |------|-------------|---------| | `string` | Short text field | Title, subtitle, URL | | `text` | Multi-line text | Body paragraph | | `number` | Numeric value | Volunteer count, price | | `boolean` | True/false toggle | Show/hide element | | `array` | List of items | Features, testimonials | ### Simple Property ```json { "title": { "type": "string", "label": "Title" } } ``` **Rendered in GrapesJS:** Text input labeled "Title" ### Array Property ```json { "features": { "type": "array", "label": "Features", "items": { "title": "string", "description": "string", "icon": "string" } } } ``` **Rendered in GrapesJS:** - Repeatable item group - Add/remove buttons - Each item has 3 fields (title, description, icon) ### Defaults Matching **Schema:** ```json { "heading": { "type": "string", "label": "Heading" }, "count": { "type": "number", "label": "Count" } } ``` **Valid Defaults:** ```json { "heading": "Our Impact", "count": 42 } ``` **Invalid Defaults:** ```json { "heading": 123, // Type mismatch (should be string) "count": "foo" // Type mismatch (should be number) } ``` --- ## Admin Workflow ### Using Default Blocks 1. **Open Editor:** Admin → Pages → Click "Edit" on any page 2. **Locate Block:** Left panel → Expand "Headers" category 3. **Drag Block:** Drag "Hero Section" to canvas 4. **Configure:** Click block → Right panel shows properties - Title: `"Join the Movement"` - Subtitle: `"Together we can make a difference."` - CTA Text: `"Sign Up"` - CTA URL: `"/shifts"` 5. **Save:** Press `Ctrl+S` → Block HTML stored in database ### Creating Custom Blocks **Note:** Custom block creation UI not implemented. Use API directly. **Example: Campaign Stats Block** ```bash curl -X POST http://localhost:4000/api/page-blocks \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "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 } }' ``` **Result:** - New block appears in left panel under "Campaign" category - Dragging block inserts HTML (requires `generateBlockHtml` update) ### Updating Block Defaults **Use Case:** Update hero CTA text for all new pages ```bash curl -X PUT http://localhost:4000/api/page-blocks/default-hero \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "defaults": { "title": "Welcome to Our 2026 Campaign", "subtitle": "Join us in making a difference.", "ctaText": "Get Started Today", "ctaUrl": "/shifts" } }' ``` **Effect:** - New pages using hero block get updated defaults - Existing pages unchanged (HTML already rendered) --- ## Code Examples ### Fetching Blocks for Editor ```typescript import { api } from '@/lib/api'; import type { PageBlock } from '@/types/api'; async function loadBlocks(): Promise { const { data } = await api.get('/page-blocks'); return data.sort((a, b) => { // Sort by category, then sortOrder const catCompare = (a.category || '').localeCompare(b.category || ''); return catCompare !== 0 ? catCompare : a.sortOrder - b.sortOrder; }); } ``` ### Creating Custom Block ```typescript async function createCampaignStatsBlock() { const { data } = await api.post('/page-blocks', { 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, }, }); console.log('Created block:', data.id); return data; } ``` ### Extending generateBlockHtml() ```typescript // In admin/src/components/GrapesJSEditor.tsx function generateBlockHtml(type: string, defaults: Record): string { switch (type) { // ... existing cases ... case 'campaign-stats': { const volunteers = defaults.volunteers || 0; const emails = defaults.emails || 0; const events = defaults.events || 0; return `

Our Impact

${(volunteers as number).toLocaleString()}
Volunteers
${(emails as number).toLocaleString()}
Emails Sent
${events}
Events
`; } default: return `

Custom block: ${type}

`; } } ``` --- ## Troubleshooting ### Problem: Block Not Appearing in Editor **Symptoms:** - Created block via API - Not visible in left panel - Other blocks show correctly **Causes:** 1. GrapesJSEditor not re-fetching blocks 2. `generateBlockHtml()` missing case 3. Category name mismatch **Solutions:** 1. **Reload editor:** - Close page editor → Re-open - Blocks fetched on mount 2. **Add HTML generation case:** ```typescript case 'my-new-block': return `
My block HTML
`; ``` 3. **Check category:** ```sql SELECT category FROM page_blocks WHERE type = 'my-new-block'; -- Category should match GrapesJS panel (case-sensitive) ``` 4. **Verify API response:** ```bash curl -H "Authorization: Bearer $TOKEN" http://localhost:4000/api/page-blocks # Should include new block in response ``` --- ### Problem: Default Values Not Applying **Symptoms:** - Drag block to canvas → Fields are empty - Expected pre-filled title/subtitle **Causes:** 1. Defaults not matching schema keys 2. HTML template ignores defaults 3. Type mismatch (string vs number) **Solutions:** 1. **Verify defaults match schema:** ```json // Schema { "title": { "type": "string" } } // Defaults (good) { "title": "Welcome" } // Defaults (bad - key mismatch) { "heading": "Welcome" } ``` 2. **Check HTML template:** ```typescript // Good - uses defaults return `

${defaults.title || 'Fallback'}

`; // Bad - ignores defaults return `

Hardcoded Title

`; ``` 3. **Fix type mismatch:** ```typescript // If schema says "number", defaults must be number { "count": { "type": "number" } } { "count": 42 } // Good { "count": "42" } // Bad ``` --- ### Problem: Block HTML Not Rendering **Symptoms:** - Block appears in panel - Dragging to canvas shows nothing or error **Causes:** 1. `generateBlockHtml()` returns invalid HTML 2. Inline styles have syntax errors 3. Missing closing tags **Solutions:** 1. **Validate HTML:** ```typescript const html = generateBlockHtml('my-block', defaults); console.log(html); // Check for malformed tags ``` 2. **Test inline styles:** ```html
``` 3. **Use template literals carefully:** ```typescript // Ensure all ${} expressions return strings return `
${defaults.title || ''}
`; ``` --- ## Performance Considerations ### Block Count Impact **Threshold:** 50+ blocks in library **Symptoms:** - Slow editor initialization (~1s+) - Left panel laggy on scroll **Mitigations:** 1. **Category filtering:** - Only fetch blocks for specific category - Lazy-load categories on expand 2. **Pagination:** - Load first 20 blocks, fetch more on scroll - Not implemented in current version 3. **Caching:** - Store blocks in localStorage - Refresh only when version changes ### Schema Complexity **Issue:** Deeply nested array schemas (3+ levels) slow GrapesJS rendering **Example:** ```json { "sections": { "type": "array", "items": { "features": { "type": "array", "items": { "details": { "type": "array" } } } } } } ``` **Alternative:** Flatten structure or use CODE mode --- ## Security Considerations ### Admin-Only Access **Protection:** All `/api/page-blocks` endpoints require admin role ```typescript router.use(authenticate); router.use(requireRole(UserRole.SUPER_ADMIN, UserRole.INFLUENCE_ADMIN, UserRole.MAP_ADMIN)); ``` **Risk:** Malicious admin creates XSS block with `` --- ## Related Documentation ### Frontend Components - **[GrapesJSEditor](/v2/frontend/components/GrapesJSEditor)** — Block registration logic - **[LandingPageEditor](/v2/frontend/pages/LandingPageEditor)** — Fetches blocks for editor ### Backend Modules - **[blocks.routes](/v2/backend/modules/pages/blocks.routes)** — CRUD endpoints - **[blocks.service](/v2/backend/modules/pages/blocks.service)** — Business logic - **[pages.schemas](/v2/backend/modules/pages/pages.schemas)** — Zod schemas ### Database - **[PageBlock Model](/v2/database/models/pages)** — Schema + indexes ### Features - **[Page Builder](page-builder.md)** — Landing page system - **[GrapesJS Editor](grapes-editor.md)** — Editor integration ### Seed Data - **[api/prisma/seed.ts](https://github.com/changemaker-lite/changemaker.lite/blob/v2/api/prisma/seed.ts)** — Default blocks definition