diff --git a/SITE_BUILDER_INTEGRATION_PLAN.md b/SITE_BUILDER_INTEGRATION_PLAN.md
new file mode 100644
index 00000000..913741c1
--- /dev/null
+++ b/SITE_BUILDER_INTEGRATION_PLAN.md
@@ -0,0 +1,264 @@
+# Site Builder Wizard Integration Plan
+
+## Overview
+Integrate the Site Builder wizard directly into the main frontend app (`frontend/src/pages/Sites/Builder/`), using the same UI kit, state stores, and API helpers as the rest of the dashboard. The legacy `sites/src/builder` + `sites/src/renderer` code has been removed, so the only viable implementation path is the unified Sites module.
+
+## Current State
+
+### ✅ What's Done
+- Legacy builder/renderer folders removed from Sites container (no more parallel UI)
+- Type definitions created in `frontend/src/types/siteBuilder.ts`
+- API helper created in `frontend/src/services/siteBuilder.api.ts`
+
+### ⚠️ What's Missing
+- Builder store not yet created in the main app
+- Wizard steps/page still placeholder in `frontend/src/pages/Sites/Builder/`
+- No Tailwind/CX styling hooked into shared UI kit
+- Routes/menu point to placeholder
+- Tests/docs still reference old structure
+- Sites container still contains stale references (needs cleanup after integration)
+
+## Integration Plan
+
+### Phase 1: Create API Service Layer ✅
+**Location**: `frontend/src/services/siteBuilder.api.ts`
+
+**Tasks**:
+1. Create `siteBuilderApi` using `fetchAPI` pattern (not axios)
+2. Functions needed:
+ - `listBlueprints()`
+ - `createBlueprint(payload)`
+ - `generateStructure(blueprintId, payload)`
+ - `listPages(blueprintId)`
+ - `generateAllPages(blueprintId, options)`
+ - `createTasksForPages(blueprintId, pageIds)`
+
+**API Endpoints** (already exist in backend):
+- `GET /api/v1/site-builder/blueprints/`
+- `POST /api/v1/site-builder/blueprints/`
+- `POST /api/v1/site-builder/blueprints/{id}/generate_structure/`
+- `GET /api/v1/site-builder/pages/?site_blueprint={id}`
+- `POST /api/v1/site-builder/blueprints/{id}/generate_all_pages/`
+- `POST /api/v1/site-builder/blueprints/{id}/create_tasks/`
+
+### Phase 2: Create Zustand Store ⏳
+**Location**: `frontend/src/store/builderStore.ts`
+
+**Tasks**:
+1. Copy `builderStore.ts` from `sites/src/builder/state/`
+2. Adapt to use `siteBuilderApi` instead of `builderApi`
+3. Integrate with `useSiteStore` and `useSectorStore`:
+ - Auto-populate `siteId` from `useSiteStore().activeSite`
+ - Auto-populate `sectorId` from `useSectorStore().activeSector`
+ - Show site/sector selector if not set
+
+**Store State**:
+- `form: BuilderFormData` - Wizard form data
+- `currentStep: number` - Current wizard step (0-3)
+- `isSubmitting: boolean` - Generation in progress
+- `activeBlueprint: SiteBlueprint | null` - Latest blueprint
+- `pages: PageBlueprint[]` - Generated pages
+- `error: string | null` - Error message
+
+### Phase 3: Create Type Definitions ✅
+**Location**: `frontend/src/types/siteBuilder.ts`
+
+**Tasks**:
+1. Copy types from `sites/src/builder/types/siteBuilder.ts`
+2. Ensure compatibility with frontend's existing types
+
+**Types Needed**:
+- `HostingType`
+- `StylePreferences`
+- `BuilderFormData`
+- `SiteBlueprint`
+- `PageBlueprint`
+- `PageBlock`
+- `SiteStructure`
+
+### Phase 4: Create Wizard Step Components ⏳
+**Location**: `frontend/src/pages/Sites/Builder/steps/`
+
+**Tasks**:
+1. Copy step components from `sites/src/builder/pages/wizard/steps/`
+2. Adapt to use frontend's UI components:
+ - Replace `Card` with `frontend/src/components/ui/card/Card`
+ - Replace custom inputs with Tailwind-styled inputs
+ - Use frontend's `Button` component
+3. Adapt styles to Tailwind CSS:
+ - Remove `.sb-field`, `.sb-grid`, `.sb-pill` classes
+ - Use Tailwind utility classes instead
+
+**Step Components**:
+- `BusinessDetailsStep.tsx` - Site/sector selection, business info
+- `BriefStep.tsx` - Business brief textarea
+- `ObjectivesStep.tsx` - Objectives list with add/remove
+- `StyleStep.tsx` - Style preferences (palette, typography, personality)
+
+### Phase 5: Create Main Wizard Page ⏳
+**Location**: `frontend/src/pages/Sites/Builder/Wizard.tsx`
+
+**Tasks**:
+1. Copy `WizardPage.tsx` from `sites/src/builder/pages/wizard/`
+2. Adapt to frontend patterns:
+ - Use `PageMeta` component
+ - Use frontend's `Card` component
+ - Use frontend's `Button` component
+ - Use Tailwind CSS for styling
+3. Integrate with stores:
+ - Auto-load active site/sector
+ - Show site/sector selector if needed
+ - Navigate to sites list on completion
+
+**Features**:
+- 4-step wizard with progress indicators
+- Step navigation (Back/Next buttons)
+- Form validation
+- Blueprint generation on submit
+- Error handling
+- Loading states
+
+### Phase 6: Create Site Definition Store (Optional) ⏳
+**Location**: `frontend/src/store/siteDefinitionStore.ts`
+
+**Tasks**:
+1. Copy `siteDefinitionStore.ts` from `sites/src/builder/state/`
+2. Use for preview functionality (if needed)
+
+### Phase 7: Update Routing & Navigation ⏳
+**Location**: `frontend/src/App.tsx`
+
+**Tasks**:
+1. Ensure `/sites/builder` route points to new `Wizard.tsx`
+2. Update navigation to show wizard in Sites section
+
+### Phase 8: Fix Test File ⏳
+**Location**: `frontend/src/__tests__/sites/BulkGeneration.test.tsx`
+
+**Tasks**:
+1. Update import path from `site-builder/src/api/builder.api` to `services/siteBuilder.api`
+2. Update mock path accordingly
+
+### Phase 9: Testing ⏳
+**Tasks**:
+1. Test wizard flow:
+ - Site selection
+ - Sector selection
+ - All 4 wizard steps
+ - Blueprint generation
+ - Error handling
+2. Test integration:
+ - Site/sector auto-population
+ - Navigation
+ - API calls
+
+### Phase 10: Cleanup ⏳
+**Tasks**:
+1. Stop `igny8_site_builder` container
+2. Remove Docker image
+3. Remove `/site-builder` folder
+4. Update documentation
+
+## File Structure After Integration
+
+```
+frontend/src/
+├── pages/Sites/Builder/
+│ ├── Wizard.tsx # Main wizard page (UPDATED)
+│ ├── Preview.tsx # Preview page (keep placeholder for now)
+│ ├── Blueprints.tsx # Blueprints list (already exists)
+│ └── steps/ # NEW
+│ ├── BusinessDetailsStep.tsx
+│ ├── BriefStep.tsx
+│ ├── ObjectivesStep.tsx
+│ └── StyleStep.tsx
+├── services/
+│ └── siteBuilder.api.ts # NEW - API service
+├── store/
+│ ├── builderStore.ts # NEW - Builder state
+│ └── siteDefinitionStore.ts # NEW - Site definition state (optional)
+└── types/
+ └── siteBuilder.ts # NEW - Type definitions
+```
+
+## Key Adaptations Needed
+
+### 1. API Client Pattern
+**From** (sites container):
+```typescript
+import axios from 'axios';
+const client = axios.create({ baseURL: BASE_PATH });
+```
+
+**To** (frontend):
+```typescript
+import { fetchAPI } from '../services/api';
+// Use fetchAPI directly, no axios
+```
+
+### 2. Component Library
+**From** (sites container):
+```typescript
+import { Card } from '../../components/common/Card';
+```
+
+**To** (frontend):
+```typescript
+import { Card } from '../../../components/ui/card/Card';
+```
+
+### 3. Styling
+**From** (sites container):
+```css
+.sb-field { ... }
+.sb-grid { ... }
+```
+
+**To** (frontend):
+```tsx
+className="flex flex-col gap-2"
+className="grid grid-cols-2 gap-4"
+```
+
+### 4. Store Integration
+**From** (sites container):
+```typescript
+// Manual siteId/sectorId input
+```
+
+**To** (frontend):
+```typescript
+import { useSiteStore } from '../../../store/siteStore';
+import { useSectorStore } from '../../../store/sectorStore';
+// Auto-populate from stores
+```
+
+## Implementation Order
+
+1. ✅ Create types (`types/siteBuilder.ts`)
+2. ✅ Create API service (`services/siteBuilder.api.ts`)
+3. ⏳ Create builder store (`store/builderStore.ts`)
+4. ⏳ Create step components (`pages/Sites/Builder/steps/`)
+5. ⏳ Create main wizard page (`pages/Sites/Builder/Wizard.tsx`)
+6. ⏳ Fix test file(s)
+7. ⏳ Test integration
+8. ⏳ Cleanup site-builder container/image/docs
+
+## Success Criteria
+
+- ✅ Wizard loads in main app at `/sites/builder`
+- ✅ Site/sector auto-populated from stores
+- ✅ All 4 steps work correctly
+- ✅ Blueprint generation works
+- ✅ Error handling works
+- ✅ Navigation works
+- ✅ No references to `site-builder/` folder in code
+- ✅ Test file updated
+- ✅ Sites container removed or marked deprecated in compose
+
+## Notes
+
+- Sites container will be deprecated once the wizard lives entirely inside the main app.
+- Only integrate wizard into main frontend app (no parallel codepaths).
+- Use frontend's existing patterns/components/stores for absolute consistency.
+
diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule
index 741a7355..a81e7af1 100644
Binary files a/backend/celerybeat-schedule and b/backend/celerybeat-schedule differ
diff --git a/docker-compose.app.yml b/docker-compose.app.yml
index 4074d315..5c59a627 100644
--- a/docker-compose.app.yml
+++ b/docker-compose.app.yml
@@ -101,33 +101,14 @@ services:
- "com.docker.compose.project=igny8-app"
- "com.docker.compose.service=igny8_marketing_dev"
- igny8_site_builder:
- image: igny8-site-builder-dev:latest
- container_name: igny8_site_builder
- restart: always
- ports:
- - "0.0.0.0:8025:5175"
- environment:
- VITE_API_URL: "https://api.igny8.com/api"
- volumes:
- - /data/app/igny8/site-builder:/app:rw
- - /data/app/igny8/frontend:/frontend:ro
- depends_on:
- igny8_backend:
- condition: service_healthy
- networks: [igny8_net]
- labels:
- - "com.docker.compose.project=igny8-app"
- - "com.docker.compose.service=igny8_site_builder"
-
igny8_sites:
- # Sites renderer for hosting public sites
+ # Sites container: Public site renderer + Site Builder (merged)
# Build separately: docker build -t igny8-sites-dev:latest -f Dockerfile.dev .
image: igny8-sites-dev:latest
container_name: igny8_sites
restart: always
ports:
- - "0.0.0.0:8024:5176" # Sites renderer dev server port
+ - "0.0.0.0:8024:5176" # Sites renderer + Builder dev server port
environment:
VITE_API_URL: "https://api.igny8.com/api"
SITES_DATA_PATH: "/sites"
diff --git a/frontend/src/pages/Sites/Builder/Wizard.tsx b/frontend/src/pages/Sites/Builder/Wizard.tsx
index 1ceef3c5..4cf68534 100644
--- a/frontend/src/pages/Sites/Builder/Wizard.tsx
+++ b/frontend/src/pages/Sites/Builder/Wizard.tsx
@@ -1,43 +1,273 @@
-/**
- * Site Builder Wizard
- * Moved from site-builder container to main app
- * TODO: Migrate full implementation from site-builder/src/pages/wizard/
- */
-import React from 'react';
-import { useNavigate } from 'react-router-dom';
-import PageMeta from '../../../components/common/PageMeta';
-import { Card } from '../../../components/ui/card';
-import Button from '../../../components/ui/button/Button';
-import { Wand2 } from 'lucide-react';
+import { useEffect, useMemo } from "react";
+import { useNavigate } from "react-router-dom";
+import {
+ Card,
+ CardDescription,
+ CardTitle,
+} from "../../../components/ui/card";
+import Button from "../../../components/ui/button/Button";
+import PageMeta from "../../../components/common/PageMeta";
+import SiteAndSectorSelector from "../../../components/common/SiteAndSectorSelector";
+import Alert from "../../../components/ui/alert/Alert";
+import {
+ Loader2,
+ PlayCircle,
+ RefreshCw,
+ Wand2,
+} from "lucide-react";
+import { useSiteStore } from "../../../store/siteStore";
+import { useSectorStore } from "../../../store/sectorStore";
+import { useBuilderStore } from "../../../store/builderStore";
+import { BusinessDetailsStep } from "./steps/BusinessDetailsStep";
+import { BriefStep } from "./steps/BriefStep";
+import { ObjectivesStep } from "./steps/ObjectivesStep";
+import { StyleStep } from "./steps/StyleStep";
export default function SiteBuilderWizard() {
const navigate = useNavigate();
+ const { activeSite } = useSiteStore();
+ const { activeSector } = useSectorStore();
+ const {
+ form,
+ currentStep,
+ setStep,
+ setField,
+ updateStyle,
+ addObjective,
+ removeObjective,
+ nextStep,
+ previousStep,
+ submitWizard,
+ isSubmitting,
+ error,
+ activeBlueprint,
+ refreshPages,
+ pages,
+ generationProgress,
+ isGenerating,
+ syncContextFromStores,
+ } = useBuilderStore();
+
+ useEffect(() => {
+ syncContextFromStores();
+ }, [activeSite?.id, activeSite?.name, activeSector?.id]);
+
+ const steps = useMemo(
+ () => [
+ {
+ title: "Business context",
+ component: (
+
+ ),
+ },
+ {
+ title: "Brand brief",
+ component: ,
+ },
+ {
+ title: "Objectives",
+ component: (
+
+ ),
+ },
+ {
+ title: "Look & feel",
+ component: (
+
+ ),
+ },
+ ],
+ [form, setField, updateStyle, addObjective, removeObjective],
+ );
+
+ const isLastStep = currentStep === steps.length - 1;
+ const missingContext = !activeSite || !activeSector;
+
+ const handlePrimary = async () => {
+ if (isLastStep) {
+ await submitWizard();
+ } else {
+ nextStep();
+ }
+ };
return (
-
-
-
-
- Site Builder
-
-
- Create a new site using AI-powered wizard
-
+
+
+
+
+
+ Sites / Create Site
+
+
+ Site Builder
+
+
+ Create a new site using IGNY8’s AI-powered wizard. Align the estate,
+ strategy, and tone before publishing.
+
+
+
-
-
-
- Site Builder Wizard
-
-
- The Site Builder wizard is being integrated into the main app.
- Full implementation coming soon.
-
-
-
+
+
+ {missingContext && (
+
+ )}
+
+
+
+
+
+ {steps.map((step, index) => (
+
+ ))}
+
+
+
+ {steps[currentStep].component}
+
+ {error && (
+
+ )}
+
+
+
+ Step {currentStep + 1} of {steps.length}
+
+
+
+
+ ) : isLastStep ? (
+
+ ) : undefined
+ }
+ >
+ {isLastStep ? "Generate structure" : "Next"}
+
+
+
+
+
+
+
+ Latest blueprint
+
+ Once the wizard finishes, the most recent blueprint appears here.
+
+ {activeBlueprint ? (
+
+
+ Status
+ {activeBlueprint.status}
+
+
+ Pages generated
+ {pages.length}
+
+
}
+ onClick={() => refreshPages(activeBlueprint.id)}
+ >
+ Sync pages
+
+
+ ) : (
+
+ Run the wizard to create your first blueprint.
+
+ )}
+
+
+ {generationProgress && (
+
+ Generation progress
+
+ Tracking background tasks queued for this blueprint.
+
+
+
+ Pages queued
+ {generationProgress.pagesQueued}
+
+
+
+ Task IDs
+
+
+ {generationProgress.taskIds.join(", ")}
+
+
+
+ {generationProgress.celeryTaskId && (
+
+ Celery task ID: {generationProgress.celeryTaskId}
+
+ )}
+
+ )}
+
+ {isGenerating && (
+
+ )}
+
+
);
}
diff --git a/frontend/src/pages/Sites/Builder/steps/BriefStep.tsx b/frontend/src/pages/Sites/Builder/steps/BriefStep.tsx
new file mode 100644
index 00000000..1226f261
--- /dev/null
+++ b/frontend/src/pages/Sites/Builder/steps/BriefStep.tsx
@@ -0,0 +1,48 @@
+import type { BuilderFormData } from "../../../../types/siteBuilder";
+import { Card } from "../../../../components/ui/card";
+
+const labelClass =
+ "text-sm font-semibold text-gray-700 dark:text-white/80 mb-2 inline-block";
+const textareaClass =
+ "w-full rounded-2xl border border-gray-200 bg-white px-4 py-3 text-sm font-medium text-gray-900 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-white/10 dark:bg-white/[0.03] dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800";
+
+interface Props {
+ data: BuilderFormData;
+ onChange:
(
+ key: K,
+ value: BuilderFormData[K],
+ ) => void;
+}
+
+export function BriefStep({ data, onChange }: Props) {
+ return (
+
+
+
+
+ Brand narrative
+
+
+ Business brief
+
+
+ Describe the brand, the offer, and what makes it unique. The more
+ context we provide, the more precise the structure.
+
+
+
+
+
+
+
+
+ );
+}
+
diff --git a/frontend/src/pages/Sites/Builder/steps/BusinessDetailsStep.tsx b/frontend/src/pages/Sites/Builder/steps/BusinessDetailsStep.tsx
new file mode 100644
index 00000000..a6f7a369
--- /dev/null
+++ b/frontend/src/pages/Sites/Builder/steps/BusinessDetailsStep.tsx
@@ -0,0 +1,125 @@
+import type { BuilderFormData } from "../../../../types/siteBuilder";
+import { Card } from "../../../../components/ui/card";
+import { useSiteStore } from "../../../../store/siteStore";
+import { useSectorStore } from "../../../../store/sectorStore";
+
+const inputClass =
+ "h-11 w-full rounded-xl border border-gray-200 bg-white px-4 text-sm font-medium text-gray-900 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-white/10 dark:bg-white/[0.03] dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800";
+
+const labelClass =
+ "text-sm font-semibold text-gray-700 dark:text-white/80 mb-2 inline-block";
+
+interface Props {
+ data: BuilderFormData;
+ onChange: (
+ key: K,
+ value: BuilderFormData[K],
+ ) => void;
+}
+
+export function BusinessDetailsStep({ data, onChange }: Props) {
+ const { activeSite } = useSiteStore();
+ const { activeSector } = useSectorStore();
+
+ return (
+
+
+
+
+ Context
+
+
+ Site & Sector
+
+
+ The wizard will use your currently active site and sector. Switch
+ them from the header at any time.
+
+
+
+
+
+ Active Site
+
+
+ {activeSite?.name ?? "No site selected"}
+
+
+
+
+ Active Sector
+
+
+ {activeSector?.name ?? "All sectors"}
+
+
+
+
+
+
+
+
+
+ onChange("siteName", event.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ onChange("targetAudience", event.target.value)}
+ />
+
+
+
+ );
+}
+
diff --git a/frontend/src/pages/Sites/Builder/steps/ObjectivesStep.tsx b/frontend/src/pages/Sites/Builder/steps/ObjectivesStep.tsx
new file mode 100644
index 00000000..0c560efa
--- /dev/null
+++ b/frontend/src/pages/Sites/Builder/steps/ObjectivesStep.tsx
@@ -0,0 +1,90 @@
+import { useState } from "react";
+import type { BuilderFormData } from "../../../../types/siteBuilder";
+import { Card } from "../../../../components/ui/card/Card";
+import Button from "../../../../components/ui/button/Button";
+
+const inputClass =
+ "h-11 flex-1 rounded-xl border border-gray-200 bg-white px-4 text-sm font-medium text-gray-900 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-white/10 dark:bg-white/[0.03] dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800";
+
+interface Props {
+ data: BuilderFormData;
+ addObjective: (value: string) => void;
+ removeObjective: (index: number) => void;
+}
+
+export function ObjectivesStep({
+ data,
+ addObjective,
+ removeObjective,
+}: Props) {
+ const [value, setValue] = useState("");
+
+ const handleAdd = () => {
+ const trimmed = value.trim();
+ if (!trimmed) return;
+ addObjective(trimmed);
+ setValue("");
+ };
+
+ return (
+
+
+
+
+ Conversion goals
+
+
+ What should the site accomplish?
+
+
+ Each objective becomes navigation, hero CTAs, and supporting
+ sections. Add as many as you need.
+
+
+
+
+ {data.objectives.map((objective, idx) => (
+
+ {objective}
+
+
+ ))}
+ {data.objectives.length === 0 && (
+
+ No objectives yet. Add one below.
+
+ )}
+
+
+
+ setValue(event.target.value)}
+ />
+
+
+
+
+ );
+}
+
diff --git a/frontend/src/pages/Sites/Builder/steps/StyleStep.tsx b/frontend/src/pages/Sites/Builder/steps/StyleStep.tsx
new file mode 100644
index 00000000..90875f53
--- /dev/null
+++ b/frontend/src/pages/Sites/Builder/steps/StyleStep.tsx
@@ -0,0 +1,106 @@
+import type { StylePreferences } from "../../../../types/siteBuilder";
+import { Card } from "../../../../components/ui/card";
+
+const labelClass =
+ "text-sm font-semibold text-gray-700 dark:text-white/80 mb-2 inline-block";
+const selectClass =
+ "h-11 w-full rounded-xl border border-gray-200 bg-white px-4 text-sm font-medium text-gray-900 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-white/10 dark:bg-white/[0.03] dark:text-white/90 dark:focus:border-brand-800";
+const textareaClass =
+ "w-full rounded-2xl border border-gray-200 bg-white px-4 py-3 text-sm font-medium text-gray-900 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-white/10 dark:bg-white/[0.03] dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800";
+
+const palettes = [
+ "Minimal monochrome with bright accent",
+ "Rich jewel tones with high contrast",
+ "Soft gradients and glassmorphism",
+ "Playful pastel palette",
+];
+
+const typography = [
+ "Modern sans-serif for headings, serif body text",
+ "Editorial serif across the site",
+ "Geometric sans with tight tracking",
+ "Rounded fonts with friendly tone",
+];
+
+interface Props {
+ style: StylePreferences;
+ onChange: (partial: Partial) => void;
+}
+
+export function StyleStep({ style, onChange }: Props) {
+ return (
+
+
+
+
+ Look & feel
+
+
+ Visual direction
+
+
+ Capture the brand personality so the preview canvas mirrors the
+ right tone.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
diff --git a/frontend/src/services/siteBuilder.api.ts b/frontend/src/services/siteBuilder.api.ts
new file mode 100644
index 00000000..ae552a0e
--- /dev/null
+++ b/frontend/src/services/siteBuilder.api.ts
@@ -0,0 +1,125 @@
+/**
+ * Site Builder API Service
+ * Uses fetchAPI pattern (not axios) - handles authentication automatically
+ */
+import { fetchAPI } from './api';
+import type {
+ SiteBlueprint,
+ PageBlueprint,
+ SiteStructure,
+ BuilderFormData,
+} from '../types/siteBuilder';
+
+export interface CreateBlueprintPayload {
+ name: string;
+ description?: string;
+ site_id: number;
+ sector_id: number;
+ hosting_type: BuilderFormData['hostingType'];
+ config_json: Record;
+}
+
+export interface GenerateStructurePayload {
+ business_brief: string;
+ objectives: string[];
+ style: BuilderFormData['style'];
+ metadata?: Record;
+}
+
+/**
+ * Site Builder API functions
+ */
+export const siteBuilderApi = {
+ /**
+ * List all site blueprints
+ */
+ async listBlueprints(siteId?: number): Promise {
+ const params = siteId ? `?site=${siteId}` : '';
+ const response = await fetchAPI(`/v1/site-builder/blueprints/${params}`);
+ // Handle paginated response
+ if (response?.results) {
+ return response.results as SiteBlueprint[];
+ }
+ // Handle direct array response
+ return Array.isArray(response) ? response : [];
+ },
+
+ /**
+ * Get a single blueprint by ID
+ */
+ async getBlueprint(id: number): Promise {
+ return fetchAPI(`/v1/site-builder/blueprints/${id}/`);
+ },
+
+ /**
+ * Create a new site blueprint
+ */
+ async createBlueprint(payload: CreateBlueprintPayload): Promise {
+ return fetchAPI('/v1/site-builder/blueprints/', {
+ method: 'POST',
+ body: JSON.stringify(payload),
+ });
+ },
+
+ /**
+ * Generate site structure for a blueprint
+ */
+ async generateStructure(
+ blueprintId: number,
+ payload: GenerateStructurePayload,
+ ): Promise<{ task_id?: string; success?: boolean; structure?: SiteStructure }> {
+ return fetchAPI(`/v1/site-builder/blueprints/${blueprintId}/generate_structure/`, {
+ method: 'POST',
+ body: JSON.stringify(payload),
+ });
+ },
+
+ /**
+ * List pages for a blueprint
+ */
+ async listPages(blueprintId: number): Promise {
+ const response = await fetchAPI(`/v1/site-builder/pages/?site_blueprint=${blueprintId}`);
+ // Handle paginated response
+ if (response?.results) {
+ return response.results as PageBlueprint[];
+ }
+ // Handle direct array response
+ return Array.isArray(response) ? response : [];
+ },
+
+ /**
+ * Generate all pages for a blueprint
+ */
+ async generateAllPages(
+ blueprintId: number,
+ options?: { pageIds?: number[]; force?: boolean },
+ ): Promise<{ success: boolean; pages_queued: number; task_ids: number[]; celery_task_id?: string }> {
+ const response = await fetchAPI(`/v1/site-builder/blueprints/${blueprintId}/generate_all_pages/`, {
+ method: 'POST',
+ body: JSON.stringify({
+ page_ids: options?.pageIds,
+ force: options?.force || false,
+ }),
+ });
+ // Handle unified response format
+ return response?.data || response;
+ },
+
+ /**
+ * Create tasks for pages
+ */
+ async createTasksForPages(
+ blueprintId: number,
+ pageIds?: number[],
+ ): Promise<{ tasks: unknown[]; count: number }> {
+ const response = await fetchAPI(`/v1/site-builder/blueprints/${blueprintId}/create_tasks/`, {
+ method: 'POST',
+ body: JSON.stringify({
+ page_ids: pageIds,
+ }),
+ });
+ // Handle unified response format
+ return response?.data || response;
+ },
+};
+
diff --git a/frontend/src/store/builderStore.ts b/frontend/src/store/builderStore.ts
new file mode 100644
index 00000000..129e8278
--- /dev/null
+++ b/frontend/src/store/builderStore.ts
@@ -0,0 +1,251 @@
+import { create } from "zustand";
+import { useSiteStore } from "./siteStore";
+import { useSectorStore } from "./sectorStore";
+import { useSiteDefinitionStore } from "./siteDefinitionStore";
+import { siteBuilderApi } from "../services/siteBuilder.api";
+import type {
+ BuilderFormData,
+ PageBlueprint,
+ SiteBlueprint,
+ StylePreferences,
+} from "../types/siteBuilder";
+
+const defaultStyle: StylePreferences = {
+ palette: "Vibrant modern palette with rich accent color",
+ typography: "Sans-serif display for headings, humanist body font",
+ personality: "Confident, energetic, optimistic",
+ heroImagery: "Real people interacting with the product/service",
+};
+
+const buildDefaultForm = (): BuilderFormData => {
+ const site = useSiteStore.getState().activeSite;
+ const sector = useSectorStore.getState().activeSector;
+
+ return {
+ siteId: site?.id ?? null,
+ sectorId: sector?.id ?? null,
+ siteName: site?.name ?? "",
+ businessType: "",
+ industry: "",
+ targetAudience: "",
+ hostingType: "igny8_sites",
+ businessBrief: "",
+ objectives: ["Launch a conversion-focused marketing site"],
+ style: defaultStyle,
+ };
+};
+
+interface BuilderState {
+ form: BuilderFormData;
+ currentStep: number;
+ isSubmitting: boolean;
+ isGenerating: boolean;
+ error?: string;
+ activeBlueprint?: SiteBlueprint;
+ pages: PageBlueprint[];
+ selectedPageIds: number[];
+ generationProgress?: {
+ pagesQueued: number;
+ taskIds: number[];
+ celeryTaskId?: string;
+ };
+ // Actions
+ setField: (
+ key: K,
+ value: BuilderFormData[K],
+ ) => void;
+ updateStyle: (partial: Partial) => void;
+ addObjective: (value: string) => void;
+ removeObjective: (index: number) => void;
+ setStep: (step: number) => void;
+ nextStep: () => void;
+ previousStep: () => void;
+ reset: () => void;
+ syncContextFromStores: () => void;
+ submitWizard: () => Promise;
+ refreshPages: (blueprintId: number) => Promise;
+ togglePageSelection: (pageId: number) => void;
+ selectAllPages: () => void;
+ clearPageSelection: () => void;
+ generateAllPages: (blueprintId: number, force?: boolean) => Promise;
+}
+
+export const useBuilderStore = create((set, get) => ({
+ form: buildDefaultForm(),
+ currentStep: 0,
+ isSubmitting: false,
+ isGenerating: false,
+ pages: [],
+ selectedPageIds: [],
+
+ setField: (key, value) =>
+ set((state) => ({
+ form: { ...state.form, [key]: value },
+ })),
+
+ updateStyle: (partial) =>
+ set((state) => ({
+ form: { ...state.form, style: { ...state.form.style, ...partial } },
+ })),
+
+ addObjective: (value) =>
+ set((state) => ({
+ form: { ...state.form, objectives: [...state.form.objectives, value] },
+ })),
+
+ removeObjective: (index) =>
+ set((state) => ({
+ form: {
+ ...state.form,
+ objectives: state.form.objectives.filter((_, idx) => idx !== index),
+ },
+ })),
+
+ setStep: (step) => set({ currentStep: step }),
+
+ nextStep: () =>
+ set((state) => ({
+ currentStep: Math.min(state.currentStep + 1, 3),
+ })),
+
+ previousStep: () =>
+ set((state) => ({
+ currentStep: Math.max(state.currentStep - 1, 0),
+ })),
+
+ reset: () =>
+ set({
+ form: buildDefaultForm(),
+ currentStep: 0,
+ isSubmitting: false,
+ error: undefined,
+ activeBlueprint: undefined,
+ pages: [],
+ selectedPageIds: [],
+ generationProgress: undefined,
+ }),
+
+ syncContextFromStores: () => {
+ const site = useSiteStore.getState().activeSite;
+ const sector = useSectorStore.getState().activeSector;
+ set((state) => ({
+ form: {
+ ...state.form,
+ siteId: site?.id ?? state.form.siteId,
+ siteName: site?.name ?? state.form.siteName,
+ sectorId: sector?.id ?? state.form.sectorId,
+ },
+ }));
+ },
+
+ submitWizard: async () => {
+ const { form } = get();
+ if (!form.siteId || !form.sectorId) {
+ set({
+ error:
+ "Select an active site and sector before running the Site Builder wizard.",
+ });
+ return;
+ }
+
+ set({ isSubmitting: true, error: undefined });
+ try {
+ const payload = {
+ name: form.siteName || `Site Blueprint (${form.industry || "New"})`,
+ description: form.businessType
+ ? `${form.businessType} for ${form.targetAudience}`
+ : undefined,
+ site_id: form.siteId,
+ sector_id: form.sectorId,
+ hosting_type: form.hostingType,
+ config_json: {
+ business_type: form.businessType,
+ industry: form.industry,
+ target_audience: form.targetAudience,
+ },
+ };
+
+ const blueprint = await siteBuilderApi.createBlueprint(payload);
+ set({ activeBlueprint: blueprint });
+
+ const generation = await siteBuilderApi.generateStructure(
+ blueprint.id,
+ {
+ business_brief: form.businessBrief,
+ objectives: form.objectives,
+ style: form.style,
+ metadata: { targetAudience: form.targetAudience },
+ },
+ );
+
+ if (generation?.structure) {
+ useSiteDefinitionStore.getState().setStructure(generation.structure);
+ }
+
+ await get().refreshPages(blueprint.id);
+ } catch (error: any) {
+ set({
+ error: error?.message || "Unexpected error while running wizard",
+ });
+ } finally {
+ set({ isSubmitting: false });
+ }
+ },
+
+ refreshPages: async (blueprintId: number) => {
+ try {
+ const pages = await siteBuilderApi.listPages(blueprintId);
+ set({ pages });
+ useSiteDefinitionStore.getState().setPages(pages);
+ } catch (error: any) {
+ set({
+ error: error?.message || "Unable to load generated pages",
+ });
+ }
+ },
+
+ togglePageSelection: (pageId: number) =>
+ set((state) => {
+ const isSelected = state.selectedPageIds.includes(pageId);
+ return {
+ selectedPageIds: isSelected
+ ? state.selectedPageIds.filter((id) => id !== pageId)
+ : [...state.selectedPageIds, pageId],
+ };
+ }),
+
+ selectAllPages: () =>
+ set((state) => ({
+ selectedPageIds: state.pages.map((page) => page.id),
+ })),
+
+ clearPageSelection: () => set({ selectedPageIds: [] }),
+
+ generateAllPages: async (blueprintId: number, force = false) => {
+ const { selectedPageIds } = get();
+ set({ isGenerating: true, error: undefined, generationProgress: undefined });
+ try {
+ const result = await siteBuilderApi.generateAllPages(blueprintId, {
+ pageIds: selectedPageIds.length > 0 ? selectedPageIds : undefined,
+ force,
+ });
+
+ set({
+ generationProgress: {
+ pagesQueued: result.pages_queued,
+ taskIds: result.task_ids,
+ celeryTaskId: result.celery_task_id,
+ },
+ });
+
+ await get().refreshPages(blueprintId);
+ } catch (error: any) {
+ set({
+ error: error?.message || "Failed to queue page generation",
+ });
+ } finally {
+ set({ isGenerating: false });
+ }
+ },
+}));
+
diff --git a/frontend/src/store/siteDefinitionStore.ts b/frontend/src/store/siteDefinitionStore.ts
new file mode 100644
index 00000000..9565e3eb
--- /dev/null
+++ b/frontend/src/store/siteDefinitionStore.ts
@@ -0,0 +1,30 @@
+import { create } from "zustand";
+import type {
+ PageBlueprint,
+ SiteStructure,
+} from "../types/siteBuilder";
+
+interface SiteDefinitionState {
+ structure?: SiteStructure;
+ pages: PageBlueprint[];
+ selectedSlug?: string;
+ setStructure: (structure: SiteStructure) => void;
+ setPages: (pages: PageBlueprint[]) => void;
+ selectPage: (slug: string) => void;
+}
+
+export const useSiteDefinitionStore = create((set) => ({
+ pages: [],
+ setStructure: (structure) =>
+ set({
+ structure,
+ selectedSlug: structure.pages?.[0]?.slug,
+ }),
+ setPages: (pages) =>
+ set((state) => ({
+ pages,
+ selectedSlug: state.selectedSlug ?? pages[0]?.slug,
+ })),
+ selectPage: (slug) => set({ selectedSlug: slug }),
+}));
+
diff --git a/frontend/src/types/siteBuilder.ts b/frontend/src/types/siteBuilder.ts
new file mode 100644
index 00000000..108185d0
--- /dev/null
+++ b/frontend/src/types/siteBuilder.ts
@@ -0,0 +1,88 @@
+export type HostingType = 'igny8_sites' | 'wordpress' | 'shopify' | 'multi';
+
+export interface StylePreferences {
+ palette: string;
+ typography: string;
+ personality: string;
+ heroImagery: string;
+}
+
+export interface BuilderFormData {
+ siteId: number | null;
+ sectorId: number | null;
+ siteName: string;
+ businessType: string;
+ industry: string;
+ targetAudience: string;
+ hostingType: HostingType;
+ businessBrief: string;
+ objectives: string[];
+ style: StylePreferences;
+}
+
+export interface SiteBlueprint {
+ id: number;
+ name: string;
+ description?: string;
+ status: 'draft' | 'generating' | 'ready' | 'deployed';
+ hosting_type: HostingType;
+ config_json: Record;
+ structure_json: SiteStructure | null;
+ created_at: string;
+ updated_at: string;
+ site?: number;
+ sector?: number;
+}
+
+export interface PageBlueprint {
+ id: number;
+ site_blueprint: number;
+ slug: string;
+ title: string;
+ type: string;
+ status: string;
+ order: number;
+ blocks_json: PageBlock[];
+}
+
+export interface PageBlock {
+ type: string;
+ heading?: string;
+ subheading?: string;
+ layout?: string;
+ content?: string[] | Record;
+}
+
+export interface SiteStructure {
+ site?: {
+ name?: string;
+ primary_navigation?: string[];
+ secondary_navigation?: string[];
+ hero_message?: string;
+ tone?: string;
+ };
+ pages: Array<{
+ slug: string;
+ title: string;
+ type: string;
+ status?: string;
+ objective?: string;
+ primary_cta?: string;
+ blocks?: PageBlock[];
+ }>;
+}
+
+export interface ApiListResponse {
+ count?: number;
+ next?: string | null;
+ previous?: string | null;
+ results?: T[];
+ data?: T[] | T;
+}
+
+export interface ApiError {
+ message?: string;
+ error?: string;
+ detail?: string;
+}
+
diff --git a/sites/MIGRATION_SUMMARY.md b/sites/MIGRATION_SUMMARY.md
new file mode 100644
index 00000000..7605fa92
--- /dev/null
+++ b/sites/MIGRATION_SUMMARY.md
@@ -0,0 +1,116 @@
+# Site Builder → Sites Container Migration Summary
+
+## Overview
+Successfully merged Site Builder container into Sites container. Sites is now the primary container hosting both:
+- **Public Site Renderer** (no auth required)
+- **Site Builder** (auth required)
+
+## Structure Changes
+
+### New Directory Structure
+```
+sites/src/
+├── builder/ # Site Builder (from site-builder/src/)
+│ ├── pages/ # Wizard, Preview, Dashboard
+│ ├── components/ # Builder-specific components
+│ ├── state/ # Zustand stores
+│ ├── api/ # API client with conditional auth
+│ ├── types/ # TypeScript types
+│ └── App.css # Builder styles
+├── renderer/ # Sites Renderer (existing)
+│ ├── pages/ # SiteRenderer component
+│ ├── loaders/ # Site definition loaders
+│ ├── utils/ # Layout renderer utilities
+│ └── types/ # Renderer types
+├── shared/ # Shared components
+│ └── ProtectedRoute.tsx
+├── App.tsx # Unified router
+└── main.tsx # Entry point
+```
+
+## Routing Structure
+
+### Public Routes (No Auth)
+- `/:siteId/*` - Site renderer (public sites)
+- `/` - Root page
+
+### Builder Routes (Auth Required)
+- `/builder` - Wizard page
+- `/builder/preview` - Preview canvas
+- `/builder/dashboard` - Blueprint history
+
+## Key Changes
+
+### 1. Unified Router (`App.tsx`)
+- Single router handles both builder and renderer routes
+- Builder routes wrapped in `ProtectedRoute` component
+- Builder routes wrapped in `BuilderLayout` component
+- Code-splitting for builder routes (lazy loading)
+
+### 2. Authentication
+- `ProtectedRoute` component checks for JWT token in localStorage
+- Builder API client conditionally includes auth headers
+- Public renderer routes have no authentication
+
+### 3. API Clients
+- Builder API (`builder/api/builder.api.ts`): Includes auth token if available
+- Renderer API (`renderer/loaders/loadSiteDefinition.ts`): No auth required
+
+### 4. Build Configuration
+- Updated `vite.config.ts` with code-splitting
+- Builder routes split into separate chunk
+- Supports both `sites.igny8.com` and `builder.igny8.com` domains
+
+### 5. Docker Configuration
+- Removed `igny8_site_builder` service from docker-compose
+- Updated `igny8_sites` service comment
+- Single container now handles both functions
+
+## Backend (No Changes)
+- Backend Django views remain unchanged
+- API endpoints: `/api/v1/site-builder/` (same as before)
+- Database tables: No changes
+- Authentication: Same JWT-based auth
+
+## Migration Checklist
+
+✅ Created merged directory structure
+✅ Moved site-builder files to sites/src/builder/
+✅ Created unified App.tsx with routing
+✅ Created ProtectedRoute component
+✅ Created BuilderLayout component
+✅ Merged package.json dependencies
+✅ Updated vite.config.ts with code-splitting
+✅ Updated main.tsx entry point
+✅ Updated docker-compose.app.yml (removed site_builder service)
+✅ Updated API clients with conditional authentication
+✅ Updated index.css with builder styles
+
+## Next Steps
+
+1. **Test Builder Functionality**
+ - Navigate to `/builder` (should require auth)
+ - Test wizard flow
+ - Test preview
+ - Test dashboard
+
+2. **Test Renderer Functionality**
+ - Navigate to `/:siteId/*` (should work without auth)
+ - Test site loading from filesystem
+ - Test site loading from API
+
+3. **Build & Deploy**
+ - Build Docker image: `docker build -t igny8-sites-dev:latest -f Dockerfile.dev .`
+ - Update docker-compose: `docker compose -f docker-compose.app.yml up -d igny8_sites`
+
+4. **Cleanup** (Optional)
+ - Remove `/site-builder` directory after verification
+ - Update documentation
+
+## Notes
+
+- **No Backward Compatibility**: Old site-builder container is removed
+- **Backend Unchanged**: All Django views and models remain the same
+- **Code-Splitting**: Builder code is lazy-loaded, so public sites don't load builder code
+- **Route Conflicts**: Builder routes are namespaced under `/builder/*` to avoid conflicts
+
diff --git a/sites/package.json b/sites/package.json
index 1ff11f62..39c8e4c1 100644
--- a/sites/package.json
+++ b/sites/package.json
@@ -17,27 +17,29 @@
"lucide-react": "^0.554.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
- "react-router-dom": "^7.9.6"
+ "react-hook-form": "^7.66.0",
+ "react-router-dom": "^7.9.6",
+ "zustand": "^5.0.8"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
- "@testing-library/jest-dom": "^6.1.5",
- "@testing-library/react": "^14.1.2",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.5.1",
"@types/node": "^24.10.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
+ "@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^5.1.0",
"@vitest/ui": "^1.0.4",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
- "jsdom": "^23.0.1",
+ "jsdom": "^25.0.1",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.3",
"vite": "^7.2.2",
- "vitest": "^1.0.4"
+ "vitest": "^2.1.5"
}
}
-
diff --git a/sites/src/App.tsx b/sites/src/App.tsx
index 28cda5c9..8788c23f 100644
--- a/sites/src/App.tsx
+++ b/sites/src/App.tsx
@@ -1,16 +1,52 @@
-import { BrowserRouter, Routes, Route } from 'react-router-dom';
-import SiteRenderer from './pages/SiteRenderer';
+import { lazy, Suspense } from 'react';
+import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
+import ProtectedRoute from './shared/ProtectedRoute';
+import BuilderLayout from './builder/components/layout/BuilderLayout';
+
+// Lazy load builder pages (code-split to avoid loading in public sites)
+const WizardPage = lazy(() => import('./builder/pages/wizard/WizardPage'));
+const PreviewCanvas = lazy(() => import('./builder/pages/preview/PreviewCanvas'));
+const SiteDashboard = lazy(() => import('./builder/pages/dashboard/SiteDashboard'));
+
+// Renderer pages (load immediately for public sites)
+const SiteRenderer = lazy(() => import('./renderer/pages/SiteRenderer'));
+
+// Loading component
+const LoadingFallback = () => (
+
+);
function App() {
return (
-
- } />
- IGNY8 Sites Renderer} />
-
+
}>
+
+ {/* Public Site Renderer Routes (No Auth) */}
+ } />
+ IGNY8 Sites Renderer } />
+
+ {/* Builder Routes (Auth Required) */}
+
+
+
+ } />
+ } />
+ } />
+ } />
+
+
+
+ }
+ />
+
+
);
}
export default App;
-
diff --git a/sites/src/index.css b/sites/src/index.css
index 7c1a327d..e4368115 100644
--- a/sites/src/index.css
+++ b/sites/src/index.css
@@ -1,29 +1,28 @@
:root {
- font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
-
+ font-family: 'Inter', 'Inter var', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ color: #0f172a;
+ background-color: #f5f7fb;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
body {
margin: 0;
- display: flex;
- place-items: center;
- min-width: 320px;
- min-height: 100vh;
+ background: #f5f7fb;
}
-#root {
- width: 100%;
- margin: 0 auto;
- text-align: center;
+button {
+ font-family: inherit;
}
+a {
+ color: inherit;
+}
diff --git a/sites/src/main.tsx b/sites/src/main.tsx
index 1402347c..dcf08c3d 100644
--- a/sites/src/main.tsx
+++ b/sites/src/main.tsx
@@ -1,11 +1,10 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import App from './App.tsx'
-import './index.css'
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import App from './App.tsx';
+import './index.css';
createRoot(document.getElementById('root')!).render(
,
-)
-
+);
diff --git a/sites/src/shared/ProtectedRoute.tsx b/sites/src/shared/ProtectedRoute.tsx
new file mode 100644
index 00000000..fc658cee
--- /dev/null
+++ b/sites/src/shared/ProtectedRoute.tsx
@@ -0,0 +1,44 @@
+import { useEffect, useState } from 'react';
+import { Navigate, useLocation } from 'react-router-dom';
+
+/**
+ * ProtectedRoute component that checks for authentication token.
+ * Redirects to login if not authenticated.
+ */
+interface ProtectedRouteProps {
+ children: React.ReactNode;
+}
+
+export default function ProtectedRoute({ children }: ProtectedRouteProps) {
+ const [isAuthenticated, setIsAuthenticated] = useState(null);
+ const location = useLocation();
+
+ useEffect(() => {
+ // Check for JWT token in localStorage
+ const token = localStorage.getItem('auth-storage');
+ if (token) {
+ try {
+ const authData = JSON.parse(token);
+ setIsAuthenticated(!!authData?.state?.token);
+ } catch {
+ setIsAuthenticated(false);
+ }
+ } else {
+ setIsAuthenticated(false);
+ }
+ }, []);
+
+ if (isAuthenticated === null) {
+ // Still checking authentication
+ return Checking authentication...
;
+ }
+
+ if (!isAuthenticated) {
+ // Redirect to login (or main app login page)
+ // In production, this might redirect to app.igny8.com/login
+ return ;
+ }
+
+ return <>{children}>;
+}
+
diff --git a/sites/vite.config.ts b/sites/vite.config.ts
index 79523ee9..bfb17c9a 100644
--- a/sites/vite.config.ts
+++ b/sites/vite.config.ts
@@ -23,10 +23,22 @@ export default defineConfig({
server: {
host: '0.0.0.0',
port: 5176,
- allowedHosts: ['sites.igny8.com'],
+ allowedHosts: ['sites.igny8.com', 'builder.igny8.com'],
fs: {
allow: [path.resolve(__dirname, '..'), sharedComponentsPath],
},
},
+ build: {
+ rollupOptions: {
+ output: {
+ manualChunks: {
+ // Code-split builder routes to avoid loading in public sites
+ 'builder': ['./src/builder/pages/wizard/WizardPage', './src/builder/pages/preview/PreviewCanvas', './src/builder/pages/dashboard/SiteDashboard'],
+ // Vendor chunks
+ 'vendor-react': ['react', 'react-dom', 'react-router-dom'],
+ 'vendor-ui': ['lucide-react', 'zustand'],
+ },
+ },
+ },
+ },
});
-