Add generate_page_content functionality for structured page content generation
- Introduced a new AI function `generate_page_content` to create structured content for website pages using JSON blocks. - Updated `AIEngine` to handle the new function and return appropriate messages for content generation. - Enhanced `PageGenerationService` to utilize the new AI function for generating page content based on blueprints. - Modified `prompts.py` to include detailed content generation requirements for the new function. - Updated site rendering logic to accommodate structured content blocks in various layouts.
This commit is contained in:
84
DEPLOYMENT_GUIDE.md
Normal file
84
DEPLOYMENT_GUIDE.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Site Deployment Guide
|
||||
|
||||
## How Site Deployment Works
|
||||
|
||||
### Overview
|
||||
When you deploy a site blueprint, the system:
|
||||
1. Builds a site definition (merging Content from Writer into PageBlueprint blocks)
|
||||
2. Writes it to the filesystem at `/data/app/sites-data/clients/{site_id}/v{version}/`
|
||||
3. The Sites Renderer reads from this filesystem to serve the public site
|
||||
|
||||
### Deployment Process
|
||||
|
||||
#### Step 1: Prepare Your Site
|
||||
1. **Generate Structure**: Create site blueprint with pages (has placeholder blocks)
|
||||
2. **Generate Content**: Pages → Tasks → Content (Writer generates real content)
|
||||
3. **Publish Content**: In Content Manager, set Content status to `'publish'` for each page
|
||||
4. **Publish Pages**: In Page Manager, set Page status to `'published'` (for navigation)
|
||||
|
||||
#### Step 2: Deploy the Blueprint
|
||||
**API Endpoint**: `POST /api/v1/publisher/deploy/{blueprint_id}/`
|
||||
|
||||
**What Happens**:
|
||||
- `SitesRendererAdapter.deploy()` is called
|
||||
- For each page, it finds the associated Writer Task
|
||||
- If Content exists and is published, it uses `Content.json_blocks` instead of blueprint placeholders
|
||||
- Builds complete site definition with all pages
|
||||
- Writes to filesystem: `/data/app/sites-data/clients/{site_id}/v{version}/site.json`
|
||||
- Creates deployment record
|
||||
|
||||
#### Step 3: Access Your Site
|
||||
**Public URL**: `https://sites.igny8.com/{siteSlug}`
|
||||
|
||||
**How It Works**:
|
||||
- Sites Renderer loads site definition from filesystem (or API fallback)
|
||||
- Shows navigation menu with all published pages
|
||||
- Home route (`/siteSlug`) shows only home page content
|
||||
- Page routes (`/siteSlug/pageSlug`) show specific page content
|
||||
|
||||
### Navigation Menu
|
||||
|
||||
The navigation menu automatically includes:
|
||||
- **Home** link (always shown)
|
||||
- All pages with status `'published'` or `'ready'` (excluding home)
|
||||
- Pages are sorted by their `order` field
|
||||
|
||||
### Page Routing
|
||||
|
||||
- **Homepage**: `https://sites.igny8.com/{siteSlug}` → Shows home page only
|
||||
- **Individual Pages**: `https://sites.igny8.com/{siteSlug}/{pageSlug}` → Shows that specific page
|
||||
- Example: `https://sites.igny8.com/auto-g8/products`
|
||||
- Example: `https://sites.igny8.com/auto-g8/blog`
|
||||
|
||||
### Content Merging
|
||||
|
||||
When deploying:
|
||||
- **If Content is published**: Uses `Content.json_blocks` (actual written content)
|
||||
- **If Content not published**: Uses `PageBlueprint.blocks_json` (placeholder blocks)
|
||||
- **Content takes precedence**: Published Content always replaces blueprint placeholders
|
||||
|
||||
### Important Notes
|
||||
|
||||
1. **Two Statuses**:
|
||||
- `PageBlueprint.status = 'published'` → Controls page visibility in navigation
|
||||
- `Content.status = 'publish'` → Controls which content is used (real vs placeholder)
|
||||
|
||||
2. **Redeploy Required**: After publishing Content, you must **redeploy** the site for changes to appear
|
||||
|
||||
3. **All Pages Deployed**: The deployment includes ALL pages from the blueprint, but only published pages show in navigation
|
||||
|
||||
4. **Navigation Auto-Generated**: If no explicit navigation is set in blueprint, it auto-generates from published pages
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**Problem**: Navigation only shows "Home"
|
||||
- **Solution**: Make sure other pages have status `'published'` or `'ready'` in Page Manager
|
||||
|
||||
**Problem**: Pages show placeholder content instead of real content
|
||||
- **Solution**:
|
||||
1. Check Content status is `'publish'` in Content Manager
|
||||
2. Redeploy the site blueprint
|
||||
|
||||
**Problem**: Pages not accessible via URL
|
||||
- **Solution**: Make sure pages have status `'published'` or `'ready'` and the site is deployed
|
||||
|
||||
65
SITE_BUILDER_WORKFLOW_EXPLANATION.md
Normal file
65
SITE_BUILDER_WORKFLOW_EXPLANATION.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Site Builder Workflow - Explanation & Fix
|
||||
|
||||
## The Problem (FIXED)
|
||||
|
||||
You were experiencing confusion because there were **TWO separate systems** that weren't properly connected:
|
||||
|
||||
1. **Page Blueprint** (`PageBlueprint.blocks_json`) - Contains placeholder/sample blocks from AI structure generation
|
||||
2. **Writer Content** (`Content` model) - Contains actual generated content from Writer tasks
|
||||
|
||||
**The disconnect**: When deploying a site, it only used `PageBlueprint.blocks_json` (placeholders), NOT the actual `Content` from Writer.
|
||||
|
||||
## Current Workflow (How It Works Now)
|
||||
|
||||
### Step 1: Structure Generation
|
||||
- AI generates site structure → Creates `SiteBlueprint` with `PageBlueprint` pages
|
||||
- Each `PageBlueprint` has `blocks_json` with **placeholder/sample blocks**
|
||||
|
||||
### Step 2: Content Generation
|
||||
- Pages are sent to Writer as `Tasks` (title pattern: `[Site Builder] {Page Title}`)
|
||||
- Writer generates actual content → Creates `Content` records
|
||||
- `Content` has `html_content` and `json_blocks` with **real content**
|
||||
|
||||
### Step 3: Publishing Status
|
||||
- **Page Blueprint Status** (`PageBlueprint.status`): Set to `'published'` in Page Manager
|
||||
- Controls if page appears in navigation and is accessible
|
||||
- **Content Status** (`Content.status`): Set to `'publish'` in Content Manager
|
||||
- Controls if actual written content is used (vs placeholders)
|
||||
|
||||
### Step 4: Deployment (FIXED)
|
||||
- When you deploy, `SitesRendererAdapter._build_site_definition()` now:
|
||||
1. For each page, finds the associated Writer Task (by title pattern)
|
||||
2. Finds the Content record for that task
|
||||
3. **If Content exists and status is 'publish'**, uses `Content.json_blocks` instead of `PageBlueprint.blocks_json`
|
||||
4. If Content has `html_content` but no `json_blocks`, converts it to a text block
|
||||
5. Uses the merged/actual content blocks for deployment
|
||||
|
||||
## URLs
|
||||
|
||||
- **Public Site URL**: `https://sites.igny8.com/{siteSlug}`
|
||||
- Shows deployed site with actual content (if Content is published)
|
||||
- Falls back to blueprint placeholders if Content not published
|
||||
- **Individual Pages**: `https://sites.igny8.com/{siteSlug}/{pageSlug}`
|
||||
- Example: `https://sites.igny8.com/auto-g8/products`
|
||||
|
||||
## How To Use It Now
|
||||
|
||||
1. **Generate Structure**: Create site blueprint with pages (has placeholder blocks)
|
||||
2. **Generate Content**: Pages → Tasks → Content (Writer generates real content)
|
||||
3. **Publish Content**: In Content Manager, set Content status to `'publish'`
|
||||
4. **Publish Pages**: In Page Manager, set Page status to `'published'` (for navigation)
|
||||
5. **Deploy Site**: Deploy the blueprint - it will automatically use published Content
|
||||
|
||||
## What Changed
|
||||
|
||||
✅ **Fixed**: `SitesRendererAdapter._build_site_definition()` now merges published Content into PageBlueprint blocks during deployment
|
||||
|
||||
✅ **Result**: When you deploy, the site shows actual written content, not placeholders
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Two Statuses**: Page status controls visibility, Content status controls which content is used
|
||||
- **Deployment Required**: After publishing Content, you need to **redeploy** the site for changes to appear
|
||||
- **Content Takes Precedence**: If Content is published, it replaces blueprint placeholders
|
||||
- **Fallback**: If Content not published, blueprint placeholders are used
|
||||
|
||||
185
TEMPLATE_SYSTEM_EXPLANATION.md
Normal file
185
TEMPLATE_SYSTEM_EXPLANATION.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# Template System - Current State & Plans
|
||||
|
||||
## Current Template Architecture
|
||||
|
||||
### 1. Site-Level Layouts (Implemented)
|
||||
**Location**: `sites/src/utils/layoutRenderer.tsx`
|
||||
|
||||
**Available Layouts**:
|
||||
- `default` - Standard header, content, footer
|
||||
- `minimal` - Clean, minimal design
|
||||
- `magazine` - Editorial, content-focused
|
||||
- `ecommerce` - Product-focused
|
||||
- `portfolio` - Showcase layout
|
||||
- `blog` - Content-first
|
||||
- `corporate` - Business layout
|
||||
|
||||
**How it works**:
|
||||
- Set in `SiteBlueprint.structure_json.layout`
|
||||
- Applied to the entire site
|
||||
- All pages use the same layout
|
||||
|
||||
### 2. Block Templates (Implemented)
|
||||
**Location**: `sites/src/utils/templateEngine.tsx`
|
||||
|
||||
**Available Block Types**:
|
||||
- `hero` - Hero section with title, subtitle, CTA
|
||||
- `text` - Text content block
|
||||
- `features` - Feature grid
|
||||
- `testimonials` - Testimonials section
|
||||
- `services` - Services grid
|
||||
- `stats` - Statistics panel
|
||||
- `cta` - Call to action
|
||||
- `image` - Image block
|
||||
- `video` - Video block
|
||||
- `form` - Contact form
|
||||
- `faq` - FAQ accordion
|
||||
- `quote` - Quote block
|
||||
- `grid` - Grid layout
|
||||
- `card` - Card block
|
||||
- `list` - List block
|
||||
- `accordion` - Accordion block
|
||||
|
||||
**How it works**:
|
||||
- Each page has `blocks_json` array
|
||||
- Each block has `type` and `data`
|
||||
- `renderTemplate()` renders blocks based on type
|
||||
|
||||
### 3. Page Types (Defined, but NO templates yet)
|
||||
**Location**: `backend/igny8_core/business/site_building/models.py`
|
||||
|
||||
**Page Type Choices**:
|
||||
- `home` - Homepage
|
||||
- `about` - About page
|
||||
- `services` - Services page
|
||||
- `products` - Products page
|
||||
- `blog` - Blog page
|
||||
- `contact` - Contact page
|
||||
- `custom` - Custom page
|
||||
|
||||
**Current State**:
|
||||
- Page types are stored but **NOT used for rendering**
|
||||
- All pages render the same way regardless of type
|
||||
- No page-type-specific templates exist
|
||||
|
||||
## Missing: Page-Type Templates
|
||||
|
||||
### What's Missing
|
||||
Currently, there's **NO page-type-specific template system**. All pages render identically:
|
||||
- Home page renders the same as Products page
|
||||
- Blog page renders the same as Contact page
|
||||
- No special handling for different page types
|
||||
|
||||
### Where Page-Type Templates Should Be
|
||||
|
||||
**Proposed Location**: `sites/src/utils/pageTypeRenderer.tsx`
|
||||
|
||||
**Proposed Structure**:
|
||||
```typescript
|
||||
// Page-type specific renderers
|
||||
function renderHomePage(page: PageDefinition, blocks: Block[]): React.ReactElement {
|
||||
// Home-specific template: Hero, features, testimonials, CTA
|
||||
}
|
||||
|
||||
function renderProductsPage(page: PageDefinition, blocks: Block[]): React.ReactElement {
|
||||
// Products-specific template: Product grid, filters, categories
|
||||
}
|
||||
|
||||
function renderBlogPage(page: PageDefinition, blocks: Block[]): React.ReactElement {
|
||||
// Blog-specific template: Post list, sidebar, pagination
|
||||
}
|
||||
|
||||
function renderContactPage(page: PageDefinition, blocks: Block[]): React.ReactElement {
|
||||
// Contact-specific template: Form, map, contact info
|
||||
}
|
||||
```
|
||||
|
||||
### How It Should Work
|
||||
|
||||
1. **In `layoutRenderer.tsx`**: After determining site layout, check page type
|
||||
2. **Route to page-type renderer**: If page type has specific template, use it
|
||||
3. **Fallback to default**: If no page-type template, use default block rendering
|
||||
|
||||
**Example Flow**:
|
||||
```
|
||||
Site Definition → Site Layout (default/minimal/etc.)
|
||||
↓
|
||||
Page Type (home/products/blog/etc.)
|
||||
↓
|
||||
Page-Type Template (if exists) OR Default Block Rendering
|
||||
↓
|
||||
Block Templates (hero/text/features/etc.)
|
||||
```
|
||||
|
||||
## Current Rendering Flow
|
||||
|
||||
```
|
||||
SiteRenderer
|
||||
↓
|
||||
loadSiteDefinition()
|
||||
↓
|
||||
renderLayout(siteDefinition) → Uses site.layout (default/minimal/etc.)
|
||||
↓
|
||||
renderDefaultLayout() → Renders all pages the same way
|
||||
↓
|
||||
renderTemplate(block) → Renders individual blocks
|
||||
```
|
||||
|
||||
## Proposed Enhanced Flow
|
||||
|
||||
```
|
||||
SiteRenderer
|
||||
↓
|
||||
loadSiteDefinition()
|
||||
↓
|
||||
renderLayout(siteDefinition) → Uses site.layout
|
||||
↓
|
||||
For each page:
|
||||
↓
|
||||
Check page.type (home/products/blog/etc.)
|
||||
↓
|
||||
If page-type template exists:
|
||||
→ renderPageTypeTemplate(page)
|
||||
Else:
|
||||
→ renderDefaultPageTemplate(page)
|
||||
↓
|
||||
renderTemplate(block) → Renders blocks
|
||||
```
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Create Page-Type Renderers
|
||||
- Create `sites/src/utils/pageTypeRenderer.tsx`
|
||||
- Implement templates for each page type:
|
||||
- `home` - Hero + features + testimonials layout
|
||||
- `products` - Product grid + filters
|
||||
- `blog` - Post list + sidebar
|
||||
- `contact` - Form + map + info
|
||||
- `about` - Team + mission + values
|
||||
- `services` - Service cards + descriptions
|
||||
|
||||
### Phase 2: Integrate with Layout Renderer
|
||||
- Modify `renderDefaultLayout()` to check page type
|
||||
- Route to page-type renderer if template exists
|
||||
- Fallback to current block rendering
|
||||
|
||||
### Phase 3: Make Templates Configurable
|
||||
- Allow templates to be customized per site
|
||||
- Store template preferences in `SiteBlueprint.config_json`
|
||||
- Support custom templates
|
||||
|
||||
## Current Files
|
||||
|
||||
- **Site Layouts**: `sites/src/utils/layoutRenderer.tsx`
|
||||
- **Block Templates**: `sites/src/utils/templateEngine.tsx`
|
||||
- **Page Types**: `backend/igny8_core/business/site_building/models.py` (PageBlueprint.PAGE_TYPE_CHOICES)
|
||||
- **Missing**: Page-type templates (not implemented yet)
|
||||
|
||||
## Summary
|
||||
|
||||
✅ **Implemented**: Site-level layouts, block templates
|
||||
❌ **Missing**: Page-type-specific templates
|
||||
📝 **Planned**: Page-type renderers in `sites/src/utils/pageTypeRenderer.tsx`
|
||||
|
||||
Currently, all pages render the same way. Page types are stored but not used for rendering. To add page-type templates, create a new file `pageTypeRenderer.tsx` and integrate it into the layout renderer.
|
||||
|
||||
Binary file not shown.
@@ -36,6 +36,8 @@ class AIEngine:
|
||||
return f"{count} task{'s' if count != 1 else ''}"
|
||||
elif function_name == 'generate_site_structure':
|
||||
return "1 site blueprint"
|
||||
elif function_name == 'generate_page_content':
|
||||
return f"{count} page{'s' if count != 1 else ''}"
|
||||
return f"{count} item{'s' if count != 1 else ''}"
|
||||
|
||||
def _build_validation_message(self, function_name: str, payload: dict, count: int, input_description: str) -> str:
|
||||
@@ -86,8 +88,11 @@ class AIEngine:
|
||||
blueprint_name = ''
|
||||
if isinstance(data, dict):
|
||||
blueprint = data.get('blueprint')
|
||||
blueprint_name = f"“{getattr(blueprint, 'name', '')}”" if blueprint and getattr(blueprint, 'name', None) else ''
|
||||
if blueprint and getattr(blueprint, 'name', None):
|
||||
blueprint_name = f'"{blueprint.name}"'
|
||||
return f"Preparing site blueprint {blueprint_name}".strip()
|
||||
elif function_name == 'generate_page_content':
|
||||
return f"Preparing {count} page{'s' if count != 1 else ''} for content generation"
|
||||
return f"Preparing {count} item{'s' if count != 1 else ''}"
|
||||
|
||||
def _get_ai_call_message(self, function_name: str, count: int) -> str:
|
||||
@@ -102,6 +107,8 @@ class AIEngine:
|
||||
return f"Creating image{'s' if count != 1 else ''} with AI"
|
||||
elif function_name == 'generate_site_structure':
|
||||
return "Designing complete site architecture"
|
||||
elif function_name == 'generate_page_content':
|
||||
return f"Generating structured page content"
|
||||
return f"Processing with AI"
|
||||
|
||||
def _get_parse_message(self, function_name: str) -> str:
|
||||
@@ -116,6 +123,8 @@ class AIEngine:
|
||||
return "Processing images"
|
||||
elif function_name == 'generate_site_structure':
|
||||
return "Compiling site map"
|
||||
elif function_name == 'generate_page_content':
|
||||
return "Structuring content blocks"
|
||||
return "Processing results"
|
||||
|
||||
def _get_parse_message_with_count(self, function_name: str, count: int) -> str:
|
||||
@@ -136,6 +145,8 @@ class AIEngine:
|
||||
return "Writing In‑article Image Prompts"
|
||||
elif function_name == 'generate_site_structure':
|
||||
return f"{count} page blueprint{'s' if count != 1 else ''} mapped"
|
||||
elif function_name == 'generate_page_content':
|
||||
return f"{count} page{'s' if count != 1 else ''} with structured blocks"
|
||||
return f"{count} item{'s' if count != 1 else ''} processed"
|
||||
|
||||
def _get_save_message(self, function_name: str, count: int) -> str:
|
||||
@@ -153,6 +164,8 @@ class AIEngine:
|
||||
return f"Assigning {count} Prompts to Dedicated Slots"
|
||||
elif function_name == 'generate_site_structure':
|
||||
return f"Publishing {count} page blueprint{'s' if count != 1 else ''}"
|
||||
elif function_name == 'generate_page_content':
|
||||
return f"Saving {count} page{'s' if count != 1 else ''} with content blocks"
|
||||
return f"Saving {count} item{'s' if count != 1 else ''}"
|
||||
|
||||
def execute(self, fn: BaseAIFunction, payload: dict) -> dict:
|
||||
|
||||
@@ -7,6 +7,7 @@ from igny8_core.ai.functions.generate_content import GenerateContentFunction
|
||||
from igny8_core.ai.functions.generate_images import GenerateImagesFunction, generate_images_core
|
||||
from igny8_core.ai.functions.generate_image_prompts import GenerateImagePromptsFunction
|
||||
from igny8_core.ai.functions.generate_site_structure import GenerateSiteStructureFunction
|
||||
from igny8_core.ai.functions.generate_page_content import GeneratePageContentFunction
|
||||
|
||||
__all__ = [
|
||||
'AutoClusterFunction',
|
||||
@@ -16,4 +17,5 @@ __all__ = [
|
||||
'generate_images_core',
|
||||
'GenerateImagePromptsFunction',
|
||||
'GenerateSiteStructureFunction',
|
||||
'GeneratePageContentFunction',
|
||||
]
|
||||
|
||||
273
backend/igny8_core/ai/functions/generate_page_content.py
Normal file
273
backend/igny8_core/ai/functions/generate_page_content.py
Normal file
@@ -0,0 +1,273 @@
|
||||
"""
|
||||
Generate Page Content AI Function
|
||||
Site Builder specific content generation that outputs structured JSON blocks.
|
||||
|
||||
This is separate from the default writer module's GenerateContentFunction.
|
||||
It uses different prompts optimized for site builder pages and outputs
|
||||
structured blocks_json format instead of HTML.
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
from typing import Dict, List, Any
|
||||
from django.db import transaction
|
||||
from igny8_core.ai.base import BaseAIFunction
|
||||
from igny8_core.business.site_building.models import PageBlueprint
|
||||
from igny8_core.business.content.models import Tasks, Content
|
||||
from igny8_core.ai.ai_core import AICore
|
||||
from igny8_core.ai.prompts import PromptRegistry
|
||||
from igny8_core.ai.settings import get_model_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GeneratePageContentFunction(BaseAIFunction):
|
||||
"""
|
||||
Generate structured page content for Site Builder pages.
|
||||
Outputs JSON blocks format optimized for site rendering.
|
||||
"""
|
||||
|
||||
def get_name(self) -> str:
|
||||
return 'generate_page_content'
|
||||
|
||||
def get_metadata(self) -> Dict:
|
||||
return {
|
||||
'display_name': 'Generate Page Content',
|
||||
'description': 'Generate structured page content with JSON blocks for Site Builder',
|
||||
'phases': {
|
||||
'INIT': 'Initializing page content generation...',
|
||||
'PREP': 'Loading page blueprint and building prompt...',
|
||||
'AI_CALL': 'Generating structured content with AI...',
|
||||
'PARSE': 'Parsing JSON blocks...',
|
||||
'SAVE': 'Saving blocks to page...',
|
||||
'DONE': 'Page content generated!'
|
||||
}
|
||||
}
|
||||
|
||||
def get_max_items(self) -> int:
|
||||
return 20 # Max pages per batch
|
||||
|
||||
def validate(self, payload: dict, account=None) -> Dict:
|
||||
"""Validate page blueprint IDs"""
|
||||
result = super().validate(payload, account)
|
||||
if not result['valid']:
|
||||
return result
|
||||
|
||||
page_ids = payload.get('ids', [])
|
||||
if page_ids:
|
||||
from igny8_core.business.site_building.models import PageBlueprint
|
||||
queryset = PageBlueprint.objects.filter(id__in=page_ids)
|
||||
if account:
|
||||
queryset = queryset.filter(account=account)
|
||||
|
||||
if queryset.count() == 0:
|
||||
return {'valid': False, 'error': 'No page blueprints found'}
|
||||
|
||||
return {'valid': True}
|
||||
|
||||
def prepare(self, payload: dict, account=None) -> List:
|
||||
"""Load page blueprints with relationships"""
|
||||
page_ids = payload.get('ids', [])
|
||||
|
||||
queryset = PageBlueprint.objects.filter(id__in=page_ids)
|
||||
if account:
|
||||
queryset = queryset.filter(account=account)
|
||||
|
||||
# Preload relationships
|
||||
pages = list(queryset.select_related(
|
||||
'site_blueprint', 'account', 'site', 'sector'
|
||||
))
|
||||
|
||||
if not pages:
|
||||
raise ValueError("No page blueprints found")
|
||||
|
||||
return pages
|
||||
|
||||
def build_prompt(self, data: Any, account=None) -> str:
|
||||
"""Build page content generation prompt optimized for Site Builder"""
|
||||
if isinstance(data, list):
|
||||
page = data[0] if data else None
|
||||
else:
|
||||
page = data
|
||||
|
||||
if not page:
|
||||
raise ValueError("No page blueprint provided")
|
||||
|
||||
account = account or page.account
|
||||
|
||||
# Build page context
|
||||
page_context = {
|
||||
'PAGE_TITLE': page.title or page.slug.replace('-', ' ').title(),
|
||||
'PAGE_SLUG': page.slug,
|
||||
'PAGE_TYPE': page.type or 'custom',
|
||||
'SITE_NAME': page.site_blueprint.name if page.site_blueprint else '',
|
||||
'SITE_DESCRIPTION': page.site_blueprint.description or '',
|
||||
}
|
||||
|
||||
# Extract existing block structure hints
|
||||
block_hints = []
|
||||
if page.blocks_json:
|
||||
for block in page.blocks_json[:5]: # First 5 blocks as hints
|
||||
if isinstance(block, dict):
|
||||
block_type = block.get('type', '')
|
||||
heading = block.get('heading') or block.get('title') or ''
|
||||
if block_type and heading:
|
||||
block_hints.append(f"- {block_type}: {heading}")
|
||||
|
||||
if block_hints:
|
||||
page_context['EXISTING_BLOCKS'] = '\n'.join(block_hints)
|
||||
else:
|
||||
page_context['EXISTING_BLOCKS'] = 'None (new page)'
|
||||
|
||||
# Get site blueprint structure hints
|
||||
structure_hints = ''
|
||||
if page.site_blueprint and page.site_blueprint.structure_json:
|
||||
structure = page.site_blueprint.structure_json
|
||||
if isinstance(structure, dict):
|
||||
layout = structure.get('layout', 'default')
|
||||
theme = structure.get('theme', {})
|
||||
structure_hints = f"Layout: {layout}\nTheme: {json.dumps(theme, indent=2)}"
|
||||
|
||||
page_context['STRUCTURE_HINTS'] = structure_hints or 'Default layout'
|
||||
|
||||
# Get prompt from registry (site-builder specific)
|
||||
prompt = PromptRegistry.get_prompt(
|
||||
function_name='generate_page_content',
|
||||
account=account,
|
||||
context=page_context
|
||||
)
|
||||
|
||||
return prompt
|
||||
|
||||
def parse_response(self, response: str, step_tracker=None) -> Dict:
|
||||
"""Parse AI response - must be JSON with blocks structure"""
|
||||
import json
|
||||
|
||||
# Try to extract JSON from response
|
||||
try:
|
||||
# Try direct JSON parse
|
||||
parsed = json.loads(response.strip())
|
||||
except json.JSONDecodeError:
|
||||
# Try to extract JSON object from text
|
||||
try:
|
||||
# Look for JSON object in response
|
||||
start = response.find('{')
|
||||
end = response.rfind('}')
|
||||
if start != -1 and end != -1 and end > start:
|
||||
json_str = response[start:end + 1]
|
||||
parsed = json.loads(json_str)
|
||||
else:
|
||||
raise ValueError("No JSON object found in response")
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
logger.error(f"Failed to parse page content response as JSON: {e}")
|
||||
logger.error(f"Response preview: {response[:500]}")
|
||||
raise ValueError(f"Invalid JSON response from AI: {str(e)}")
|
||||
|
||||
if not isinstance(parsed, dict):
|
||||
raise ValueError("Response must be a JSON object")
|
||||
|
||||
# Validate required fields
|
||||
if 'blocks' not in parsed and 'blocks_json' not in parsed:
|
||||
raise ValueError("Response must include 'blocks' or 'blocks_json' field")
|
||||
|
||||
# Normalize to 'blocks' key
|
||||
if 'blocks_json' in parsed:
|
||||
parsed['blocks'] = parsed.pop('blocks_json')
|
||||
|
||||
return parsed
|
||||
|
||||
def save_output(
|
||||
self,
|
||||
parsed: Any,
|
||||
original_data: Any,
|
||||
account=None,
|
||||
progress_tracker=None,
|
||||
step_tracker=None
|
||||
) -> Dict:
|
||||
"""Save blocks to PageBlueprint and create/update Content record"""
|
||||
if isinstance(original_data, list):
|
||||
page = original_data[0] if original_data else None
|
||||
else:
|
||||
page = original_data
|
||||
|
||||
if not page:
|
||||
raise ValueError("No page blueprint provided for saving")
|
||||
|
||||
if not isinstance(parsed, dict):
|
||||
raise ValueError("Parsed response must be a dict")
|
||||
|
||||
blocks = parsed.get('blocks', [])
|
||||
if not blocks:
|
||||
raise ValueError("No blocks found in parsed response")
|
||||
|
||||
# Ensure blocks is a list
|
||||
if not isinstance(blocks, list):
|
||||
blocks = [blocks]
|
||||
|
||||
with transaction.atomic():
|
||||
# Update PageBlueprint with generated blocks
|
||||
page.blocks_json = blocks
|
||||
page.status = 'ready' # Mark as ready after content generation
|
||||
page.save(update_fields=['blocks_json', 'status', 'updated_at'])
|
||||
|
||||
# Find or create associated Task
|
||||
task_title = f"[Site Builder] {page.title or page.slug.replace('-', ' ').title()}"
|
||||
task = Tasks.objects.filter(
|
||||
account=page.account,
|
||||
site=page.site,
|
||||
sector=page.sector,
|
||||
title=task_title
|
||||
).first()
|
||||
|
||||
# Create or update Content record with blocks
|
||||
if task:
|
||||
content_record, created = Content.objects.get_or_create(
|
||||
task=task,
|
||||
defaults={
|
||||
'account': page.account,
|
||||
'site': page.site,
|
||||
'sector': page.sector,
|
||||
'title': parsed.get('title') or page.title,
|
||||
'html_content': parsed.get('html_content', ''),
|
||||
'word_count': parsed.get('word_count', 0),
|
||||
'status': 'draft',
|
||||
'json_blocks': blocks, # Store blocks in json_blocks
|
||||
'metadata': {
|
||||
'page_id': page.id,
|
||||
'page_slug': page.slug,
|
||||
'page_type': page.type,
|
||||
'generated_by': 'generate_page_content'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if not created:
|
||||
# Update existing content
|
||||
content_record.json_blocks = blocks
|
||||
content_record.html_content = parsed.get('html_content', content_record.html_content)
|
||||
content_record.word_count = parsed.get('word_count', content_record.word_count)
|
||||
content_record.title = parsed.get('title') or content_record.title or page.title
|
||||
if not content_record.metadata:
|
||||
content_record.metadata = {}
|
||||
content_record.metadata.update({
|
||||
'page_id': page.id,
|
||||
'page_slug': page.slug,
|
||||
'page_type': page.type,
|
||||
'generated_by': 'generate_page_content'
|
||||
})
|
||||
content_record.save()
|
||||
else:
|
||||
logger.warning(f"No task found for page {page.id}, skipping Content record creation")
|
||||
content_record = None
|
||||
|
||||
logger.info(
|
||||
f"[GeneratePageContentFunction] Saved {len(blocks)} blocks to page {page.id} "
|
||||
f"(Content ID: {content_record.id if content_record else 'N/A'})"
|
||||
)
|
||||
|
||||
return {
|
||||
'count': 1,
|
||||
'pages_updated': 1,
|
||||
'blocks_count': len(blocks),
|
||||
'content_id': content_record.id if content_record else None
|
||||
}
|
||||
|
||||
@@ -522,6 +522,169 @@ CONTENT REQUIREMENTS:
|
||||
- Include call-to-action sections
|
||||
""",
|
||||
|
||||
'generate_page_content': """You are a Site Builder content specialist. Generate structured page content optimized for website pages with JSON blocks format.
|
||||
|
||||
Your task is to generate content that will be rendered as a modern website page with structured blocks (hero, features, testimonials, text, CTA, etc.).
|
||||
|
||||
INPUT DATA:
|
||||
----------
|
||||
Page Title: [IGNY8_PAGE_TITLE]
|
||||
Page Slug: [IGNY8_PAGE_SLUG]
|
||||
Page Type: [IGNY8_PAGE_TYPE] (home, products, blog, contact, about, services, custom)
|
||||
Site Name: [IGNY8_SITE_NAME]
|
||||
Site Description: [IGNY8_SITE_DESCRIPTION]
|
||||
Existing Block Hints: [IGNY8_EXISTING_BLOCKS]
|
||||
Structure Hints: [IGNY8_STRUCTURE_HINTS]
|
||||
|
||||
OUTPUT FORMAT:
|
||||
--------------
|
||||
Return ONLY a JSON object in this exact structure:
|
||||
|
||||
{
|
||||
"title": "[Page title - SEO optimized, natural]",
|
||||
"html_content": "[Full HTML content for fallback/SEO - complete article]",
|
||||
"word_count": [Integer - word count of HTML content],
|
||||
"blocks": [
|
||||
{
|
||||
"type": "hero",
|
||||
"data": {
|
||||
"heading": "[Compelling hero headline]",
|
||||
"subheading": "[Supporting subheadline]",
|
||||
"content": "[Brief hero description - 1-2 sentences]",
|
||||
"buttonText": "[CTA button text]",
|
||||
"buttonLink": "[CTA link URL]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"data": {
|
||||
"heading": "[Section heading]",
|
||||
"content": "[Rich text content with paragraphs, lists, etc.]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "features",
|
||||
"data": {
|
||||
"heading": "[Features section heading]",
|
||||
"content": [
|
||||
"[Feature 1: Description]",
|
||||
"[Feature 2: Description]",
|
||||
"[Feature 3: Description]"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "testimonials",
|
||||
"data": {
|
||||
"heading": "[Testimonials heading]",
|
||||
"subheading": "[Optional subheading]",
|
||||
"content": [
|
||||
"[Testimonial quote 1]",
|
||||
"[Testimonial quote 2]",
|
||||
"[Testimonial quote 3]"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cta",
|
||||
"data": {
|
||||
"heading": "[CTA heading]",
|
||||
"subheading": "[CTA subheading]",
|
||||
"content": "[CTA description]",
|
||||
"buttonText": "[Button text]",
|
||||
"buttonLink": "[Button link]"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
BLOCK TYPE GUIDELINES:
|
||||
----------------------
|
||||
Based on page type, use appropriate blocks:
|
||||
|
||||
**Home Page:**
|
||||
- Start with "hero" block (compelling headline + CTA)
|
||||
- Follow with "features" or "text" blocks
|
||||
- Include "testimonials" block
|
||||
- End with "cta" block
|
||||
|
||||
**Products Page:**
|
||||
- Start with "text" block (product overview)
|
||||
- Use "features" or "grid" blocks for product listings
|
||||
- Include "text" blocks for product details
|
||||
|
||||
**Blog Page:**
|
||||
- Use "text" blocks for article content
|
||||
- Can include "quote" blocks for highlights
|
||||
- Structure as readable article format
|
||||
|
||||
**Contact Page:**
|
||||
- Start with "text" block (contact info)
|
||||
- Use "form" block structure hints
|
||||
- Include "text" blocks for location/hours
|
||||
|
||||
**About Page:**
|
||||
- Start with "hero" or "text" block
|
||||
- Use "features" for team/values
|
||||
- Include "stats" block if applicable
|
||||
- End with "text" block
|
||||
|
||||
**Services Page:**
|
||||
- Start with "text" block (service overview)
|
||||
- Use "features" for service offerings
|
||||
- Include "text" blocks for details
|
||||
|
||||
CONTENT REQUIREMENTS:
|
||||
---------------------
|
||||
1. **Hero Block** (for home/about pages):
|
||||
- Compelling headline (8-12 words)
|
||||
- Clear value proposition
|
||||
- Strong CTA button
|
||||
|
||||
2. **Text Blocks**:
|
||||
- Natural, engaging copy
|
||||
- SEO-optimized headings
|
||||
- Varied content (paragraphs, lists, emphasis)
|
||||
|
||||
3. **Features Blocks**:
|
||||
- 3-6 features
|
||||
- Clear benefit statements
|
||||
- Action-oriented language
|
||||
|
||||
4. **Testimonials Blocks**:
|
||||
- 3-5 authentic-sounding testimonials
|
||||
- Specific, believable quotes
|
||||
- Varied lengths
|
||||
|
||||
5. **CTA Blocks**:
|
||||
- Clear value proposition
|
||||
- Strong action words
|
||||
- Compelling button text
|
||||
|
||||
6. **HTML Content** (for SEO):
|
||||
- Complete article version of all blocks
|
||||
- Proper HTML structure
|
||||
- SEO-optimized with headings, paragraphs, lists
|
||||
- 800-1500 words total
|
||||
|
||||
TONE & STYLE:
|
||||
-------------
|
||||
- Professional but approachable
|
||||
- Clear and concise
|
||||
- Benefit-focused
|
||||
- Action-oriented
|
||||
- Natural keyword usage (not forced)
|
||||
- No generic phrases or placeholder text
|
||||
|
||||
IMPORTANT:
|
||||
----------
|
||||
- Return ONLY the JSON object
|
||||
- Do NOT include markdown formatting
|
||||
- Do NOT include explanations or comments
|
||||
- Ensure all blocks have proper "type" and "data" structure
|
||||
- HTML content should be complete and standalone
|
||||
- Blocks should be optimized for the specific page type""",
|
||||
|
||||
'taxonomy_generation': """You are a taxonomy and categorization specialist. Generate comprehensive taxonomy page content that organizes and explains categories, tags, and hierarchical structures.
|
||||
|
||||
INPUT:
|
||||
@@ -597,6 +760,7 @@ CONTENT REQUIREMENTS:
|
||||
'extract_image_prompts': 'image_prompt_extraction',
|
||||
'generate_image_prompts': 'image_prompt_extraction',
|
||||
'generate_site_structure': 'site_structure_generation',
|
||||
'generate_page_content': 'generate_page_content', # Site Builder specific
|
||||
'optimize_content': 'optimize_content',
|
||||
# Phase 8: Universal Content Types
|
||||
'generate_product_content': 'product_generation',
|
||||
|
||||
@@ -105,6 +105,7 @@ class SitesRendererAdapter(BaseAdapter):
|
||||
def _build_site_definition(self, site_blueprint: SiteBlueprint) -> Dict[str, Any]:
|
||||
"""
|
||||
Build site definition JSON from blueprint.
|
||||
Merges actual Content from Writer into PageBlueprint blocks.
|
||||
|
||||
Args:
|
||||
site_blueprint: SiteBlueprint instance
|
||||
@@ -112,15 +113,55 @@ class SitesRendererAdapter(BaseAdapter):
|
||||
Returns:
|
||||
dict: Site definition structure
|
||||
"""
|
||||
from igny8_core.business.content.models import Tasks, Content
|
||||
|
||||
# Get all pages
|
||||
pages = []
|
||||
for page in site_blueprint.pages.all().order_by('order'):
|
||||
# Get blocks from blueprint (placeholders)
|
||||
blocks = page.blocks_json or []
|
||||
|
||||
# Try to find actual Content from Writer
|
||||
# PageBlueprint -> Task (by title pattern) -> Content
|
||||
task_title = f"[Site Builder] {page.title or page.slug.replace('-', ' ').title()}"
|
||||
task = Tasks.objects.filter(
|
||||
account=page.account,
|
||||
site=page.site,
|
||||
sector=page.sector,
|
||||
title=task_title
|
||||
).first()
|
||||
|
||||
# If task exists, get its Content
|
||||
if task and hasattr(task, 'content_record'):
|
||||
content = task.content_record
|
||||
# If content is published, merge its blocks
|
||||
if content and content.status == 'publish' and content.json_blocks:
|
||||
# Merge Content.json_blocks into PageBlueprint.blocks_json
|
||||
# Content blocks take precedence over blueprint placeholders
|
||||
blocks = content.json_blocks
|
||||
logger.info(
|
||||
f"[SitesRendererAdapter] Using published Content blocks for page {page.slug} "
|
||||
f"(Content ID: {content.id})"
|
||||
)
|
||||
elif content and content.status == 'publish' and content.html_content:
|
||||
# If no json_blocks but has html_content, convert to text block
|
||||
blocks = [{
|
||||
'type': 'text',
|
||||
'data': {
|
||||
'content': content.html_content,
|
||||
'title': content.title or page.title
|
||||
}
|
||||
}]
|
||||
logger.info(
|
||||
f"[SitesRendererAdapter] Converted HTML content to text block for page {page.slug}"
|
||||
)
|
||||
|
||||
pages.append({
|
||||
'id': page.id,
|
||||
'slug': page.slug,
|
||||
'title': page.title,
|
||||
'type': page.type,
|
||||
'blocks': page.blocks_json,
|
||||
'blocks': blocks,
|
||||
'status': page.status,
|
||||
})
|
||||
|
||||
|
||||
@@ -23,10 +23,14 @@ class PageGenerationService:
|
||||
|
||||
def __init__(self):
|
||||
self.content_service = ContentGenerationService()
|
||||
# Site Builder uses its own AI function for structured block generation
|
||||
from igny8_core.ai.functions.generate_page_content import GeneratePageContentFunction
|
||||
self.page_content_function = GeneratePageContentFunction()
|
||||
|
||||
def generate_page_content(self, page_blueprint: PageBlueprint, force_regenerate: bool = False) -> dict:
|
||||
"""
|
||||
Generate (or regenerate) content for a single Site Builder page.
|
||||
Uses Site Builder specific AI function that outputs structured JSON blocks.
|
||||
|
||||
Args:
|
||||
page_blueprint: Target PageBlueprint instance.
|
||||
@@ -35,19 +39,39 @@ class PageGenerationService:
|
||||
if not page_blueprint:
|
||||
raise ValueError("Page blueprint is required")
|
||||
|
||||
task = self._ensure_task(page_blueprint, force_regenerate=force_regenerate)
|
||||
|
||||
# Mark page as generating before handing off to Writer pipeline
|
||||
# Mark page as generating
|
||||
page_blueprint.status = 'generating'
|
||||
page_blueprint.save(update_fields=['status', 'updated_at'])
|
||||
|
||||
account = page_blueprint.account
|
||||
|
||||
# Use Site Builder specific AI function for structured block generation
|
||||
from igny8_core.ai.engine import AIEngine
|
||||
|
||||
ai_engine = AIEngine(account=account)
|
||||
|
||||
logger.info(
|
||||
"[PageGenerationService] Triggering content generation for page %s (task %s)",
|
||||
"[PageGenerationService] Generating structured content for page %s using generate_page_content function",
|
||||
page_blueprint.id,
|
||||
task.id,
|
||||
)
|
||||
return self.content_service.generate_content([task.id], account)
|
||||
|
||||
# Execute Site Builder page content generation
|
||||
result = ai_engine.execute(
|
||||
self.page_content_function,
|
||||
{'ids': [page_blueprint.id]}
|
||||
)
|
||||
|
||||
if result.get('error'):
|
||||
page_blueprint.status = 'draft'
|
||||
page_blueprint.save(update_fields=['status', 'updated_at'])
|
||||
raise ValueError(f"Content generation failed: {result.get('error')}")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'page_id': page_blueprint.id,
|
||||
'blocks_count': result.get('blocks_count', 0),
|
||||
'content_id': result.get('content_id')
|
||||
}
|
||||
|
||||
def regenerate_page(self, page_blueprint: PageBlueprint) -> dict:
|
||||
"""Force regeneration by dropping the cached task metadata."""
|
||||
|
||||
@@ -41,18 +41,18 @@ function SiteRenderer() {
|
||||
}
|
||||
|
||||
// Build navigation from site definition
|
||||
// Show pages that are published, ready, or in navigation (excluding home and draft/generating)
|
||||
const navigation = siteDefinition.navigation || siteDefinition.pages
|
||||
// Show all published/ready pages (excluding home and draft/generating)
|
||||
// Use explicit navigation if available, otherwise auto-generate from pages
|
||||
const navigation = siteDefinition.navigation && siteDefinition.navigation.length > 0
|
||||
? siteDefinition.navigation
|
||||
: siteDefinition.pages
|
||||
.filter(p =>
|
||||
p.slug !== 'home' &&
|
||||
(p.status === 'published' || p.status === 'ready' || siteDefinition.navigation?.some(n => n.slug === p.slug))
|
||||
(p.status === 'published' || p.status === 'ready') &&
|
||||
p.status !== 'draft' &&
|
||||
p.status !== 'generating'
|
||||
)
|
||||
.sort((a, b) => {
|
||||
// Try to get order from navigation or use page order
|
||||
const navA = siteDefinition.navigation?.find(n => n.slug === a.slug);
|
||||
const navB = siteDefinition.navigation?.find(n => n.slug === b.slug);
|
||||
return (navA?.order ?? a.order ?? 0) - (navB?.order ?? b.order ?? 0);
|
||||
})
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
.map(page => ({
|
||||
label: page.title,
|
||||
slug: page.slug,
|
||||
@@ -63,14 +63,10 @@ function SiteRenderer() {
|
||||
const currentPageSlug = pageSlug || 'home';
|
||||
const currentPage = siteDefinition.pages.find(p => p.slug === currentPageSlug);
|
||||
|
||||
// If specific page requested, show only that page; otherwise show all published/ready pages
|
||||
const pagesToRender = currentPageSlug && currentPageSlug !== 'home' && currentPage
|
||||
// Show only the current page (home page on home route, specific page on page route)
|
||||
const pagesToRender = currentPage
|
||||
? [currentPage]
|
||||
: siteDefinition.pages.filter(p =>
|
||||
p.status === 'published' ||
|
||||
p.status === 'ready' ||
|
||||
(p.slug === 'home' && p.status !== 'draft' && p.status !== 'generating')
|
||||
);
|
||||
: []; // Fallback: no page found
|
||||
|
||||
return (
|
||||
<div className="site-renderer" style={{
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import React from 'react';
|
||||
import type { SiteDefinition } from '../types';
|
||||
import { renderTemplate } from './templateEngine';
|
||||
import { renderPageByType } from './pageTypeRenderer';
|
||||
import {
|
||||
DefaultLayout,
|
||||
MinimalLayout,
|
||||
@@ -65,28 +66,20 @@ function renderDefaultLayout(siteDefinition: SiteDefinition): React.ReactElement
|
||||
const showHero = isHomePage || (homePage && siteDefinition.pages.length > 1);
|
||||
const hero: React.ReactNode = (showHero && heroBlock) ? (renderTemplate(heroBlock) as React.ReactNode) : undefined;
|
||||
|
||||
// Render all pages as sections (excluding hero from home page if it exists)
|
||||
// Render all pages using page-type-specific templates
|
||||
const sections = siteDefinition.pages
|
||||
.filter((page) => page.status !== 'draft' && page.status !== 'generating')
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
.map((page) => {
|
||||
// Filter out hero block if it's the home page (already rendered as hero)
|
||||
// Use page-type-specific renderer if available
|
||||
const blocksToRender = page.slug === 'home' && heroBlock && showHero
|
||||
? page.blocks?.filter(b => b.type !== 'hero') || []
|
||||
: page.blocks || [];
|
||||
|
||||
// Render using page-type template
|
||||
return (
|
||||
<div key={page.id} className="page" data-page-slug={page.slug} style={{ textAlign: 'center' }}>
|
||||
{page.slug !== 'home' && <h2 style={{ textAlign: 'center', marginBottom: '1.5rem' }}>{page.title}</h2>}
|
||||
{blocksToRender.length > 0 ? (
|
||||
blocksToRender.map((block, index) => (
|
||||
<div key={index} className="block" data-block-type={block.type} style={{ textAlign: 'center' }}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))
|
||||
) : page.slug !== 'home' ? (
|
||||
<p>No content available for this page.</p>
|
||||
) : null}
|
||||
<div key={page.id} className="page-wrapper" data-page-slug={page.slug}>
|
||||
{renderPageByType(page, blocksToRender)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -107,17 +100,11 @@ function renderMinimalLayout(siteDefinition: SiteDefinition): React.ReactElement
|
||||
const mainContent = (
|
||||
<>
|
||||
{siteDefinition.pages
|
||||
.filter((page) => page.status !== 'draft')
|
||||
.filter((page) => page.status !== 'draft' && page.status !== 'generating')
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
.map((page) => (
|
||||
<div key={page.id} className="page" data-page-slug={page.slug}>
|
||||
<h2>{page.title}</h2>
|
||||
{page.blocks && page.blocks.length > 0 ? (
|
||||
page.blocks.map((block, index) => (
|
||||
<div key={index} className="block" data-block-type={block.type}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))
|
||||
) : null}
|
||||
<div key={page.id} className="page-wrapper" data-page-slug={page.slug}>
|
||||
{renderPageByType(page, page.blocks || [])}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
@@ -138,16 +125,11 @@ function renderMagazineLayout(siteDefinition: SiteDefinition): React.ReactElemen
|
||||
const mainContent = (
|
||||
<>
|
||||
{siteDefinition.pages
|
||||
.filter((page) => page.status !== 'draft')
|
||||
.filter((page) => page.status !== 'draft' && page.status !== 'generating')
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
.map((page) => (
|
||||
<div key={page.id} className="page" data-page-slug={page.slug}>
|
||||
{page.blocks && page.blocks.length > 0 ? (
|
||||
page.blocks.map((block, index) => (
|
||||
<div key={index} className="block" data-block-type={block.type}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))
|
||||
) : null}
|
||||
<div key={page.id} className="page-wrapper" data-page-slug={page.slug}>
|
||||
{renderPageByType(page, page.blocks || [])}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
@@ -168,16 +150,11 @@ function renderEcommerceLayout(siteDefinition: SiteDefinition): React.ReactEleme
|
||||
const mainContent = (
|
||||
<>
|
||||
{siteDefinition.pages
|
||||
.filter((page) => page.status !== 'draft')
|
||||
.filter((page) => page.status !== 'draft' && page.status !== 'generating')
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
.map((page) => (
|
||||
<div key={page.id} className="page" data-page-slug={page.slug}>
|
||||
{page.blocks && page.blocks.length > 0 ? (
|
||||
page.blocks.map((block, index) => (
|
||||
<div key={index} className="block" data-block-type={block.type}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))
|
||||
) : null}
|
||||
<div key={page.id} className="page-wrapper" data-page-slug={page.slug}>
|
||||
{renderPageByType(page, page.blocks || [])}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
@@ -198,16 +175,11 @@ function renderPortfolioLayout(siteDefinition: SiteDefinition): React.ReactEleme
|
||||
const mainContent = (
|
||||
<>
|
||||
{siteDefinition.pages
|
||||
.filter((page) => page.status !== 'draft')
|
||||
.filter((page) => page.status !== 'draft' && page.status !== 'generating')
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
.map((page) => (
|
||||
<div key={page.id} className="page" data-page-slug={page.slug}>
|
||||
{page.blocks && page.blocks.length > 0 ? (
|
||||
page.blocks.map((block, index) => (
|
||||
<div key={index} className="block" data-block-type={block.type}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))
|
||||
) : null}
|
||||
<div key={page.id} className="page-wrapper" data-page-slug={page.slug}>
|
||||
{renderPageByType(page, page.blocks || [])}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
@@ -228,16 +200,11 @@ function renderBlogLayout(siteDefinition: SiteDefinition): React.ReactElement {
|
||||
const mainContent = (
|
||||
<>
|
||||
{siteDefinition.pages
|
||||
.filter((page) => page.status !== 'draft')
|
||||
.filter((page) => page.status !== 'draft' && page.status !== 'generating')
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
.map((page) => (
|
||||
<div key={page.id} className="page" data-page-slug={page.slug}>
|
||||
{page.blocks && page.blocks.length > 0 ? (
|
||||
page.blocks.map((block, index) => (
|
||||
<div key={index} className="block" data-block-type={block.type}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))
|
||||
) : null}
|
||||
<div key={page.id} className="page-wrapper" data-page-slug={page.slug}>
|
||||
{renderPageByType(page, page.blocks || [])}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
@@ -258,16 +225,11 @@ function renderCorporateLayout(siteDefinition: SiteDefinition): React.ReactEleme
|
||||
const mainContent = (
|
||||
<>
|
||||
{siteDefinition.pages
|
||||
.filter((page) => page.status !== 'draft')
|
||||
.filter((page) => page.status !== 'draft' && page.status !== 'generating')
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
.map((page) => (
|
||||
<div key={page.id} className="page" data-page-slug={page.slug}>
|
||||
{page.blocks && page.blocks.length > 0 ? (
|
||||
page.blocks.map((block, index) => (
|
||||
<div key={index} className="block" data-block-type={block.type}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))
|
||||
) : null}
|
||||
<div key={page.id} className="page-wrapper" data-page-slug={page.slug}>
|
||||
{renderPageByType(page, page.blocks || [])}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
|
||||
374
sites/src/utils/pageTypeRenderer.tsx
Normal file
374
sites/src/utils/pageTypeRenderer.tsx
Normal file
@@ -0,0 +1,374 @@
|
||||
/**
|
||||
* Page Type Renderer
|
||||
* Renders page-type-specific templates for different page types.
|
||||
*
|
||||
* Each page type has its own template structure optimized for that content type.
|
||||
*/
|
||||
import React from 'react';
|
||||
import type { PageDefinition, Block } from '../types';
|
||||
import { renderTemplate } from './templateEngine';
|
||||
|
||||
/**
|
||||
* Render a page based on its type.
|
||||
* Falls back to default rendering if no specific template exists.
|
||||
*/
|
||||
export function renderPageByType(page: PageDefinition, blocks: Block[]): React.ReactElement {
|
||||
// Determine page type - use page.type if available, otherwise infer from slug
|
||||
let pageType = page.type;
|
||||
if (!pageType || pageType === 'custom') {
|
||||
// Infer type from slug
|
||||
if (page.slug === 'home') pageType = 'home';
|
||||
else if (page.slug === 'products') pageType = 'products';
|
||||
else if (page.slug === 'blog') pageType = 'blog';
|
||||
else if (page.slug === 'contact') pageType = 'contact';
|
||||
else if (page.slug === 'about') pageType = 'about';
|
||||
else if (page.slug === 'services') pageType = 'services';
|
||||
else pageType = 'custom';
|
||||
}
|
||||
|
||||
switch (pageType) {
|
||||
case 'home':
|
||||
return renderHomePage(page, blocks);
|
||||
case 'products':
|
||||
return renderProductsPage(page, blocks);
|
||||
case 'blog':
|
||||
return renderBlogPage(page, blocks);
|
||||
case 'contact':
|
||||
return renderContactPage(page, blocks);
|
||||
case 'about':
|
||||
return renderAboutPage(page, blocks);
|
||||
case 'services':
|
||||
return renderServicesPage(page, blocks);
|
||||
case 'custom':
|
||||
default:
|
||||
return renderCustomPage(page, blocks);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Home Page Template
|
||||
* Structure: Hero → Features → Testimonials → CTA
|
||||
*/
|
||||
function renderHomePage(page: PageDefinition, blocks: Block[]): React.ReactElement {
|
||||
// Find specific blocks
|
||||
const heroBlock = blocks.find(b => b.type === 'hero');
|
||||
const featuresBlock = blocks.find(b => b.type === 'features' || b.type === 'grid');
|
||||
const testimonialsBlock = blocks.find(b => b.type === 'testimonials');
|
||||
const ctaBlock = blocks.find(b => b.type === 'cta');
|
||||
const otherBlocks = blocks.filter(b =>
|
||||
!['hero', 'features', 'grid', 'testimonials', 'cta'].includes(b.type)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="page-home" data-page-slug={page.slug}>
|
||||
{/* Hero Section */}
|
||||
{heroBlock && (
|
||||
<section className="home-hero">
|
||||
{renderTemplate(heroBlock)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Features Section */}
|
||||
{featuresBlock && (
|
||||
<section className="home-features" style={{ padding: '3rem 2rem', background: '#f9fafb' }}>
|
||||
{renderTemplate(featuresBlock)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Other Content Blocks */}
|
||||
{otherBlocks.length > 0 && (
|
||||
<section className="home-content" style={{ padding: '3rem 2rem' }}>
|
||||
{otherBlocks.map((block, index) => (
|
||||
<div key={index} className="block" data-block-type={block.type}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Testimonials Section */}
|
||||
{testimonialsBlock && (
|
||||
<section className="home-testimonials" style={{ padding: '3rem 2rem', background: '#f9fafb' }}>
|
||||
{renderTemplate(testimonialsBlock)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* CTA Section */}
|
||||
{ctaBlock && (
|
||||
<section className="home-cta">
|
||||
{renderTemplate(ctaBlock)}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Products Page Template
|
||||
* Structure: Title → Product Grid → Filters (if available)
|
||||
*/
|
||||
function renderProductsPage(page: PageDefinition, blocks: Block[]): React.ReactElement {
|
||||
const productsBlock = blocks.find(b => b.type === 'products' || b.type === 'grid');
|
||||
const otherBlocks = blocks.filter(b => b.type !== 'products' && b.type !== 'grid');
|
||||
|
||||
return (
|
||||
<div className="page-products" data-page-slug={page.slug}>
|
||||
<header style={{ marginBottom: '2rem', textAlign: 'center' }}>
|
||||
<h1 style={{ fontSize: '2.5rem', fontWeight: 'bold', marginBottom: '1rem' }}>
|
||||
{page.title}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
{/* Product Grid */}
|
||||
{productsBlock && (
|
||||
<section className="products-grid" style={{ padding: '2rem 0' }}>
|
||||
{renderTemplate(productsBlock)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Other Content */}
|
||||
{otherBlocks.length > 0 && (
|
||||
<section className="products-content" style={{ padding: '2rem 0' }}>
|
||||
{otherBlocks.map((block, index) => (
|
||||
<div key={index} className="block" data-block-type={block.type}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Blog Page Template
|
||||
* Structure: Title → Post List/Grid → Sidebar (if available)
|
||||
*/
|
||||
function renderBlogPage(page: PageDefinition, blocks: Block[]): React.ReactElement {
|
||||
const heroBlock = blocks.find(b => b.type === 'hero');
|
||||
const contentBlocks = blocks.filter(b => b.type !== 'hero');
|
||||
|
||||
return (
|
||||
<div className="page-blog" data-page-slug={page.slug}>
|
||||
{/* Hero/Header */}
|
||||
{heroBlock ? (
|
||||
<section className="blog-hero">
|
||||
{renderTemplate(heroBlock)}
|
||||
</section>
|
||||
) : (
|
||||
<header style={{ marginBottom: '2rem', textAlign: 'center', padding: '2rem 0' }}>
|
||||
<h1 style={{ fontSize: '2.5rem', fontWeight: 'bold', marginBottom: '0.5rem' }}>
|
||||
{page.title}
|
||||
</h1>
|
||||
</header>
|
||||
)}
|
||||
|
||||
{/* Blog Content - Grid Layout for Posts */}
|
||||
<section className="blog-content" style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
|
||||
gap: '2rem',
|
||||
padding: '2rem 0'
|
||||
}}>
|
||||
{contentBlocks.map((block, index) => (
|
||||
<article key={index} className="blog-post" style={{
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
padding: '1.5rem',
|
||||
background: '#fff'
|
||||
}}>
|
||||
{renderTemplate(block)}
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contact Page Template
|
||||
* Structure: Title → Contact Info → Form → Map (if available)
|
||||
*/
|
||||
function renderContactPage(page: PageDefinition, blocks: Block[]): React.ReactElement {
|
||||
const formBlock = blocks.find(b => b.type === 'form');
|
||||
const textBlocks = blocks.filter(b => b.type === 'text' || b.type === 'section');
|
||||
const mapBlock = blocks.find(b => b.type === 'image' && b.data?.caption?.toLowerCase().includes('map'));
|
||||
const otherBlocks = blocks.filter(b =>
|
||||
b.type !== 'form' &&
|
||||
b.type !== 'text' &&
|
||||
b.type !== 'section' &&
|
||||
!(b.type === 'image' && b.data?.caption?.toLowerCase().includes('map'))
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="page-contact" data-page-slug={page.slug}>
|
||||
<header style={{ marginBottom: '2rem', textAlign: 'center' }}>
|
||||
<h1 style={{ fontSize: '2.5rem', fontWeight: 'bold', marginBottom: '1rem' }}>
|
||||
{page.title}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
|
||||
gap: '2rem',
|
||||
padding: '2rem 0'
|
||||
}}>
|
||||
{/* Contact Information */}
|
||||
{textBlocks.length > 0 && (
|
||||
<section className="contact-info">
|
||||
{textBlocks.map((block, index) => (
|
||||
<div key={index} className="block" data-block-type={block.type}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Contact Form */}
|
||||
{formBlock && (
|
||||
<section className="contact-form">
|
||||
{renderTemplate(formBlock)}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Map */}
|
||||
{mapBlock && (
|
||||
<section className="contact-map" style={{ marginTop: '2rem' }}>
|
||||
{renderTemplate(mapBlock)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Other Content */}
|
||||
{otherBlocks.length > 0 && (
|
||||
<section className="contact-other" style={{ marginTop: '2rem' }}>
|
||||
{otherBlocks.map((block, index) => (
|
||||
<div key={index} className="block" data-block-type={block.type}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* About Page Template
|
||||
* Structure: Hero → Mission → Team/Values → Stats
|
||||
*/
|
||||
function renderAboutPage(page: PageDefinition, blocks: Block[]): React.ReactElement {
|
||||
const heroBlock = blocks.find(b => b.type === 'hero');
|
||||
const statsBlock = blocks.find(b => b.type === 'stats');
|
||||
const servicesBlock = blocks.find(b => b.type === 'services' || b.type === 'features');
|
||||
const otherBlocks = blocks.filter(b =>
|
||||
!['hero', 'stats', 'services', 'features'].includes(b.type)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="page-about" data-page-slug={page.slug}>
|
||||
{/* Hero Section */}
|
||||
{heroBlock && (
|
||||
<section className="about-hero">
|
||||
{renderTemplate(heroBlock)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Main Content */}
|
||||
<section className="about-content" style={{ padding: '3rem 2rem' }}>
|
||||
{otherBlocks.map((block, index) => (
|
||||
<div key={index} className="block" data-block-type={block.type} style={{ marginBottom: '2rem' }}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{/* Services/Features Section */}
|
||||
{servicesBlock && (
|
||||
<section className="about-services" style={{ padding: '3rem 2rem', background: '#f9fafb' }}>
|
||||
{renderTemplate(servicesBlock)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Stats Section */}
|
||||
{statsBlock && (
|
||||
<section className="about-stats" style={{ padding: '3rem 2rem' }}>
|
||||
{renderTemplate(statsBlock)}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Services Page Template
|
||||
* Structure: Title → Service Cards → Details
|
||||
*/
|
||||
function renderServicesPage(page: PageDefinition, blocks: Block[]): React.ReactElement {
|
||||
const heroBlock = blocks.find(b => b.type === 'hero');
|
||||
const servicesBlock = blocks.find(b => b.type === 'services' || b.type === 'features');
|
||||
const otherBlocks = blocks.filter(b =>
|
||||
b.type !== 'hero' && b.type !== 'services' && b.type !== 'features'
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="page-services" data-page-slug={page.slug}>
|
||||
{/* Hero/Header */}
|
||||
{heroBlock ? (
|
||||
<section className="services-hero">
|
||||
{renderTemplate(heroBlock)}
|
||||
</section>
|
||||
) : (
|
||||
<header style={{ marginBottom: '2rem', textAlign: 'center', padding: '2rem 0' }}>
|
||||
<h1 style={{ fontSize: '2.5rem', fontWeight: 'bold', marginBottom: '0.5rem' }}>
|
||||
{page.title}
|
||||
</h1>
|
||||
</header>
|
||||
)}
|
||||
|
||||
{/* Services Grid */}
|
||||
{servicesBlock && (
|
||||
<section className="services-grid" style={{ padding: '2rem 0' }}>
|
||||
{renderTemplate(servicesBlock)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Additional Content */}
|
||||
{otherBlocks.length > 0 && (
|
||||
<section className="services-content" style={{ padding: '2rem 0' }}>
|
||||
{otherBlocks.map((block, index) => (
|
||||
<div key={index} className="block" data-block-type={block.type}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom Page Template
|
||||
* Default rendering for custom pages - renders all blocks in order
|
||||
*/
|
||||
function renderCustomPage(page: PageDefinition, blocks: Block[]): React.ReactElement {
|
||||
return (
|
||||
<div className="page-custom" data-page-slug={page.slug}>
|
||||
<header style={{ marginBottom: '2rem', textAlign: 'center' }}>
|
||||
<h1 style={{ fontSize: '2.5rem', fontWeight: 'bold', marginBottom: '1rem' }}>
|
||||
{page.title}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<section className="custom-content" style={{ padding: '2rem 0' }}>
|
||||
{blocks.map((block, index) => (
|
||||
<div key={index} className="block" data-block-type={block.type} style={{ marginBottom: '2rem' }}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user