diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule
index a0d31a14..8dd13fa1 100644
Binary files a/backend/celerybeat-schedule and b/backend/celerybeat-schedule differ
diff --git a/backend/igny8_core/settings.py b/backend/igny8_core/settings.py
index 4dae88e4..cf5983cd 100644
--- a/backend/igny8_core/settings.py
+++ b/backend/igny8_core/settings.py
@@ -454,11 +454,17 @@ CORS_ALLOWED_ORIGINS = [
"https://app.igny8.com",
"https://igny8.com",
"https://www.igny8.com",
+ "https://sites.igny8.com",
"http://localhost:5173",
"http://localhost:5174",
+ "http://localhost:5176",
+ "http://localhost:8024",
"http://localhost:3000",
"http://127.0.0.1:5173",
"http://127.0.0.1:5174",
+ "http://127.0.0.1:5176",
+ "http://127.0.0.1:8024",
+ "http://31.97.144.105:8024",
]
CORS_ALLOW_CREDENTIALS = True
diff --git a/sites/Dockerfile.dev b/sites/Dockerfile.dev
index 64399848..ef25028d 100644
--- a/sites/Dockerfile.dev
+++ b/sites/Dockerfile.dev
@@ -6,7 +6,7 @@ WORKDIR /app
# Copy package manifests first for better caching
COPY package*.json ./
-RUN npm install
+RUN npm install --legacy-peer-deps
# Copy source (still bind-mounted at runtime, but needed for initial run)
COPY . .
diff --git a/sites/src/App.tsx b/sites/src/App.tsx
index 8788c23f..5c948155 100644
--- a/sites/src/App.tsx
+++ b/sites/src/App.tsx
@@ -9,7 +9,7 @@ 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'));
+const SiteRenderer = lazy(() => import('./pages/SiteRenderer'));
// Loading component
const LoadingFallback = () => (
diff --git a/sites/src/builder/components/layout/BuilderLayout.tsx b/sites/src/builder/components/layout/BuilderLayout.tsx
new file mode 100644
index 00000000..d70e8054
--- /dev/null
+++ b/sites/src/builder/components/layout/BuilderLayout.tsx
@@ -0,0 +1,23 @@
+import { ReactNode } from 'react';
+
+interface BuilderLayoutProps {
+ children: ReactNode;
+}
+
+/**
+ * BuilderLayout - Layout wrapper for Site Builder pages
+ * Provides consistent layout for builder routes
+ */
+export default function BuilderLayout({ children }: BuilderLayoutProps) {
+ return (
+
+
+
+ {children}
+
+
+ );
+}
+
diff --git a/sites/src/builder/pages/dashboard/SiteDashboard.tsx b/sites/src/builder/pages/dashboard/SiteDashboard.tsx
new file mode 100644
index 00000000..57f90bd3
--- /dev/null
+++ b/sites/src/builder/pages/dashboard/SiteDashboard.tsx
@@ -0,0 +1,14 @@
+/**
+ * SiteDashboard - Site Builder Dashboard
+ * Placeholder component for builder dashboard functionality
+ */
+export default function SiteDashboard() {
+ return (
+
+
Site Builder Dashboard
+
This is a placeholder for the Site Builder Dashboard.
+
The builder functionality can be accessed from the main app at /sites/builder/dashboard
+
+ );
+}
+
diff --git a/sites/src/builder/pages/preview/PreviewCanvas.tsx b/sites/src/builder/pages/preview/PreviewCanvas.tsx
new file mode 100644
index 00000000..f232277c
--- /dev/null
+++ b/sites/src/builder/pages/preview/PreviewCanvas.tsx
@@ -0,0 +1,14 @@
+/**
+ * PreviewCanvas - Site Builder Preview
+ * Placeholder component for builder preview functionality
+ */
+export default function PreviewCanvas() {
+ return (
+
+
Site Builder Preview
+
This is a placeholder for the Site Builder Preview.
+
The builder functionality can be accessed from the main app at /sites/builder/preview
+
+ );
+}
+
diff --git a/sites/src/builder/pages/wizard/WizardPage.tsx b/sites/src/builder/pages/wizard/WizardPage.tsx
new file mode 100644
index 00000000..2ecedffb
--- /dev/null
+++ b/sites/src/builder/pages/wizard/WizardPage.tsx
@@ -0,0 +1,14 @@
+/**
+ * WizardPage - Site Builder Wizard
+ * Placeholder component for builder wizard functionality
+ */
+export default function WizardPage() {
+ return (
+
+
Site Builder Wizard
+
This is a placeholder for the Site Builder Wizard.
+
The builder functionality can be accessed from the main app at /sites/builder
+
+ );
+}
+
diff --git a/sites/src/loaders/loadSiteDefinition.ts b/sites/src/loaders/loadSiteDefinition.ts
index 0966d280..b5b51f1f 100644
--- a/sites/src/loaders/loadSiteDefinition.ts
+++ b/sites/src/loaders/loadSiteDefinition.ts
@@ -7,9 +7,39 @@
import axios from 'axios';
import type { SiteDefinition } from '../types';
-const API_URL = import.meta.env.VITE_API_URL || 'https://api.igny8.com/api';
const SITES_DATA_PATH = import.meta.env.SITES_DATA_PATH || '/sites';
+/**
+ * Get API base URL - auto-detect based on current origin
+ */
+function getApiBaseUrl(): string {
+ // First check environment variables
+ const envUrl = import.meta.env.VITE_API_URL;
+ if (envUrl) {
+ return envUrl.endsWith('/api') ? envUrl : `${envUrl}/api`;
+ }
+
+ // Auto-detect based on current origin (browser only)
+ if (typeof window !== 'undefined') {
+ const origin = window.location.origin;
+
+ // If accessing via IP address, use direct backend port
+ if (/^\d+\.\d+\.\d+\.\d+/.test(origin) || origin.includes('localhost') || origin.includes('127.0.0.1')) {
+ // Backend is on port 8011 (external) when accessed via IP
+ if (origin.includes(':8024')) {
+ return origin.replace(':8024', ':8011') + '/api';
+ }
+ // Default: try port 8011
+ return origin.split(':')[0] + ':8011/api';
+ }
+ }
+
+ // Production: use subdomain
+ return 'https://api.igny8.com/api';
+}
+
+const API_URL = getApiBaseUrl();
+
/**
* Load site definition by site ID.
* First tries to load from filesystem (deployed sites),
@@ -18,13 +48,26 @@ const SITES_DATA_PATH = import.meta.env.SITES_DATA_PATH || '/sites';
export async function loadSiteDefinition(siteId: string): Promise {
// Try API endpoint for deployed site definition first
try {
- const response = await axios.get(`${API_URL}/v1/publisher/sites/${siteId}/definition/`);
+ const response = await axios.get(`${API_URL}/v1/publisher/sites/${siteId}/definition/`, {
+ timeout: 10000, // 10 second timeout
+ headers: {
+ 'Accept': 'application/json',
+ },
+ });
if (response.data) {
return response.data as SiteDefinition;
}
} catch (error) {
// API load failed, try blueprint endpoint as fallback
- console.warn('Failed to load deployed site definition, trying blueprint:', error);
+ console.error('Failed to load deployed site definition:', error);
+ if (axios.isAxiosError(error)) {
+ console.error('Error details:', {
+ message: error.message,
+ code: error.code,
+ response: error.response?.status,
+ url: error.config?.url,
+ });
+ }
}
// Fallback to blueprint API (for non-deployed sites)
diff --git a/sites/src/utils/fileAccess.ts b/sites/src/utils/fileAccess.ts
index 016dee8d..981949cb 100644
--- a/sites/src/utils/fileAccess.ts
+++ b/sites/src/utils/fileAccess.ts
@@ -7,7 +7,30 @@
*/
const SITES_DATA_PATH = import.meta.env.SITES_DATA_PATH || '/sites';
-const API_URL = import.meta.env.VITE_API_URL || 'https://api.igny8.com/api';
+
+/**
+ * Get API base URL - auto-detect based on current origin
+ */
+function getApiBaseUrl(): string {
+ const envUrl = import.meta.env.VITE_API_URL;
+ if (envUrl) {
+ return envUrl.endsWith('/api') ? envUrl : `${envUrl}/api`;
+ }
+
+ if (typeof window !== 'undefined') {
+ const origin = window.location.origin;
+ if (/^\d+\.\d+\.\d+\.\d+/.test(origin) || origin.includes('localhost') || origin.includes('127.0.0.1')) {
+ if (origin.includes(':8024')) {
+ return origin.replace(':8024', ':8011') + '/api';
+ }
+ return origin.split(':')[0] + ':8011/api';
+ }
+ }
+
+ return 'https://api.igny8.com/api';
+}
+
+const API_URL = getApiBaseUrl();
/**
* Get file URL for a site asset.
diff --git a/sites/src/utils/layoutRenderer.tsx b/sites/src/utils/layoutRenderer.tsx
index deaad19d..69bd823f 100644
--- a/sites/src/utils/layoutRenderer.tsx
+++ b/sites/src/utils/layoutRenderer.tsx
@@ -189,16 +189,22 @@ function renderNavigation(navigation: SiteDefinition['navigation']): React.React
* Render pages.
*/
function renderPages(pages: SiteDefinition['pages']): React.ReactElement[] {
+ // Filter pages - include ready, generating, and deployed statuses
+ // Only exclude draft status
return pages
- .filter((page) => page.status === 'ready')
+ .filter((page) => page.status !== 'draft')
.map((page) => (
{page.title}
- {page.blocks.map((block, index) => (
-
- {renderTemplate(block)}
-
- ))}
+ {page.blocks && page.blocks.length > 0 ? (
+ page.blocks.map((block, index) => (
+
+ {renderTemplate(block)}
+
+ ))
+ ) : (
+
No content available for this page.
+ )}
));
}
diff --git a/sites/src/utils/templateEngine.tsx b/sites/src/utils/templateEngine.tsx
index 0d702ded..808822a7 100644
--- a/sites/src/utils/templateEngine.tsx
+++ b/sites/src/utils/templateEngine.tsx
@@ -11,8 +11,20 @@ import type { Block } from '../types';
* Render a block using the template engine.
* Imports shared components dynamically.
*/
-export function renderTemplate(block: Block): React.ReactElement {
- const { type, data } = block;
+export function renderTemplate(block: Block | any): React.ReactElement {
+ // Handle both formats: { type, data } or { type, ...properties }
+ let type: string;
+ let data: Record;
+
+ if (block.type && block.data) {
+ // Standard format: { type, data }
+ type = block.type;
+ data = block.data;
+ } else {
+ // API format: { type, heading, subheading, content, ... }
+ type = block.type;
+ data = block;
+ }
try {
// Try to import shared component
@@ -20,6 +32,10 @@ export function renderTemplate(block: Block): React.ReactElement {
switch (type) {
case 'hero':
return renderHeroBlock(data);
+ case 'features':
+ return renderFeaturesBlock(data);
+ case 'testimonials':
+ return renderTestimonialsBlock(data);
case 'text':
return renderTextBlock(data);
case 'image':
@@ -42,6 +58,14 @@ export function renderTemplate(block: Block): React.ReactElement {
return renderFormBlock(data);
case 'accordion':
return renderAccordionBlock(data);
+ case 'faq':
+ return renderFAQBlock(data);
+ case 'cta':
+ return renderCTABlock(data);
+ case 'services':
+ return renderServicesBlock(data);
+ case 'stats':
+ return renderStatsBlock(data);
default:
return Unknown block type: {type}
;
}
@@ -55,12 +79,18 @@ export function renderTemplate(block: Block): React.ReactElement {
* Render hero block.
*/
function renderHeroBlock(data: Record): React.ReactElement {
+ // Handle both API format (heading/subheading) and template format (title/subtitle)
+ const title = data.heading || data.title || 'Hero Title';
+ const subtitle = data.subheading || data.subtitle;
+ const content = Array.isArray(data.content) ? data.content.join(' ') : data.content;
+
return (
- {data.title || 'Hero Title'}
- {data.subtitle && {data.subtitle}
}
+ {title}
+ {subtitle && {subtitle}
}
+ {content && {content}
}
{data.buttonText && (
-
+
{data.buttonText}
)}
@@ -243,3 +273,154 @@ function renderAccordionBlock(data: Record): React.ReactElement {
);
}
+/**
+ * Render features block.
+ */
+function renderFeaturesBlock(data: Record): React.ReactElement {
+ const heading = data.heading || data.title || 'Features';
+ const subheading = data.subheading || data.subtitle;
+ const content = Array.isArray(data.content) ? data.content : [];
+ const layout = data.layout || 'two-column';
+
+ return (
+
+ {heading && {heading}
}
+ {subheading && {subheading}
}
+
+ {content.map((item: string, index: number) => (
+
+ ))}
+
+
+ );
+}
+
+/**
+ * Render testimonials block.
+ */
+function renderTestimonialsBlock(data: Record): React.ReactElement {
+ const heading = data.heading || data.title || 'Testimonials';
+ const subheading = data.subheading || data.subtitle;
+ const content = Array.isArray(data.content) ? data.content : [];
+ const layout = data.layout || 'cards';
+
+ return (
+
+ {heading && {heading}
}
+ {subheading && {subheading}
}
+
+ {content.map((item: string, index: number) => (
+
+ ))}
+
+
+ );
+}
+
+/**
+ * Render FAQ block.
+ */
+function renderFAQBlock(data: Record): React.ReactElement {
+ const heading = data.heading || data.title || 'FAQ';
+ const subheading = data.subheading || data.subtitle;
+ const content = Array.isArray(data.content) ? data.content : [];
+
+ return (
+
+ {heading && {heading}
}
+ {subheading && {subheading}
}
+
+ {content.map((item: string, index: number) => (
+
+ ))}
+
+
+ );
+}
+
+/**
+ * Render CTA (Call to Action) block.
+ */
+function renderCTABlock(data: Record): React.ReactElement {
+ const heading = data.heading || data.title || 'Call to Action';
+ const subheading = data.subheading || data.subtitle;
+ const content = Array.isArray(data.content) ? data.content[0] : data.content;
+
+ return (
+
+ {heading && {heading}
}
+ {subheading && {subheading}
}
+ {content && {content}
}
+ {data.buttonText && (
+
+ {data.buttonText}
+
+ )}
+
+ );
+}
+
+/**
+ * Render services block.
+ */
+function renderServicesBlock(data: Record): React.ReactElement {
+ const heading = data.heading || data.title || 'Services';
+ const subheading = data.subheading || data.subtitle;
+ const content = Array.isArray(data.content) ? data.content : [];
+
+ return (
+
+ {heading && {heading}
}
+ {subheading && {subheading}
}
+
+ {content.map((item: string, index: number) => (
+
+ ))}
+
+
+ );
+}
+
+/**
+ * Render stats block.
+ */
+function renderStatsBlock(data: Record): React.ReactElement {
+ const heading = data.heading || data.title || 'Statistics';
+ const subheading = data.subheading || data.subtitle;
+ const content = Array.isArray(data.content) ? data.content : [];
+
+ return (
+
+ {heading && {heading}
}
+ {subheading && {subheading}
}
+
+ {content.map((item: string, index: number) => (
+
+ ))}
+
+
+ );
+}
+