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:
IGNY8 VPS (Salman)
2025-11-18 23:30:20 +00:00
parent 6c6133a683
commit e4e7ddfdf3
13 changed files with 1283 additions and 100 deletions

84
DEPLOYMENT_GUIDE.md Normal file
View 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

View 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

View 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.

View File

@@ -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 Inarticle 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:

View File

@@ -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',
]

View 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
}

View File

@@ -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',

View File

@@ -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,
})

View File

@@ -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."""

View File

@@ -41,36 +41,32 @@ 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
.filter(p =>
p.slug !== 'home' &&
(p.status === 'published' || p.status === 'ready' || siteDefinition.navigation?.some(n => n.slug === p.slug))
)
.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);
})
.map(page => ({
label: page.title,
slug: page.slug,
order: page.order || 0
}));
// 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') &&
p.status !== 'draft' &&
p.status !== 'generating'
)
.sort((a, b) => (a.order || 0) - (b.order || 0))
.map(page => ({
label: page.title,
slug: page.slug,
order: page.order || 0
}));
// Filter pages based on current route
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={{

View File

@@ -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>
))}
</>

View 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>
);
}