From e4e7ddfdf3b2c3cf892dd92910c24098da5a61f0 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Tue, 18 Nov 2025 23:30:20 +0000 Subject: [PATCH] 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. --- DEPLOYMENT_GUIDE.md | 84 ++++ SITE_BUILDER_WORKFLOW_EXPLANATION.md | 65 +++ TEMPLATE_SYSTEM_EXPLANATION.md | 185 +++++++++ backend/celerybeat-schedule | Bin 16384 -> 16384 bytes backend/igny8_core/ai/engine.py | 15 +- backend/igny8_core/ai/functions/__init__.py | 2 + .../ai/functions/generate_page_content.py | 273 +++++++++++++ backend/igny8_core/ai/prompts.py | 164 ++++++++ .../adapters/sites_renderer_adapter.py | 43 +- .../services/page_generation_service.py | 36 +- sites/src/pages/SiteRenderer.tsx | 44 +-- sites/src/utils/layoutRenderer.tsx | 98 ++--- sites/src/utils/pageTypeRenderer.tsx | 374 ++++++++++++++++++ 13 files changed, 1283 insertions(+), 100 deletions(-) create mode 100644 DEPLOYMENT_GUIDE.md create mode 100644 SITE_BUILDER_WORKFLOW_EXPLANATION.md create mode 100644 TEMPLATE_SYSTEM_EXPLANATION.md create mode 100644 backend/igny8_core/ai/functions/generate_page_content.py create mode 100644 sites/src/utils/pageTypeRenderer.tsx diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md new file mode 100644 index 00000000..c46af8b4 --- /dev/null +++ b/DEPLOYMENT_GUIDE.md @@ -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 + diff --git a/SITE_BUILDER_WORKFLOW_EXPLANATION.md b/SITE_BUILDER_WORKFLOW_EXPLANATION.md new file mode 100644 index 00000000..3d63ed7b --- /dev/null +++ b/SITE_BUILDER_WORKFLOW_EXPLANATION.md @@ -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 + diff --git a/TEMPLATE_SYSTEM_EXPLANATION.md b/TEMPLATE_SYSTEM_EXPLANATION.md new file mode 100644 index 00000000..f213324a --- /dev/null +++ b/TEMPLATE_SYSTEM_EXPLANATION.md @@ -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. + diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule index 784ff7cdc76a6b990a3d51552cd858ec998000a7..2116665fa22ce03f7b4448b1ba823a53e4c74a87 100644 GIT binary patch delta 30 lcmZo@U~Fh$+@NT}FD}Qqp6L$qy5&=l|7%?2hHxB-Yl32*=a diff --git a/backend/igny8_core/ai/engine.py b/backend/igny8_core/ai/engine.py index 4689eeb1..047073c8 100644 --- a/backend/igny8_core/ai/engine.py +++ b/backend/igny8_core/ai/engine.py @@ -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: diff --git a/backend/igny8_core/ai/functions/__init__.py b/backend/igny8_core/ai/functions/__init__.py index dbf2d49a..5c2ba99d 100644 --- a/backend/igny8_core/ai/functions/__init__.py +++ b/backend/igny8_core/ai/functions/__init__.py @@ -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', ] diff --git a/backend/igny8_core/ai/functions/generate_page_content.py b/backend/igny8_core/ai/functions/generate_page_content.py new file mode 100644 index 00000000..b4211627 --- /dev/null +++ b/backend/igny8_core/ai/functions/generate_page_content.py @@ -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 + } + diff --git a/backend/igny8_core/ai/prompts.py b/backend/igny8_core/ai/prompts.py index 46f2647b..6037d5ef 100644 --- a/backend/igny8_core/ai/prompts.py +++ b/backend/igny8_core/ai/prompts.py @@ -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', diff --git a/backend/igny8_core/business/publishing/services/adapters/sites_renderer_adapter.py b/backend/igny8_core/business/publishing/services/adapters/sites_renderer_adapter.py index 0fded9de..385d12f4 100644 --- a/backend/igny8_core/business/publishing/services/adapters/sites_renderer_adapter.py +++ b/backend/igny8_core/business/publishing/services/adapters/sites_renderer_adapter.py @@ -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, }) diff --git a/backend/igny8_core/business/site_building/services/page_generation_service.py b/backend/igny8_core/business/site_building/services/page_generation_service.py index bf9d8158..8fba99dd 100644 --- a/backend/igny8_core/business/site_building/services/page_generation_service.py +++ b/backend/igny8_core/business/site_building/services/page_generation_service.py @@ -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.""" diff --git a/sites/src/pages/SiteRenderer.tsx b/sites/src/pages/SiteRenderer.tsx index eb95e766..4305ca8b 100644 --- a/sites/src/pages/SiteRenderer.tsx +++ b/sites/src/pages/SiteRenderer.tsx @@ -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 (
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 ( -
- {page.slug !== 'home' &&

{page.title}

} - {blocksToRender.length > 0 ? ( - blocksToRender.map((block, index) => ( -
- {renderTemplate(block)} -
- )) - ) : page.slug !== 'home' ? ( -

No content available for this page.

- ) : null} +
+ {renderPageByType(page, blocksToRender)}
); }); @@ -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) => ( -
-

{page.title}

- {page.blocks && page.blocks.length > 0 ? ( - page.blocks.map((block, index) => ( -
- {renderTemplate(block)} -
- )) - ) : null} +
+ {renderPageByType(page, page.blocks || [])}
))} @@ -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) => ( -
- {page.blocks && page.blocks.length > 0 ? ( - page.blocks.map((block, index) => ( -
- {renderTemplate(block)} -
- )) - ) : null} +
+ {renderPageByType(page, page.blocks || [])}
))} @@ -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) => ( -
- {page.blocks && page.blocks.length > 0 ? ( - page.blocks.map((block, index) => ( -
- {renderTemplate(block)} -
- )) - ) : null} +
+ {renderPageByType(page, page.blocks || [])}
))} @@ -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) => ( -
- {page.blocks && page.blocks.length > 0 ? ( - page.blocks.map((block, index) => ( -
- {renderTemplate(block)} -
- )) - ) : null} +
+ {renderPageByType(page, page.blocks || [])}
))} @@ -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) => ( -
- {page.blocks && page.blocks.length > 0 ? ( - page.blocks.map((block, index) => ( -
- {renderTemplate(block)} -
- )) - ) : null} +
+ {renderPageByType(page, page.blocks || [])}
))} @@ -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) => ( -
- {page.blocks && page.blocks.length > 0 ? ( - page.blocks.map((block, index) => ( -
- {renderTemplate(block)} -
- )) - ) : null} +
+ {renderPageByType(page, page.blocks || [])}
))} diff --git a/sites/src/utils/pageTypeRenderer.tsx b/sites/src/utils/pageTypeRenderer.tsx new file mode 100644 index 00000000..c849609e --- /dev/null +++ b/sites/src/utils/pageTypeRenderer.tsx @@ -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 ( +
+ {/* Hero Section */} + {heroBlock && ( +
+ {renderTemplate(heroBlock)} +
+ )} + + {/* Features Section */} + {featuresBlock && ( +
+ {renderTemplate(featuresBlock)} +
+ )} + + {/* Other Content Blocks */} + {otherBlocks.length > 0 && ( +
+ {otherBlocks.map((block, index) => ( +
+ {renderTemplate(block)} +
+ ))} +
+ )} + + {/* Testimonials Section */} + {testimonialsBlock && ( +
+ {renderTemplate(testimonialsBlock)} +
+ )} + + {/* CTA Section */} + {ctaBlock && ( +
+ {renderTemplate(ctaBlock)} +
+ )} +
+ ); +} + +/** + * 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 ( +
+
+

+ {page.title} +

+
+ + {/* Product Grid */} + {productsBlock && ( +
+ {renderTemplate(productsBlock)} +
+ )} + + {/* Other Content */} + {otherBlocks.length > 0 && ( +
+ {otherBlocks.map((block, index) => ( +
+ {renderTemplate(block)} +
+ ))} +
+ )} +
+ ); +} + +/** + * 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 ( +
+ {/* Hero/Header */} + {heroBlock ? ( +
+ {renderTemplate(heroBlock)} +
+ ) : ( +
+

+ {page.title} +

+
+ )} + + {/* Blog Content - Grid Layout for Posts */} +
+ {contentBlocks.map((block, index) => ( +
+ {renderTemplate(block)} +
+ ))} +
+
+ ); +} + +/** + * 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 ( +
+
+

+ {page.title} +

+
+ +
+ {/* Contact Information */} + {textBlocks.length > 0 && ( +
+ {textBlocks.map((block, index) => ( +
+ {renderTemplate(block)} +
+ ))} +
+ )} + + {/* Contact Form */} + {formBlock && ( +
+ {renderTemplate(formBlock)} +
+ )} +
+ + {/* Map */} + {mapBlock && ( +
+ {renderTemplate(mapBlock)} +
+ )} + + {/* Other Content */} + {otherBlocks.length > 0 && ( +
+ {otherBlocks.map((block, index) => ( +
+ {renderTemplate(block)} +
+ ))} +
+ )} +
+ ); +} + +/** + * 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 ( +
+ {/* Hero Section */} + {heroBlock && ( +
+ {renderTemplate(heroBlock)} +
+ )} + + {/* Main Content */} +
+ {otherBlocks.map((block, index) => ( +
+ {renderTemplate(block)} +
+ ))} +
+ + {/* Services/Features Section */} + {servicesBlock && ( +
+ {renderTemplate(servicesBlock)} +
+ )} + + {/* Stats Section */} + {statsBlock && ( +
+ {renderTemplate(statsBlock)} +
+ )} +
+ ); +} + +/** + * 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 ( +
+ {/* Hero/Header */} + {heroBlock ? ( +
+ {renderTemplate(heroBlock)} +
+ ) : ( +
+

+ {page.title} +

+
+ )} + + {/* Services Grid */} + {servicesBlock && ( +
+ {renderTemplate(servicesBlock)} +
+ )} + + {/* Additional Content */} + {otherBlocks.length > 0 && ( +
+ {otherBlocks.map((block, index) => ( +
+ {renderTemplate(block)} +
+ ))} +
+ )} +
+ ); +} + +/** + * Custom Page Template + * Default rendering for custom pages - renders all blocks in order + */ +function renderCustomPage(page: PageDefinition, blocks: Block[]): React.ReactElement { + return ( +
+
+

+ {page.title} +

+
+ +
+ {blocks.map((block, index) => ( +
+ {renderTemplate(block)} +
+ ))} +
+
+ ); +} +