Add Sites Renderer service to Docker Compose and implement public endpoint for site definitions
- Introduced `igny8_sites` service in `docker-compose.app.yml` for serving deployed public sites. - Updated `SitesRendererAdapter` to construct deployment URLs dynamically based on environment variables. - Added `SiteDefinitionView` to provide a public API endpoint for retrieving deployed site definitions. - Enhanced `loadSiteDefinition` function to prioritize API calls for site definitions over filesystem access. - Updated frontend to utilize the new API endpoint for loading site definitions.
This commit is contained in:
Binary file not shown.
@@ -198,10 +198,16 @@ class SitesRendererAdapter(BaseAdapter):
|
|||||||
Returns:
|
Returns:
|
||||||
str: Deployment URL
|
str: Deployment URL
|
||||||
"""
|
"""
|
||||||
# TODO: Implement URL generation based on site configuration
|
|
||||||
# For now, return placeholder
|
|
||||||
site_id = site_blueprint.site.id
|
site_id = site_blueprint.site.id
|
||||||
return f"https://{site_id}.igny8.com" # Placeholder
|
|
||||||
|
# Get Sites Renderer URL from environment or use default
|
||||||
|
sites_renderer_host = os.getenv('SITES_RENDERER_HOST', '31.97.144.105')
|
||||||
|
sites_renderer_port = os.getenv('SITES_RENDERER_PORT', '8024')
|
||||||
|
sites_renderer_protocol = os.getenv('SITES_RENDERER_PROTOCOL', 'http')
|
||||||
|
|
||||||
|
# Construct URL: http://31.97.144.105:8024/{site_id}
|
||||||
|
# Sites Renderer routes: /:siteId/* -> SiteRenderer component
|
||||||
|
return f"{sites_renderer_protocol}://{sites_renderer_host}:{sites_renderer_port}/{site_id}"
|
||||||
|
|
||||||
# BaseAdapter interface implementation
|
# BaseAdapter interface implementation
|
||||||
def publish(
|
def publish(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from igny8_core.modules.publisher.views import (
|
|||||||
PublishingRecordViewSet,
|
PublishingRecordViewSet,
|
||||||
DeploymentRecordViewSet,
|
DeploymentRecordViewSet,
|
||||||
PublisherViewSet,
|
PublisherViewSet,
|
||||||
|
SiteDefinitionView,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
@@ -19,5 +20,7 @@ router.register(r'', PublisherViewSet, basename='publisher')
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', include(router.urls)),
|
path('', include(router.urls)),
|
||||||
|
# Public endpoint for Sites Renderer
|
||||||
|
path('sites/<int:site_id>/definition/', SiteDefinitionView.as_view(), name='site-definition'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,13 @@
|
|||||||
Publisher ViewSet
|
Publisher ViewSet
|
||||||
Phase 5: Sites Renderer & Publishing
|
Phase 5: Sites Renderer & Publishing
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
from rest_framework import status, viewsets
|
from rest_framework import status, viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from igny8_core.api.base import SiteSectorModelViewSet
|
from igny8_core.api.base import SiteSectorModelViewSet
|
||||||
from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove
|
from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove
|
||||||
@@ -196,3 +200,76 @@ class PublisherViewSet(viewsets.ViewSet):
|
|||||||
request
|
request
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SiteDefinitionView(APIView):
|
||||||
|
"""
|
||||||
|
Public endpoint to serve deployed site definitions.
|
||||||
|
Used by Sites Renderer to load site definitions.
|
||||||
|
No authentication required for public sites.
|
||||||
|
"""
|
||||||
|
permission_classes = [] # Public endpoint
|
||||||
|
|
||||||
|
def get(self, request, site_id):
|
||||||
|
"""
|
||||||
|
Get deployed site definition.
|
||||||
|
|
||||||
|
GET /api/v1/publisher/sites/{site_id}/definition/
|
||||||
|
"""
|
||||||
|
sites_data_path = os.getenv('SITES_DATA_PATH', '/data/app/sites-data')
|
||||||
|
|
||||||
|
# Try to find latest deployed version
|
||||||
|
site_dir = Path(sites_data_path) / 'clients' / str(site_id)
|
||||||
|
|
||||||
|
if not site_dir.exists():
|
||||||
|
return error_response(
|
||||||
|
f'Site {site_id} not found',
|
||||||
|
status.HTTP_404_NOT_FOUND,
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
||||||
|
# Look for latest version (check for 'latest' symlink or highest version number)
|
||||||
|
latest_path = site_dir / 'latest' / 'site.json'
|
||||||
|
if latest_path.exists():
|
||||||
|
try:
|
||||||
|
with open(latest_path, 'r', encoding='utf-8') as f:
|
||||||
|
definition = json.load(f)
|
||||||
|
return Response(definition, status=status.HTTP_200_OK)
|
||||||
|
except Exception as e:
|
||||||
|
return error_response(
|
||||||
|
f'Failed to load site definition: {str(e)}',
|
||||||
|
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fallback: find highest version number
|
||||||
|
version_dirs = [d for d in site_dir.iterdir() if d.is_dir() and d.name.startswith('v')]
|
||||||
|
if not version_dirs:
|
||||||
|
return error_response(
|
||||||
|
f'No deployed versions found for site {site_id}',
|
||||||
|
status.HTTP_404_NOT_FOUND,
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sort by version number (extract number from 'v1', 'v2', etc.)
|
||||||
|
try:
|
||||||
|
version_dirs.sort(key=lambda d: int(d.name[1:]) if d.name[1:].isdigit() else 0, reverse=True)
|
||||||
|
latest_version_dir = version_dirs[0]
|
||||||
|
site_json_path = latest_version_dir / 'site.json'
|
||||||
|
|
||||||
|
if site_json_path.exists():
|
||||||
|
with open(site_json_path, 'r', encoding='utf-8') as f:
|
||||||
|
definition = json.load(f)
|
||||||
|
return Response(definition, status=status.HTTP_200_OK)
|
||||||
|
except Exception as e:
|
||||||
|
return error_response(
|
||||||
|
f'Failed to load site definition: {str(e)}',
|
||||||
|
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
||||||
|
return error_response(
|
||||||
|
f'Site definition not found for site {site_id}',
|
||||||
|
status.HTTP_404_NOT_FOUND,
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,26 @@ services:
|
|||||||
- "com.docker.compose.project=igny8-app"
|
- "com.docker.compose.project=igny8-app"
|
||||||
- "com.docker.compose.service=igny8_marketing_dev"
|
- "com.docker.compose.service=igny8_marketing_dev"
|
||||||
|
|
||||||
|
igny8_sites:
|
||||||
|
# Sites Renderer - serves deployed public sites
|
||||||
|
# Build separately: docker build -t igny8-sites-dev:latest -f Dockerfile.dev .
|
||||||
|
# Accessible at http://31.97.144.105:8024 (direct) or via Caddy routing
|
||||||
|
image: igny8-sites-dev:latest
|
||||||
|
container_name: igny8_sites
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "0.0.0.0:8024:5176" # Sites renderer port (internal: 5176, external: 8024)
|
||||||
|
environment:
|
||||||
|
VITE_API_URL: "https://api.igny8.com/api"
|
||||||
|
SITES_DATA_PATH: "/sites"
|
||||||
|
volumes:
|
||||||
|
- /data/app/igny8/sites:/app:rw
|
||||||
|
- /data/app/sites-data:/sites:ro # Read-only access to deployed sites
|
||||||
|
networks: [igny8_net]
|
||||||
|
labels:
|
||||||
|
- "com.docker.compose.project=igny8-app"
|
||||||
|
- "com.docker.compose.service=igny8_sites"
|
||||||
|
|
||||||
|
|
||||||
igny8_celery_worker:
|
igny8_celery_worker:
|
||||||
image: igny8-backend:latest
|
image: igny8-backend:latest
|
||||||
|
|||||||
@@ -53,18 +53,20 @@ export default function SitePreview() {
|
|||||||
// If deployment exists but no URL, construct from Sites Renderer
|
// If deployment exists but no URL, construct from Sites Renderer
|
||||||
// Sites Renderer should be accessible at a different port or subdomain
|
// Sites Renderer should be accessible at a different port or subdomain
|
||||||
// Check if we have the Sites Renderer URL configured
|
// Check if we have the Sites Renderer URL configured
|
||||||
|
// Use VPS IP or configured URL for Sites Renderer
|
||||||
const sitesRendererUrl = import.meta.env.VITE_SITES_RENDERER_URL ||
|
const sitesRendererUrl = import.meta.env.VITE_SITES_RENDERER_URL ||
|
||||||
(window as any).__SITES_RENDERER_URL__ ||
|
(window as any).__SITES_RENDERER_URL__ ||
|
||||||
'http://localhost:8024';
|
'http://31.97.144.105:8024';
|
||||||
setPreviewUrl(`${sitesRendererUrl}/${siteId}`);
|
setPreviewUrl(`${sitesRendererUrl}/${siteId}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('No deployment record found:', error);
|
console.warn('No deployment record found:', error);
|
||||||
// If blueprint is deployed but no deployment record, try Sites Renderer directly
|
// If blueprint is deployed but no deployment record, try Sites Renderer directly
|
||||||
if (latestBlueprint.status === 'deployed') {
|
if (latestBlueprint.status === 'deployed') {
|
||||||
|
// Use VPS IP or configured URL for Sites Renderer
|
||||||
const sitesRendererUrl = import.meta.env.VITE_SITES_RENDERER_URL ||
|
const sitesRendererUrl = import.meta.env.VITE_SITES_RENDERER_URL ||
|
||||||
(window as any).__SITES_RENDERER_URL__ ||
|
(window as any).__SITES_RENDERER_URL__ ||
|
||||||
'http://localhost:8024';
|
'http://31.97.144.105:8024';
|
||||||
setPreviewUrl(`${sitesRendererUrl}/${siteId}`);
|
setPreviewUrl(`${sitesRendererUrl}/${siteId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,26 +16,30 @@ const SITES_DATA_PATH = import.meta.env.SITES_DATA_PATH || '/sites';
|
|||||||
* then falls back to API.
|
* then falls back to API.
|
||||||
*/
|
*/
|
||||||
export async function loadSiteDefinition(siteId: string): Promise<SiteDefinition> {
|
export async function loadSiteDefinition(siteId: string): Promise<SiteDefinition> {
|
||||||
// Try filesystem first (for deployed sites)
|
// Try API endpoint for deployed site definition first
|
||||||
try {
|
try {
|
||||||
const fsPath = `${SITES_DATA_PATH}/clients/${siteId}/latest/site.json`;
|
const response = await axios.get(`${API_URL}/v1/publisher/sites/${siteId}/definition/`);
|
||||||
const response = await fetch(fsPath);
|
if (response.data) {
|
||||||
if (response.ok) {
|
return response.data as SiteDefinition;
|
||||||
const definition = await response.json();
|
|
||||||
return definition;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Filesystem load failed, try API
|
// API load failed, try blueprint endpoint as fallback
|
||||||
console.warn('Failed to load from filesystem, trying API:', error);
|
console.warn('Failed to load deployed site definition, trying blueprint:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to API
|
// Fallback to blueprint API (for non-deployed sites)
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${API_URL}/v1/site-builder/blueprints/${siteId}/`);
|
const response = await axios.get(`${API_URL}/v1/site-builder/blueprints/?site=${siteId}`);
|
||||||
const blueprint = response.data;
|
const blueprints = Array.isArray(response.data?.results) ? response.data.results :
|
||||||
|
Array.isArray(response.data) ? response.data : [];
|
||||||
|
|
||||||
// Transform blueprint to site definition format
|
if (blueprints.length > 0) {
|
||||||
return transformBlueprintToSiteDefinition(blueprint);
|
const blueprint = blueprints[0]; // Get latest blueprint
|
||||||
|
// Transform blueprint to site definition format
|
||||||
|
return transformBlueprintToSiteDefinition(blueprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`No blueprint found for site ${siteId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (axios.isAxiosError(error)) {
|
if (axios.isAxiosError(error)) {
|
||||||
throw new Error(`Failed to load site: ${error.message}`);
|
throw new Error(`Failed to load site: ${error.message}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user