diff --git a/backend/igny8_core/business/content/models.py b/backend/igny8_core/business/content/models.py
index 5aeaa301..17b4f6f1 100644
--- a/backend/igny8_core/business/content/models.py
+++ b/backend/igny8_core/business/content/models.py
@@ -272,16 +272,13 @@ class Content(SiteSectorBaseModel):
class ContentTaxonomy(SiteSectorBaseModel):
"""
- Universal taxonomy model for WordPress and IGNY8 cluster-based taxonomies.
- Supports categories, tags, product attributes, and cluster mappings.
+ Simplified taxonomy model for AI-generated categories and tags.
+ Directly linked to Content via many-to-many relationship.
"""
TAXONOMY_TYPE_CHOICES = [
('category', 'Category'),
('tag', 'Tag'),
- ('product_category', 'Product Category'),
- ('product_attribute', 'Product Attribute'),
- ('cluster', 'Cluster Taxonomy'),
]
name = models.CharField(max_length=255, db_index=True, help_text="Term name")
@@ -290,7 +287,7 @@ class ContentTaxonomy(SiteSectorBaseModel):
max_length=50,
choices=TAXONOMY_TYPE_CHOICES,
db_index=True,
- help_text="Type of taxonomy"
+ help_text="Type of taxonomy (category or tag)"
)
# WordPress/external platform sync fields
@@ -298,25 +295,19 @@ class ContentTaxonomy(SiteSectorBaseModel):
max_length=100,
blank=True,
default='',
- help_text="WordPress taxonomy slug (category, post_tag, product_cat, pa_*) - empty for cluster taxonomies"
+ help_text="WordPress taxonomy slug (category, post_tag)"
)
external_id = models.IntegerField(
null=True,
blank=True,
db_index=True,
- help_text="WordPress term_id - null for cluster taxonomies"
+ help_text="WordPress term_id for sync"
)
description = models.TextField(
blank=True,
default='',
help_text="Taxonomy term description"
)
- sync_status = models.CharField(
- max_length=50,
- blank=True,
- default='',
- help_text="Synchronization status with external platforms"
- )
count = models.IntegerField(
default=0,
help_text="Number of times this term is used"
@@ -324,7 +315,7 @@ class ContentTaxonomy(SiteSectorBaseModel):
metadata = models.JSONField(
default=dict,
blank=True,
- help_text="Additional metadata for the taxonomy term"
+ help_text="Additional metadata (AI generation details, etc.)"
)
created_at = models.DateTimeField(auto_now_add=True)
@@ -483,59 +474,6 @@ class ContentClusterMap(SiteSectorBaseModel):
return f"{self.cluster.name} ({self.get_role_display()})"
-class ContentTaxonomyMap(SiteSectorBaseModel):
- """Maps content entities to blueprint taxonomies for syncing/publishing."""
-
- SOURCE_CHOICES = [
- ('blueprint', 'Blueprint'),
- ('manual', 'Manual'),
- ('import', 'Import'),
- ]
-
- content = models.ForeignKey(
- Content,
- on_delete=models.CASCADE,
- related_name='taxonomy_mappings',
- null=True,
- blank=True,
- )
- task = models.ForeignKey(
- Tasks,
- on_delete=models.CASCADE,
- related_name='taxonomy_mappings',
- null=True,
- blank=True,
- )
- taxonomy = models.ForeignKey(
- 'site_building.SiteBlueprintTaxonomy',
- on_delete=models.CASCADE,
- related_name='content_mappings',
- )
- source = models.CharField(max_length=50, choices=SOURCE_CHOICES, default='blueprint')
- metadata = models.JSONField(default=dict, blank=True)
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
-
- class Meta:
- app_label = 'writer'
- db_table = 'igny8_content_taxonomy_map'
- unique_together = [['content', 'taxonomy']]
- indexes = [
- models.Index(fields=['taxonomy']),
- models.Index(fields=['content', 'taxonomy']),
- models.Index(fields=['task', 'taxonomy']),
- ]
-
- def save(self, *args, **kwargs):
- provider = self.content or self.task
- if provider:
- self.account = provider.account
- self.site = provider.site
- self.sector = provider.sector
- super().save(*args, **kwargs)
-
- def __str__(self):
- return f"{self.taxonomy.name}"
class ContentAttribute(SiteSectorBaseModel):
diff --git a/backend/igny8_core/business/content/services/metadata_mapping_service.py b/backend/igny8_core/business/content/services/metadata_mapping_service.py
index 86d6cee5..7f7a6123 100644
--- a/backend/igny8_core/business/content/services/metadata_mapping_service.py
+++ b/backend/igny8_core/business/content/services/metadata_mapping_service.py
@@ -1,6 +1,7 @@
"""
Metadata Mapping Service
-Stage 3: Persists cluster/taxonomy/attribute mappings from Tasks to Content
+Stage 3: Persists cluster/attribute mappings from Tasks to Content
+Legacy: ContentTaxonomyMap removed - taxonomy now uses Content.taxonomy_terms M2M relationship
"""
import logging
from typing import Optional
@@ -10,9 +11,9 @@ from igny8_core.business.content.models import (
Tasks,
Content,
ContentClusterMap,
- ContentTaxonomyMap,
ContentAttributeMap,
)
+# Removed: ContentTaxonomyMap - replaced by Content.taxonomy_terms ManyToManyField
logger = logging.getLogger(__name__)
diff --git a/backend/igny8_core/business/linking/services/candidate_engine.py b/backend/igny8_core/business/linking/services/candidate_engine.py
index 59562f30..4ba958c1 100644
--- a/backend/igny8_core/business/linking/services/candidate_engine.py
+++ b/backend/igny8_core/business/linking/services/candidate_engine.py
@@ -80,16 +80,16 @@ class CandidateEngine:
def _score_candidates(self, content: Content, candidates: List[Content]) -> List[Dict]:
"""Score candidates based on relevance"""
- from igny8_core.business.content.models import ContentClusterMap, ContentTaxonomyMap
+ from igny8_core.business.content.models import ContentClusterMap
# Stage 3: Get cluster mappings for content
content_clusters = set(
ContentClusterMap.objects.filter(content=content)
.values_list('cluster_id', flat=True)
)
+ # Taxonomy matching using Content.taxonomy_terms M2M relationship
content_taxonomies = set(
- ContentTaxonomyMap.objects.filter(content=content)
- .values_list('taxonomy_id', flat=True)
+ content.taxonomy_terms.values_list('id', flat=True)
)
scored = []
@@ -106,10 +106,9 @@ class CandidateEngine:
if cluster_overlap:
score += 50 * len(cluster_overlap) # High weight for cluster matches
- # Stage 3: Taxonomy matching
+ # Stage 3: Taxonomy matching using M2M relationship
candidate_taxonomies = set(
- ContentTaxonomyMap.objects.filter(content=candidate)
- .values_list('taxonomy_id', flat=True)
+ candidate.taxonomy_terms.values_list('id', flat=True)
)
taxonomy_overlap = content_taxonomies & candidate_taxonomies
if taxonomy_overlap:
diff --git a/backend/igny8_core/business/optimization/services/analyzer.py b/backend/igny8_core/business/optimization/services/analyzer.py
index 21368f2a..378f583c 100644
--- a/backend/igny8_core/business/optimization/services/analyzer.py
+++ b/backend/igny8_core/business/optimization/services/analyzer.py
@@ -102,9 +102,8 @@ class ContentAnalyzer:
return ContentClusterMap.objects.filter(content=content).exists()
def _has_taxonomy_mapping(self, content: Content) -> bool:
- """Stage 3: Check if content has taxonomy mapping"""
- from igny8_core.business.content.models import ContentTaxonomyMap
- return ContentTaxonomyMap.objects.filter(content=content).exists()
+ """Stage 3: Check if content has taxonomy mapping (categories/tags)"""
+ return content.taxonomy_terms.exists()
def _calculate_seo_score(self, content: Content) -> float:
"""Calculate SEO score (0-100)"""
diff --git a/backend/igny8_core/business/planning/models.py b/backend/igny8_core/business/planning/models.py
index f92e72c2..8a9830ca 100644
--- a/backend/igny8_core/business/planning/models.py
+++ b/backend/igny8_core/business/planning/models.py
@@ -191,14 +191,7 @@ class ContentIdeas(SiteSectorBaseModel):
related_name='ideas',
limit_choices_to={'sector': models.F('sector')}
)
- taxonomy = models.ForeignKey(
- 'site_building.SiteBlueprintTaxonomy',
- on_delete=models.SET_NULL,
- null=True,
- blank=True,
- related_name='content_ideas',
- help_text="Optional taxonomy association when derived from blueprint planning"
- )
+ # REMOVED: taxonomy FK to SiteBlueprintTaxonomy (legacy blueprint functionality)
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='new')
estimated_word_count = models.IntegerField(default=1000)
content_type = models.CharField(
diff --git a/backend/igny8_core/business/publishing/migrations/0001_initial.py b/backend/igny8_core/business/publishing/migrations/0001_initial.py
index fad5052c..58d1477b 100644
--- a/backend/igny8_core/business/publishing/migrations/0001_initial.py
+++ b/backend/igny8_core/business/publishing/migrations/0001_initial.py
@@ -10,7 +10,7 @@ class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0001_initial'),
- ('site_building', '0001_initial'),
+ # ('site_building', '0001_initial'), # REMOVED: SiteBuilder deprecated
('writer', '0001_initial'),
]
@@ -31,12 +31,12 @@ class Migration(migrations.Migration):
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account')),
('sector', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.sector')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.site')),
- ('site_blueprint', models.ForeignKey(help_text='Site blueprint being deployed', on_delete=django.db.models.deletion.CASCADE, related_name='deployments', to='site_building.siteblueprint')),
+ # ('site_blueprint', ...) REMOVED: SiteBuilder deprecated
],
options={
'db_table': 'igny8_deployment_records',
'ordering': ['-created_at'],
- 'indexes': [models.Index(fields=['site_blueprint', 'status'], name='igny8_deplo_site_bl_14c185_idx'), models.Index(fields=['site_blueprint', 'version'], name='igny8_deplo_site_bl_34f669_idx'), models.Index(fields=['status'], name='igny8_deplo_status_5cb014_idx'), models.Index(fields=['account', 'status'], name='igny8_deplo_tenant__4de41d_idx')],
+ 'indexes': [models.Index(fields=['status'], name='igny8_deplo_status_5cb014_idx'), models.Index(fields=['account', 'status'], name='igny8_deplo_tenant__4de41d_idx')],
},
),
migrations.CreateModel(
@@ -56,12 +56,12 @@ class Migration(migrations.Migration):
('content', models.ForeignKey(blank=True, help_text='Content being published (if publishing content)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='publishing_records', to='writer.content')),
('sector', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.sector')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.site')),
- ('site_blueprint', models.ForeignKey(blank=True, help_text='Site blueprint being published (if publishing site)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='publishing_records', to='site_building.siteblueprint')),
+ # ('site_blueprint', ...) REMOVED: SiteBuilder deprecated
],
options={
'db_table': 'igny8_publishing_records',
'ordering': ['-created_at'],
- 'indexes': [models.Index(fields=['destination', 'status'], name='igny8_publi_destina_5706a3_idx'), models.Index(fields=['content', 'destination'], name='igny8_publi_content_3688ba_idx'), models.Index(fields=['site_blueprint', 'destination'], name='igny8_publi_site_bl_963f5d_idx'), models.Index(fields=['account', 'status'], name='igny8_publi_tenant__2e0749_idx')],
+ 'indexes': [models.Index(fields=['destination', 'status'], name='igny8_publi_destina_5706a3_idx'), models.Index(fields=['content', 'destination'], name='igny8_publi_content_3688ba_idx'), models.Index(fields=['account', 'status'], name='igny8_publi_tenant__2e0749_idx')],
},
),
]
diff --git a/backend/igny8_core/business/publishing/models.py b/backend/igny8_core/business/publishing/models.py
index 47e521e9..d8ae6a9c 100644
--- a/backend/igny8_core/business/publishing/models.py
+++ b/backend/igny8_core/business/publishing/models.py
@@ -18,22 +18,14 @@ class PublishingRecord(SiteSectorBaseModel):
('failed', 'Failed'),
]
- # Content or SiteBlueprint reference (one must be set)
+ # Content reference
content = models.ForeignKey(
'writer.Content',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='publishing_records',
- help_text="Content being published (if publishing content)"
- )
- site_blueprint = models.ForeignKey(
- 'site_building.SiteBlueprint',
- on_delete=models.CASCADE,
- null=True,
- blank=True,
- related_name='publishing_records',
- help_text="Site blueprint being published (if publishing site)"
+ help_text="Content being published"
)
# Destination information
@@ -80,18 +72,17 @@ class PublishingRecord(SiteSectorBaseModel):
indexes = [
models.Index(fields=['destination', 'status']),
models.Index(fields=['content', 'destination']),
- models.Index(fields=['site_blueprint', 'destination']),
models.Index(fields=['account', 'status']),
]
def __str__(self):
- target = self.content or self.site_blueprint
- return f"{target} β {self.destination} ({self.get_status_display()})"
+ return f"{self.content} β {self.destination} ({self.get_status_display()})"
class DeploymentRecord(SiteSectorBaseModel):
"""
Track site deployments to Sites renderer.
+ Legacy model - SiteBlueprint functionality removed.
"""
STATUS_CHOICES = [
@@ -102,12 +93,7 @@ class DeploymentRecord(SiteSectorBaseModel):
('rolled_back', 'Rolled Back'),
]
- site_blueprint = models.ForeignKey(
- 'site_building.SiteBlueprint',
- on_delete=models.CASCADE,
- related_name='deployments',
- help_text="Site blueprint being deployed"
- )
+ # Legacy: site_blueprint field removed - now using site from SiteSectorBaseModel directly
# Version tracking
version = models.IntegerField(
@@ -148,12 +134,12 @@ class DeploymentRecord(SiteSectorBaseModel):
db_table = 'igny8_deployment_records'
ordering = ['-created_at']
indexes = [
- models.Index(fields=['site_blueprint', 'status']),
- models.Index(fields=['site_blueprint', 'version']),
models.Index(fields=['status']),
models.Index(fields=['account', 'status']),
+ models.Index(fields=['site', 'status']),
]
def __str__(self):
- return f"{self.site_blueprint.name} v{self.version} ({self.get_status_display()})"
+ return f"Deployment v{self.version} for {self.site.name if self.site else 'Unknown'} ({self.get_status_display()})"
+
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 025c7502..8ac3df1d 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
@@ -115,7 +115,7 @@ class SitesRendererAdapter(BaseAdapter):
Returns:
dict: Site definition structure
"""
- from igny8_core.business.content.models import Tasks, Content, ContentClusterMap, ContentTaxonomyMap
+ from igny8_core.business.content.models import Tasks, Content, ContentClusterMap
# Get all pages
pages = []
@@ -129,8 +129,7 @@ class SitesRendererAdapter(BaseAdapter):
'cluster_id': None,
'cluster_name': None,
'content_structure': None,
- 'taxonomy_id': None,
- 'taxonomy_name': None,
+ 'taxonomy_terms': [], # Changed from taxonomy_id/taxonomy_name to list of terms
'internal_links': []
}
@@ -180,11 +179,13 @@ class SitesRendererAdapter(BaseAdapter):
page_metadata['cluster_name'] = cluster_map.cluster.name
page_metadata['content_structure'] = cluster_map.role or task.content_structure if task else None
- # Get taxonomy mapping
- taxonomy_map = ContentTaxonomyMap.objects.filter(content=content).first()
- if taxonomy_map and taxonomy_map.taxonomy:
- page_metadata['taxonomy_id'] = taxonomy_map.taxonomy.id
- page_metadata['taxonomy_name'] = taxonomy_map.taxonomy.name
+ # Get taxonomy terms using M2M relationship
+ taxonomy_terms = content.taxonomy_terms.all()
+ if taxonomy_terms.exists():
+ page_metadata['taxonomy_terms'] = [
+ {'id': term.id, 'name': term.name, 'type': term.taxonomy_type}
+ for term in taxonomy_terms
+ ]
# Get internal links from content
if content.internal_links:
diff --git a/backend/igny8_core/business/publishing/services/publisher_service.py b/backend/igny8_core/business/publishing/services/publisher_service.py
index 88deec5a..386c020a 100644
--- a/backend/igny8_core/business/publishing/services/publisher_service.py
+++ b/backend/igny8_core/business/publishing/services/publisher_service.py
@@ -1,22 +1,24 @@
"""
Publisher Service
-Phase 5: Sites Renderer & Publishing
+Phase 5: Content Publishing
-Main publishing orchestrator for content and sites.
+Main publishing orchestrator for content.
+Legacy: SiteBlueprint publishing removed.
"""
import logging
from typing import Optional, List, Dict, Any
from igny8_core.business.publishing.models import PublishingRecord, DeploymentRecord
-from igny8_core.business.site_building.models import SiteBlueprint
+# Removed: from igny8_core.business.site_building.models import SiteBlueprint
logger = logging.getLogger(__name__)
class PublisherService:
"""
- Main publishing service for content and sites.
+ Main publishing service for content.
Routes to appropriate adapters based on destination.
+ Legacy: SiteBlueprint publishing removed.
"""
def __init__(self):
@@ -27,36 +29,6 @@ class PublisherService:
"""Lazy load adapters to avoid circular imports"""
pass # Will be implemented when adapters are created
- def publish_to_sites(self, site_blueprint: SiteBlueprint) -> Dict[str, Any]:
- """
- Publish site to Sites renderer.
-
- Args:
- site_blueprint: SiteBlueprint instance to deploy
-
- Returns:
- dict: Deployment result with status and deployment record
- """
- from igny8_core.business.publishing.services.adapters.sites_renderer_adapter import SitesRendererAdapter
-
- adapter = SitesRendererAdapter()
- return adapter.deploy(site_blueprint)
-
- def get_deployment_status(self, site_blueprint: SiteBlueprint) -> Optional[DeploymentRecord]:
- """
- Get deployment status for a site.
-
- Args:
- site_blueprint: SiteBlueprint instance
-
- Returns:
- DeploymentRecord or None
- """
- from igny8_core.business.publishing.services.deployment_service import DeploymentService
-
- service = DeploymentService()
- return service.get_status(site_blueprint)
-
def publish_content(
self,
content_id: int,
@@ -216,7 +188,7 @@ class PublisherService:
Publish content to multiple destinations.
Args:
- content: Content instance or SiteBlueprint
+ content: Content instance
destinations: List of destination configs, e.g.:
[
{'platform': 'wordpress', 'site_url': '...', 'username': '...', 'app_password': '...'},
@@ -272,8 +244,7 @@ class PublisherService:
account=account,
site=content.site,
sector=content.sector,
- content=content if hasattr(content, 'id') and not isinstance(content, SiteBlueprint) else None,
- site_blueprint=content if isinstance(content, SiteBlueprint) else None,
+ content=content if hasattr(content, 'id') else None,
destination=platform,
status='published' if result.get('success') else 'failed',
destination_id=result.get('external_id'),
@@ -319,7 +290,7 @@ class PublisherService:
Publish content using site integrations.
Args:
- content: Content instance or SiteBlueprint
+ content: Content instance
site: Site instance
account: Account instance
platforms: Optional list of platforms to publish to (all active if None)
diff --git a/backend/igny8_core/business/site_building/admin.py b/backend/igny8_core/business/site_building/admin.py
index 9765f0be..a4d9de6e 100644
--- a/backend/igny8_core/business/site_building/admin.py
+++ b/backend/igny8_core/business/site_building/admin.py
@@ -1,250 +1,12 @@
"""
-Admin interface for Site Builder models
+Admin interface for Site Building
+Legacy SiteBlueprint admin removed - models deprecated.
"""
from django.contrib import admin
-from django.utils.html import format_html
-from django.urls import reverse
-from igny8_core.admin.base import SiteSectorAdminMixin
-from .models import (
- SiteBlueprint,
- PageBlueprint,
- BusinessType,
- AudienceProfile,
- BrandPersonality,
- HeroImageryDirection,
-)
-
-
-class PageBlueprintInline(admin.TabularInline):
- """Inline admin for Page Blueprints within Site Blueprint"""
- model = PageBlueprint
- extra = 0
- fields = ['slug', 'title', 'type', 'status', 'order']
- readonly_fields = ['slug', 'title', 'type', 'status', 'order']
- can_delete = False
- show_change_link = True
-
-
-@admin.register(SiteBlueprint)
-class SiteBlueprintAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
- """Admin interface for Site Blueprints"""
- list_display = [
- 'name',
- 'get_site_display',
- 'get_sector_display',
- 'status',
- 'hosting_type',
- 'version',
- 'get_pages_count',
- 'created_at',
- ]
- list_filter = ['status', 'hosting_type', 'site', 'sector', 'account', 'created_at']
- search_fields = ['name', 'description', 'site__name', 'sector__name']
- readonly_fields = ['created_at', 'updated_at', 'version', 'deployed_version']
- ordering = ['-created_at']
- inlines = [PageBlueprintInline]
-
- fieldsets = (
- ('Basic Information', {
- 'fields': ('name', 'description', 'account', 'site', 'sector')
- }),
- ('Status & Configuration', {
- 'fields': ('status', 'hosting_type', 'version', 'deployed_version')
- }),
- ('Configuration Data', {
- 'fields': ('config_json',),
- 'classes': ('collapse',),
- }),
- ('Structure Data', {
- 'fields': ('structure_json',),
- 'classes': ('collapse',),
- }),
- ('Timestamps', {
- 'fields': ('created_at', 'updated_at'),
- 'classes': ('collapse',),
- }),
- )
-
- def get_site_display(self, obj):
- """Safely get site name"""
- try:
- if obj.site:
- url = reverse('admin:igny8_core_auth_site_change', args=[obj.site.id])
- return format_html('{}', url, obj.site.name)
- return '-'
- except:
- return '-'
- get_site_display.short_description = 'Site'
-
- def get_sector_display(self, obj):
- """Safely get sector name"""
- try:
- return obj.sector.name if obj.sector else '-'
- except:
- return '-'
- get_sector_display.short_description = 'Sector'
-
- def get_pages_count(self, obj):
- """Get count of pages for this blueprint"""
- try:
- count = obj.pages.count()
- if count > 0:
- url = reverse('admin:site_building_pageblueprint_changelist')
- return format_html('{} pages', url, obj.id, count)
- return '0 pages'
- except:
- return '0 pages'
- get_pages_count.short_description = 'Pages'
-
-
-@admin.register(PageBlueprint)
-class PageBlueprintAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
- """Admin interface for Page Blueprints"""
- list_display = [
- 'title',
- 'slug',
- 'get_site_blueprint_display',
- 'type',
- 'status',
- 'order',
- 'get_site_display',
- 'get_sector_display',
- 'created_at',
- ]
- list_filter = ['status', 'type', 'site_blueprint', 'site', 'sector', 'created_at']
- search_fields = ['title', 'slug', 'site_blueprint__name', 'site__name']
- readonly_fields = ['created_at', 'updated_at']
- ordering = ['site_blueprint', 'order', 'created_at']
-
- fieldsets = (
- ('Basic Information', {
- 'fields': ('site_blueprint', 'title', 'slug', 'type', 'status', 'order')
- }),
- ('Account & Site', {
- 'fields': ('account', 'site', 'sector'),
- 'classes': ('collapse',),
- }),
- ('Content Blocks', {
- 'fields': ('blocks_json',),
- 'classes': ('collapse',),
- }),
- ('Timestamps', {
- 'fields': ('created_at', 'updated_at'),
- 'classes': ('collapse',),
- }),
- )
-
- def get_site_blueprint_display(self, obj):
- """Safely get site blueprint name with link"""
- try:
- if obj.site_blueprint:
- url = reverse('admin:site_building_siteblueprint_change', args=[obj.site_blueprint.id])
- return format_html('{}', url, obj.site_blueprint.name)
- return '-'
- except:
- return '-'
- get_site_blueprint_display.short_description = 'Site Blueprint'
-
- def get_site_display(self, obj):
- """Safely get site name"""
- try:
- if obj.site:
- url = reverse('admin:igny8_core_auth_site_change', args=[obj.site.id])
- return format_html('{}', url, obj.site.name)
- return '-'
- except:
- return '-'
- get_site_display.short_description = 'Site'
-
- def get_sector_display(self, obj):
- """Safely get sector name"""
- try:
- return obj.sector.name if obj.sector else '-'
- except:
- return '-'
- get_sector_display.short_description = 'Sector'
-
-
-@admin.register(BusinessType)
-class BusinessTypeAdmin(admin.ModelAdmin):
- """Admin interface for Business Types"""
- list_display = ['name', 'description', 'is_active', 'order', 'created_at']
- list_filter = ['is_active', 'created_at']
- search_fields = ['name', 'description']
- list_editable = ['is_active', 'order']
- ordering = ['order', 'name']
-
- fieldsets = (
- ('Basic Information', {
- 'fields': ('name', 'description', 'is_active', 'order')
- }),
- ('Timestamps', {
- 'fields': ('created_at', 'updated_at'),
- 'classes': ('collapse',),
- }),
- )
- readonly_fields = ['created_at', 'updated_at']
-
-
-@admin.register(AudienceProfile)
-class AudienceProfileAdmin(admin.ModelAdmin):
- """Admin interface for Audience Profiles"""
- list_display = ['name', 'description', 'is_active', 'order', 'created_at']
- list_filter = ['is_active', 'created_at']
- search_fields = ['name', 'description']
- list_editable = ['is_active', 'order']
- ordering = ['order', 'name']
-
- fieldsets = (
- ('Basic Information', {
- 'fields': ('name', 'description', 'is_active', 'order')
- }),
- ('Timestamps', {
- 'fields': ('created_at', 'updated_at'),
- 'classes': ('collapse',),
- }),
- )
- readonly_fields = ['created_at', 'updated_at']
-
-
-@admin.register(BrandPersonality)
-class BrandPersonalityAdmin(admin.ModelAdmin):
- """Admin interface for Brand Personalities"""
- list_display = ['name', 'description', 'is_active', 'order', 'created_at']
- list_filter = ['is_active', 'created_at']
- search_fields = ['name', 'description']
- list_editable = ['is_active', 'order']
- ordering = ['order', 'name']
-
- fieldsets = (
- ('Basic Information', {
- 'fields': ('name', 'description', 'is_active', 'order')
- }),
- ('Timestamps', {
- 'fields': ('created_at', 'updated_at'),
- 'classes': ('collapse',),
- }),
- )
- readonly_fields = ['created_at', 'updated_at']
-
-
-@admin.register(HeroImageryDirection)
-class HeroImageryDirectionAdmin(admin.ModelAdmin):
- """Admin interface for Hero Imagery Directions"""
- list_display = ['name', 'description', 'is_active', 'order', 'created_at']
- list_filter = ['is_active', 'created_at']
- search_fields = ['name', 'description']
- list_editable = ['is_active', 'order']
- ordering = ['order', 'name']
-
- fieldsets = (
- ('Basic Information', {
- 'fields': ('name', 'description', 'is_active', 'order')
- }),
- ('Timestamps', {
- 'fields': ('created_at', 'updated_at'),
- 'classes': ('collapse',),
- }),
- )
- readonly_fields = ['created_at', 'updated_at']
+# All SiteBuilder admin classes removed:
+# - SiteBlueprintAdmin
+# - PageBlueprintAdmin
+# - BusinessTypeAdmin, AudienceProfileAdmin, BrandPersonalityAdmin, HeroImageryDirectionAdmin
+#
+# Site Builder functionality has been deprecated and removed from the system.
diff --git a/backend/igny8_core/business/site_building/migrations/0002_remove_blueprint_models.py b/backend/igny8_core/business/site_building/migrations/0002_remove_blueprint_models.py
new file mode 100644
index 00000000..6ae8c68a
--- /dev/null
+++ b/backend/igny8_core/business/site_building/migrations/0002_remove_blueprint_models.py
@@ -0,0 +1,53 @@
+# Generated manually on 2025-12-01
+# Remove SiteBlueprint, PageBlueprint, SiteBlueprintCluster, and SiteBlueprintTaxonomy models
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('site_building', '0001_initial'), # Changed from 0002_initial
+ ]
+
+ operations = [
+ # Drop tables in reverse dependency order
+ migrations.RunSQL(
+ sql=[
+ # Drop foreign key constraints first
+ "ALTER TABLE igny8_publishing_records DROP CONSTRAINT IF EXISTS igny8_publishing_recor_site_blueprint_id_9f4e8c7a_fk_igny8_sit CASCADE;",
+ "ALTER TABLE igny8_deployment_records DROP CONSTRAINT IF EXISTS igny8_deployment_recor_site_blueprint_id_3a2b7c1d_fk_igny8_sit CASCADE;",
+
+ # Drop the tables
+ "DROP TABLE IF EXISTS igny8_site_blueprint_taxonomies CASCADE;",
+ "DROP TABLE IF EXISTS igny8_site_blueprint_clusters CASCADE;",
+ "DROP TABLE IF EXISTS igny8_page_blueprints CASCADE;",
+ "DROP TABLE IF EXISTS igny8_site_blueprints CASCADE;",
+ "DROP TABLE IF EXISTS igny8_site_builder_business_types CASCADE;",
+ "DROP TABLE IF EXISTS igny8_site_builder_audience_profiles CASCADE;",
+ "DROP TABLE IF EXISTS igny8_site_builder_brand_personalities CASCADE;",
+ "DROP TABLE IF EXISTS igny8_site_builder_hero_imagery CASCADE;",
+ ],
+ reverse_sql=[
+ # Reverse migration not supported - this is a destructive operation
+ "SELECT 1;"
+ ],
+ ),
+
+ # Also drop the site_blueprint_id column from PublishingRecord
+ migrations.RunSQL(
+ sql=[
+ "ALTER TABLE igny8_publishing_records DROP COLUMN IF EXISTS site_blueprint_id CASCADE;",
+ "DROP INDEX IF EXISTS igny8_publishing_recor_site_blueprint_id_des_b7c4e5f8_idx;",
+ ],
+ reverse_sql=["SELECT 1;"],
+ ),
+
+ # Drop the site_blueprint_id column from DeploymentRecord
+ migrations.RunSQL(
+ sql=[
+ "ALTER TABLE igny8_deployment_records DROP COLUMN IF EXISTS site_blueprint_id CASCADE;",
+ ],
+ reverse_sql=["SELECT 1;"],
+ ),
+ ]
diff --git a/backend/igny8_core/business/site_building/models.py b/backend/igny8_core/business/site_building/models.py
index 580aeaa4..5062ed69 100644
--- a/backend/igny8_core/business/site_building/models.py
+++ b/backend/igny8_core/business/site_building/models.py
@@ -1,346 +1,15 @@
"""
-Site Builder Models
-Phase 3: Site Builder
+Site Building Models
+Legacy SiteBuilder module has been removed.
+This file is kept for backwards compatibility with migrations.
"""
from django.db import models
-from django.core.validators import MinValueValidator
-from igny8_core.auth.models import SiteSectorBaseModel
-
-
-class SiteBlueprint(SiteSectorBaseModel):
- """
- Site Blueprint model for storing AI-generated site structures.
- """
-
- STATUS_CHOICES = [
- ('draft', 'Draft'),
- ('generating', 'Generating'),
- ('ready', 'Ready'),
- ('deployed', 'Deployed'),
- ]
-
- HOSTING_TYPE_CHOICES = [
- ('igny8_sites', 'IGNY8 Sites'),
- ('wordpress', 'WordPress'),
- ('shopify', 'Shopify'),
- ('multi', 'Multiple Destinations'),
- ]
-
- name = models.CharField(max_length=255, help_text="Site name")
- description = models.TextField(blank=True, null=True, help_text="Site description")
-
- # Site configuration (from wizard)
- config_json = models.JSONField(
- default=dict,
- help_text="Wizard configuration: business_type, style, objectives, etc."
- )
-
- # Generated structure (from AI)
- structure_json = models.JSONField(
- default=dict,
- help_text="AI-generated structure: pages, layout, theme, etc."
- )
-
- # Status tracking
- status = models.CharField(
- max_length=20,
- choices=STATUS_CHOICES,
- default='draft',
- db_index=True,
- help_text="Blueprint status"
- )
-
- # Hosting configuration
- hosting_type = models.CharField(
- max_length=50,
- choices=HOSTING_TYPE_CHOICES,
- default='igny8_sites',
- help_text="Target hosting platform"
- )
-
- # Version tracking
- version = models.IntegerField(default=1, validators=[MinValueValidator(1)], help_text="Blueprint version")
- deployed_version = models.IntegerField(null=True, blank=True, help_text="Currently deployed version")
-
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
-
- class Meta:
- app_label = 'site_building'
- db_table = 'igny8_site_blueprints'
- ordering = ['-created_at']
- verbose_name = 'Site Blueprint'
- verbose_name_plural = 'Site Blueprints'
- indexes = [
- models.Index(fields=['status']),
- models.Index(fields=['hosting_type']),
- models.Index(fields=['site', 'sector']),
- models.Index(fields=['account', 'status']),
- ]
-
- def __str__(self):
- return f"{self.name} ({self.get_status_display()})"
-
-
-class PageBlueprint(SiteSectorBaseModel):
- """
- Page Blueprint model for storing individual page definitions.
- """
-
- PAGE_TYPE_CHOICES = [
- ('home', 'Home'),
- ('about', 'About'),
- ('services', 'Services'),
- ('products', 'Products'),
- ('blog', 'Blog'),
- ('contact', 'Contact'),
- ('custom', 'Custom'),
- ]
-
- STATUS_CHOICES = [
- ('draft', 'Draft'),
- ('generating', 'Generating'),
- ('ready', 'Ready'),
- ('published', 'Published'),
- ]
-
- site_blueprint = models.ForeignKey(
- SiteBlueprint,
- on_delete=models.CASCADE,
- related_name='pages',
- help_text="The site blueprint this page belongs to"
- )
- slug = models.SlugField(max_length=255, help_text="Page URL slug")
- title = models.CharField(max_length=255, help_text="Page title")
-
- # Page type
- type = models.CharField(
- max_length=50,
- choices=PAGE_TYPE_CHOICES,
- default='custom',
- help_text="Page type"
- )
-
- # Page content (blocks)
- blocks_json = models.JSONField(
- default=list,
- help_text="Page content blocks: [{'type': 'hero', 'data': {...}}, ...]"
- )
-
- # Status
- status = models.CharField(
- max_length=20,
- choices=STATUS_CHOICES,
- default='draft',
- db_index=True,
- help_text="Page status"
- )
-
- # Order
- order = models.IntegerField(default=0, help_text="Page order in navigation")
-
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
-
- class Meta:
- app_label = 'site_building'
- db_table = 'igny8_page_blueprints'
- ordering = ['order', 'created_at']
- verbose_name = 'Page Blueprint'
- verbose_name_plural = 'Page Blueprints'
- unique_together = [['site_blueprint', 'slug']]
- indexes = [
- models.Index(fields=['site_blueprint', 'status']),
- models.Index(fields=['type']),
- models.Index(fields=['site_blueprint', 'order']),
- ]
-
- def save(self, *args, **kwargs):
- """Automatically set account, site, and sector from site_blueprint"""
- if self.site_blueprint:
- self.account = self.site_blueprint.account
- self.site = self.site_blueprint.site
- self.sector = self.site_blueprint.sector
- super().save(*args, **kwargs)
-
- def __str__(self):
- return f"{self.title} ({self.site_blueprint.name})"
-
-
-class SiteBlueprintCluster(SiteSectorBaseModel):
- """
- Mapping table that connects planner clusters to site blueprints
- with role/coverage metadata.
- """
-
- ROLE_CHOICES = [
- ('hub', 'Hub Page'),
- ('supporting', 'Supporting Page'),
- ('attribute', 'Attribute Page'),
- ]
-
- COVERAGE_CHOICES = [
- ('pending', 'Pending'),
- ('in_progress', 'In Progress'),
- ('complete', 'Complete'),
- ]
-
- site_blueprint = models.ForeignKey(
- SiteBlueprint,
- on_delete=models.CASCADE,
- related_name='cluster_links',
- help_text="Site blueprint that is planning coverage for the cluster",
- )
- cluster = models.ForeignKey(
- 'planner.Clusters',
- on_delete=models.CASCADE,
- related_name='blueprint_links',
- help_text="Planner cluster being mapped into the site blueprint",
- )
- role = models.CharField(max_length=50, choices=ROLE_CHOICES, default='hub')
- coverage_status = models.CharField(max_length=50, choices=COVERAGE_CHOICES, default='pending')
- metadata = models.JSONField(
- default=dict,
- blank=True,
- help_text="Additional coverage metadata (target pages, keyword counts, ai hints)",
- )
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
-
- class Meta:
- app_label = 'site_building'
- db_table = 'igny8_site_blueprint_clusters'
- unique_together = [['site_blueprint', 'cluster', 'role']]
- verbose_name = 'Site Blueprint Cluster'
- verbose_name_plural = 'Site Blueprint Clusters'
- indexes = [
- models.Index(fields=['site_blueprint', 'cluster']),
- models.Index(fields=['site_blueprint', 'coverage_status']),
- models.Index(fields=['cluster', 'role']),
- ]
-
- def save(self, *args, **kwargs):
- if self.site_blueprint:
- self.account = self.site_blueprint.account
- self.site = self.site_blueprint.site
- self.sector = self.site_blueprint.sector
- super().save(*args, **kwargs)
-
- def __str__(self):
- return f"{self.site_blueprint.name} β {self.cluster.name}"
-
-
-class SiteBlueprintTaxonomy(SiteSectorBaseModel):
- """
- Taxonomy blueprint entity for categories/tags/product attributes tied to clusters.
- """
-
- TAXONOMY_TYPE_CHOICES = [
- ('blog_category', 'Blog Category'),
- ('blog_tag', 'Blog Tag'),
- ('product_category', 'Product Category'),
- ('product_tag', 'Product Tag'),
- ('product_attribute', 'Product Attribute'),
- ('service_category', 'Service Category'),
- ]
-
- site_blueprint = models.ForeignKey(
- SiteBlueprint,
- on_delete=models.CASCADE,
- related_name='taxonomies',
- help_text="Site blueprint this taxonomy belongs to",
- )
- name = models.CharField(max_length=255, help_text="Display name")
- slug = models.SlugField(max_length=255, help_text="Slug/identifier within the site blueprint")
- taxonomy_type = models.CharField(max_length=50, choices=TAXONOMY_TYPE_CHOICES, default='blog_category')
- description = models.TextField(blank=True, null=True)
- metadata = models.JSONField(default=dict, blank=True, help_text="Additional taxonomy metadata or AI hints")
- clusters = models.ManyToManyField(
- 'planner.Clusters',
- blank=True,
- related_name='blueprint_taxonomies',
- help_text="Planner clusters that this taxonomy maps to",
- )
- external_reference = models.CharField(
- max_length=255,
- blank=True,
- null=True,
- help_text="External system ID (WordPress/WooCommerce/etc.)",
- )
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
-
- class Meta:
- app_label = 'site_building'
- db_table = 'igny8_site_blueprint_taxonomies'
- unique_together = [['site_blueprint', 'slug']]
- verbose_name = 'Site Blueprint Taxonomy'
- verbose_name_plural = 'Site Blueprint Taxonomies'
- indexes = [
- models.Index(fields=['site_blueprint', 'taxonomy_type']),
- models.Index(fields=['taxonomy_type']),
- ]
-
- def save(self, *args, **kwargs):
- if self.site_blueprint:
- self.account = self.site_blueprint.account
- self.site = self.site_blueprint.site
- self.sector = self.site_blueprint.sector
- super().save(*args, **kwargs)
-
- def __str__(self):
- return f"{self.name} ({self.get_taxonomy_type_display()})"
-
-
-class SiteBuilderOption(models.Model):
- """
- Base model for Site Builder dropdown metadata.
- """
-
- name = models.CharField(max_length=120, unique=True)
- description = models.CharField(max_length=255, blank=True)
- is_active = models.BooleanField(default=True)
- order = models.PositiveIntegerField(default=0)
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
-
- class Meta:
- abstract = True
- ordering = ['order', 'name']
-
- def __str__(self):
- return self.name
-
-
-class BusinessType(SiteBuilderOption):
- class Meta(SiteBuilderOption.Meta):
- app_label = 'site_building'
- db_table = 'igny8_site_builder_business_types'
- verbose_name = 'Business Type'
- verbose_name_plural = 'Business Types'
-
-
-class AudienceProfile(SiteBuilderOption):
- class Meta(SiteBuilderOption.Meta):
- app_label = 'site_building'
- db_table = 'igny8_site_builder_audience_profiles'
- verbose_name = 'Audience Profile'
- verbose_name_plural = 'Audience Profiles'
-
-
-class BrandPersonality(SiteBuilderOption):
- class Meta(SiteBuilderOption.Meta):
- app_label = 'site_building'
- db_table = 'igny8_site_builder_brand_personalities'
- verbose_name = 'Brand Personality'
- verbose_name_plural = 'Brand Personalities'
-
-
-class HeroImageryDirection(SiteBuilderOption):
- class Meta(SiteBuilderOption.Meta):
- app_label = 'site_building'
- db_table = 'igny8_site_builder_hero_imagery'
- verbose_name = 'Hero Imagery Direction'
- verbose_name_plural = 'Hero Imagery Directions'
-
+# All SiteBuilder models have been removed:
+# - SiteBlueprint
+# - PageBlueprint
+# - SiteBlueprintCluster
+# - SiteBlueprintTaxonomy
+# - BusinessType, AudienceProfile, BrandPersonality, HeroImageryDirection
+#
+# Taxonomy functionality moved to ContentTaxonomy model in business/content/models.py
diff --git a/backend/igny8_core/modules/planner/migrations/0002_initial.py b/backend/igny8_core/modules/planner/migrations/0002_initial.py
index f349d8df..ffb12382 100644
--- a/backend/igny8_core/modules/planner/migrations/0002_initial.py
+++ b/backend/igny8_core/modules/planner/migrations/0002_initial.py
@@ -11,15 +11,11 @@ class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0001_initial'),
('planner', '0001_initial'),
- ('site_building', '0001_initial'),
+ # ('site_building', '0001_initial'), # REMOVED: SiteBuilder deprecated
]
operations = [
- migrations.AddField(
- model_name='contentideas',
- name='taxonomy',
- field=models.ForeignKey(blank=True, help_text='Optional taxonomy association when derived from blueprint planning', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='content_ideas', to='site_building.siteblueprinttaxonomy'),
- ),
+ # REMOVED: ContentIdeas.taxonomy FK to SiteBlueprintTaxonomy (legacy blueprint)
migrations.AddField(
model_name='keywords',
name='account',
diff --git a/backend/igny8_core/modules/planner/migrations/0005_field_rename_implementation.py b/backend/igny8_core/modules/planner/migrations/0005_field_rename_implementation.py
index c9b72d79..3979ed92 100644
--- a/backend/igny8_core/modules/planner/migrations/0005_field_rename_implementation.py
+++ b/backend/igny8_core/modules/planner/migrations/0005_field_rename_implementation.py
@@ -8,7 +8,7 @@ class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0002_add_wp_api_key_to_site'),
('planner', '0004_remove_clusters_igny8_clust_context_0d6bd7_idx_and_more'),
- ('site_building', '0001_initial'),
+ # ('site_building', '0001_initial'), # REMOVED: SiteBuilder deprecated
]
operations = [
diff --git a/backend/igny8_core/modules/planner/serializers.py b/backend/igny8_core/modules/planner/serializers.py
index 213a595a..99e5f478 100644
--- a/backend/igny8_core/modules/planner/serializers.py
+++ b/backend/igny8_core/modules/planner/serializers.py
@@ -4,7 +4,7 @@ from django.conf import settings
from .models import Keywords, Clusters, ContentIdeas
from igny8_core.auth.models import SeedKeyword
from igny8_core.auth.serializers import SeedKeywordSerializer
-from igny8_core.business.site_building.models import SiteBlueprintTaxonomy
+# Removed: from igny8_core.business.site_building.models import SiteBlueprintTaxonomy
class KeywordSerializer(serializers.ModelSerializer):
@@ -209,10 +209,13 @@ class ContentIdeasSerializer(serializers.ModelSerializer):
return None
def get_taxonomy_name(self, obj):
+ """Legacy: SiteBlueprintTaxonomy removed - taxonomy now in ContentTaxonomy"""
if obj.taxonomy_id:
try:
- taxonomy = SiteBlueprintTaxonomy.objects.get(id=obj.taxonomy_id)
+ from igny8_core.business.content.models import ContentTaxonomy
+ taxonomy = ContentTaxonomy.objects.get(id=obj.taxonomy_id)
return taxonomy.name
- except SiteBlueprintTaxonomy.DoesNotExist:
+ except ContentTaxonomy.DoesNotExist:
return None
return None
+
diff --git a/backend/igny8_core/modules/publisher/views.py b/backend/igny8_core/modules/publisher/views.py
index 48438d38..f2bb3f56 100644
--- a/backend/igny8_core/modules/publisher/views.py
+++ b/backend/igny8_core/modules/publisher/views.py
@@ -18,27 +18,25 @@ from igny8_core.api.response import success_response, error_response
from igny8_core.api.throttles import DebugScopedRateThrottle
from igny8_core.business.publishing.models import PublishingRecord, DeploymentRecord
from igny8_core.business.publishing.services.publisher_service import PublisherService
-from igny8_core.business.publishing.services.deployment_readiness_service import DeploymentReadinessService
-from igny8_core.business.site_building.models import SiteBlueprint
class PublishingRecordViewSet(SiteSectorModelViewSet):
"""
ViewSet for PublishingRecord model.
"""
- queryset = PublishingRecord.objects.select_related('content', 'site_blueprint')
+ queryset = PublishingRecord.objects.select_related('content')
permission_classes = [IsAuthenticatedAndActive, IsEditorOrAbove]
throttle_scope = 'publisher'
throttle_classes = [DebugScopedRateThrottle]
def get_serializer_class(self):
- # Will be created in next step
+ # Dynamically create serializer
from rest_framework import serializers
class PublishingRecordSerializer(serializers.ModelSerializer):
class Meta:
model = PublishingRecord
- fields = '__all__'
+ exclude = [] # Legacy: site_blueprint field removed from model
return PublishingRecordSerializer
@@ -46,27 +44,29 @@ class PublishingRecordViewSet(SiteSectorModelViewSet):
class DeploymentRecordViewSet(SiteSectorModelViewSet):
"""
ViewSet for DeploymentRecord model.
+ Legacy: SiteBlueprint functionality removed.
"""
- queryset = DeploymentRecord.objects.select_related('site_blueprint')
+ queryset = DeploymentRecord.objects.all()
permission_classes = [IsAuthenticatedAndActive, IsEditorOrAbove]
throttle_scope = 'publisher'
throttle_classes = [DebugScopedRateThrottle]
def get_serializer_class(self):
- # Will be created in next step
+ # Dynamically create serializer
from rest_framework import serializers
class DeploymentRecordSerializer(serializers.ModelSerializer):
class Meta:
model = DeploymentRecord
- fields = '__all__'
+ exclude = [] # Legacy: site_blueprint field removed from model
return DeploymentRecordSerializer
class PublisherViewSet(viewsets.ViewSet):
"""
- Publisher actions for publishing content and sites.
+ Publisher actions for publishing content.
+ Legacy SiteBlueprint publishing removed - only content publishing supported.
"""
permission_classes = [IsAuthenticatedAndActive, IsEditorOrAbove]
throttle_scope = 'publisher'
@@ -75,29 +75,33 @@ class PublisherViewSet(viewsets.ViewSet):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.publisher_service = PublisherService()
- self.readiness_service = DeploymentReadinessService()
@action(detail=False, methods=['post'], url_path='publish')
def publish(self, request):
"""
- Publish content or site to destinations.
+ Publish content to destinations.
Request body:
{
- "content_id": 123, # Optional: content to publish
- "site_blueprint_id": 456, # Optional: site to publish
- "destinations": ["wordpress", "sites"] # Required: list of destinations
+ "content_id": 123, # Required: content to publish
+ "destinations": ["wordpress"] # Required: list of destinations
}
"""
import logging
logger = logging.getLogger(__name__)
content_id = request.data.get('content_id')
- site_blueprint_id = request.data.get('site_blueprint_id')
destinations = request.data.get('destinations', [])
logger.info(f"[PublisherViewSet.publish] π Publish request received: content_id={content_id}, destinations={destinations}")
+ if not content_id:
+ return error_response(
+ 'content_id is required',
+ status.HTTP_400_BAD_REQUEST,
+ request
+ )
+
if not destinations:
return error_response(
'destinations is required',
@@ -107,136 +111,26 @@ class PublisherViewSet(viewsets.ViewSet):
account = request.account
- if site_blueprint_id:
- # Publish site
- try:
- blueprint = SiteBlueprint.objects.get(id=site_blueprint_id, account=account)
- except SiteBlueprint.DoesNotExist:
- return error_response(
- f'Site blueprint {site_blueprint_id} not found',
- status.HTTP_404_NOT_FOUND,
- request
- )
-
- if 'sites' in destinations:
- result = self.publisher_service.publish_to_sites(blueprint)
- return success_response(result, request=request)
- else:
- return error_response(
- 'Site publishing only supports "sites" destination',
- status.HTTP_400_BAD_REQUEST,
- request
- )
-
- elif content_id:
- # Publish content
- logger.info(f"[PublisherViewSet.publish] π Publishing content {content_id} to {destinations}")
- result = self.publisher_service.publish_content(
- content_id,
- destinations,
- account
- )
- logger.info(f"[PublisherViewSet.publish] {'β
' if result.get('success') else 'β'} Publish result: {result}")
- return success_response(result, request=request)
-
- else:
- return error_response(
- 'Either content_id or site_blueprint_id is required',
- status.HTTP_400_BAD_REQUEST,
- request
- )
-
- @action(detail=False, methods=['get'], url_path='blueprints/(?P[^/.]+)/readiness')
- def deployment_readiness(self, request, blueprint_id):
- """
- Check deployment readiness for a site blueprint.
- Stage 4: Pre-deployment validation checks.
-
- GET /api/v1/publisher/blueprints/{blueprint_id}/readiness/
- """
- account = request.account
-
- try:
- blueprint = SiteBlueprint.objects.get(id=blueprint_id, account=account)
- except SiteBlueprint.DoesNotExist:
- return error_response(
- f'Site blueprint {blueprint_id} not found',
- status.HTTP_404_NOT_FOUND,
- request
- )
-
- readiness = self.readiness_service.check_readiness(blueprint_id)
-
- return success_response(readiness, request=request)
-
- @action(detail=False, methods=['post'], url_path='deploy/(?P[^/.]+)')
- def deploy(self, request, blueprint_id):
- """
- Deploy site blueprint to Sites renderer.
- Stage 4: Enhanced with readiness check (optional).
-
- POST /api/v1/publisher/deploy/{blueprint_id}/
-
- Request body (optional):
- {
- "skip_readiness_check": false # Set to true to skip readiness validation
- }
- """
- account = request.account
-
- try:
- blueprint = SiteBlueprint.objects.get(id=blueprint_id, account=account)
- except SiteBlueprint.DoesNotExist:
- return error_response(
- f'Site blueprint {blueprint_id} not found',
- status.HTTP_404_NOT_FOUND,
- request
- )
-
- # Stage 4: Optional readiness check
- skip_check = request.data.get('skip_readiness_check', False)
- if not skip_check:
- readiness = self.readiness_service.check_readiness(blueprint_id)
- if not readiness.get('ready'):
- return error_response(
- {
- 'message': 'Site is not ready for deployment',
- 'readiness': readiness
- },
- status.HTTP_400_BAD_REQUEST,
- request
- )
-
- result = self.publisher_service.publish_to_sites(blueprint)
-
- response_status = status.HTTP_202_ACCEPTED if result.get('success') else status.HTTP_400_BAD_REQUEST
- return success_response(result, request=request, status_code=response_status)
+ # Publish content
+ logger.info(f"[PublisherViewSet.publish] π Publishing content {content_id} to {destinations}")
+ result = self.publisher_service.publish_content(
+ content_id,
+ destinations,
+ account
+ )
+ logger.info(f"[PublisherViewSet.publish] {'β
' if result.get('success') else 'β'} Publish result: {result}")
+ return success_response(result, request=request)
@action(detail=False, methods=['get'], url_path='status/(?P[^/.]+)')
def get_status(self, request, id):
"""
- Get publishing/deployment status.
+ Get publishing status.
GET /api/v1/publisher/status/{id}/
"""
account = request.account
- # Try deployment record first
- try:
- deployment = DeploymentRecord.objects.get(id=id, account=account)
- return success_response({
- 'type': 'deployment',
- 'status': deployment.status,
- 'version': deployment.version,
- 'deployed_version': deployment.deployed_version,
- 'deployment_url': deployment.deployment_url,
- 'deployed_at': deployment.deployed_at,
- 'error_message': deployment.error_message,
- }, request=request)
- except DeploymentRecord.DoesNotExist:
- pass
-
- # Try publishing record
+ # Get publishing record
try:
publishing = PublishingRecord.objects.get(id=id, account=account)
return success_response({
diff --git a/backend/igny8_core/modules/site_builder/__init__.py b/backend/igny8_core/modules/site_builder.backup/__init__.py
similarity index 100%
rename from backend/igny8_core/modules/site_builder/__init__.py
rename to backend/igny8_core/modules/site_builder.backup/__init__.py
diff --git a/backend/igny8_core/modules/site_builder/apps.py b/backend/igny8_core/modules/site_builder.backup/apps.py
similarity index 100%
rename from backend/igny8_core/modules/site_builder/apps.py
rename to backend/igny8_core/modules/site_builder.backup/apps.py
diff --git a/backend/igny8_core/modules/site_builder/serializers.py b/backend/igny8_core/modules/site_builder.backup/serializers.py
similarity index 100%
rename from backend/igny8_core/modules/site_builder/serializers.py
rename to backend/igny8_core/modules/site_builder.backup/serializers.py
diff --git a/backend/igny8_core/modules/site_builder/urls.py b/backend/igny8_core/modules/site_builder.backup/urls.py
similarity index 100%
rename from backend/igny8_core/modules/site_builder/urls.py
rename to backend/igny8_core/modules/site_builder.backup/urls.py
diff --git a/backend/igny8_core/modules/site_builder/views.py b/backend/igny8_core/modules/site_builder.backup/views.py
similarity index 100%
rename from backend/igny8_core/modules/site_builder/views.py
rename to backend/igny8_core/modules/site_builder.backup/views.py
diff --git a/backend/igny8_core/modules/writer/migrations/0001_initial.py b/backend/igny8_core/modules/writer/migrations/0001_initial.py
index 27d7d808..415fb42a 100644
--- a/backend/igny8_core/modules/writer/migrations/0001_initial.py
+++ b/backend/igny8_core/modules/writer/migrations/0001_initial.py
@@ -12,7 +12,7 @@ class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0001_initial'),
('planner', '0001_initial'),
- ('site_building', '0001_initial'),
+ # ('site_building', '0001_initial'), # REMOVED: SiteBuilder deprecated
]
operations = [
diff --git a/backend/igny8_core/settings.py b/backend/igny8_core/settings.py
index 8e6b63df..87dfa141 100644
--- a/backend/igny8_core/settings.py
+++ b/backend/igny8_core/settings.py
@@ -53,11 +53,11 @@ INSTALLED_APPS = [
'igny8_core.modules.system.apps.SystemConfig',
'igny8_core.modules.billing.apps.BillingConfig',
'igny8_core.modules.automation.apps.AutomationConfig',
- 'igny8_core.business.site_building.apps.SiteBuildingConfig',
+ # 'igny8_core.business.site_building.apps.SiteBuildingConfig', # REMOVED: SiteBuilder/Blueprint deprecated
'igny8_core.business.optimization.apps.OptimizationConfig',
'igny8_core.business.publishing.apps.PublishingConfig',
'igny8_core.business.integration.apps.IntegrationConfig',
- 'igny8_core.modules.site_builder.apps.SiteBuilderConfig',
+ # 'igny8_core.modules.site_builder.apps.SiteBuilderConfig', # REMOVED: SiteBuilder deprecated
'igny8_core.modules.linker.apps.LinkerConfig',
'igny8_core.modules.optimizer.apps.OptimizerConfig',
'igny8_core.modules.publisher.apps.PublisherConfig',
diff --git a/backend/igny8_core/tasks/wordpress_publishing.py b/backend/igny8_core/tasks/wordpress_publishing.py
index eb0db7b3..94abb0ee 100644
--- a/backend/igny8_core/tasks/wordpress_publishing.py
+++ b/backend/igny8_core/tasks/wordpress_publishing.py
@@ -37,7 +37,7 @@ def publish_content_to_wordpress(self, content_id: int, site_integration_id: int
start_time = time.time()
try:
- from igny8_core.business.content.models import Content, ContentTaxonomyMap
+ from igny8_core.business.content.models import Content
from igny8_core.business.integration.models import SiteIntegration, SyncEvent
from igny8_core.modules.writer.models import Images
from django.utils.html import strip_tags
@@ -100,47 +100,75 @@ def publish_content_to_wordpress(self, content_id: int, site_integration_id: int
else:
publish_logger.warning(f" {log_prefix} β οΈ No content_html found - excerpt will be empty")
- # STEP 4: Get taxonomy terms (categories)
- publish_logger.info(f"{log_prefix} STEP 4: Loading taxonomy mappings for categories...")
- taxonomy_maps = ContentTaxonomyMap.objects.filter(content=content).select_related('taxonomy')
- publish_logger.info(f" {log_prefix} Found {taxonomy_maps.count()} taxonomy mappings")
+ # STEP 4: Get taxonomy terms (categories and tags)
+ publish_logger.info(f"{log_prefix} STEP 4: Loading taxonomy terms from Content.taxonomy_terms...")
- categories = []
- for mapping in taxonomy_maps:
- if mapping.taxonomy:
- categories.append(mapping.taxonomy.name)
- publish_logger.info(f" {log_prefix} π Category: '{mapping.taxonomy.name}'")
+ # Get categories from ContentTaxonomy many-to-many relationship
+ # This is the CORRECT way - matching ContentSerializer.get_categories()
+ categories = [
+ term.name
+ for term in content.taxonomy_terms.filter(taxonomy_type='category')
+ ]
+
+ publish_logger.info(f" {log_prefix} Found {len(categories)} categories from taxonomy_terms")
+ for cat in categories:
+ publish_logger.info(f" {log_prefix} π Category: '{cat}'")
+
+ # FALLBACK: If no categories from taxonomy_terms, use Cluster as category (matches UI display)
+ if not categories and content.cluster:
+ categories.append(content.cluster.name)
+ publish_logger.info(f" {log_prefix} π Category (fallback from cluster): '{content.cluster.name}'")
if not categories:
publish_logger.warning(f" {log_prefix} β οΈ No categories found for content")
else:
publish_logger.info(f" {log_prefix} β
TOTAL categories: {len(categories)}")
- # STEP 5: Get keywords as tags
- publish_logger.info(f"{log_prefix} STEP 5: Extracting keywords as tags...")
- tags = []
+ # STEP 5: Get tags from taxonomy_terms AND keywords
+ publish_logger.info(f"{log_prefix} STEP 5: Loading tags from taxonomy_terms and keywords...")
+ # First, get tags from ContentTaxonomy many-to-many relationship
+ # This matches ContentSerializer.get_tags()
+ tags = [
+ term.name
+ for term in content.taxonomy_terms.filter(taxonomy_type='tag')
+ ]
+
+ publish_logger.info(f" {log_prefix} Found {len(tags)} tags from taxonomy_terms")
+ for tag in tags:
+ publish_logger.info(f" {log_prefix} π·οΈ Tag: '{tag}'")
+
+ # Add primary keyword as tag (if not already in tags)
if content.primary_keyword:
- tags.append(content.primary_keyword)
- publish_logger.info(f" {log_prefix} π·οΈ Primary keyword: '{content.primary_keyword}'")
+ if content.primary_keyword not in tags:
+ tags.append(content.primary_keyword)
+ publish_logger.info(f" {log_prefix} π·οΈ Primary keyword: '{content.primary_keyword}'")
else:
publish_logger.warning(f" {log_prefix} β οΈ No primary keyword found")
+ # Add secondary keywords as tags (if not already in tags)
if content.secondary_keywords:
if isinstance(content.secondary_keywords, list):
- tags.extend(content.secondary_keywords)
+ for keyword in content.secondary_keywords:
+ if keyword not in tags:
+ tags.append(keyword)
publish_logger.info(f" {log_prefix} π·οΈ Secondary keywords (list): {content.secondary_keywords}")
elif isinstance(content.secondary_keywords, str):
try:
keywords = json.loads(content.secondary_keywords)
if isinstance(keywords, list):
- tags.extend(keywords)
+ for keyword in keywords:
+ if keyword not in tags:
+ tags.append(keyword)
publish_logger.info(f" {log_prefix} π·οΈ Secondary keywords (JSON): {keywords}")
except (json.JSONDecodeError, TypeError):
publish_logger.warning(f" {log_prefix} β οΈ Failed to parse secondary_keywords as JSON: {content.secondary_keywords}")
else:
publish_logger.warning(f" {log_prefix} β οΈ No secondary keywords found")
+ publish_logger.info(f" {log_prefix} β
TOTAL tags: {len(tags)}")
+
+
if not tags:
publish_logger.warning(f" {log_prefix} β οΈ No tags found for content")
else:
@@ -154,17 +182,30 @@ def publish_content_to_wordpress(self, content_id: int, site_integration_id: int
featured_image_url = None
gallery_images = []
+ def convert_image_path_to_url(image_path):
+ """Convert local image path to public URL"""
+ if not image_path:
+ return None
+ # Convert: /data/app/igny8/frontend/public/images/... -> https://app.igny8.com/images/...
+ if '/frontend/public/' in image_path:
+ relative_path = image_path.split('/frontend/public/')[-1]
+ return f"https://app.igny8.com/{relative_path}"
+ return image_path
+
for image in images:
- if image.image_type == 'featured' and image.image_url:
- featured_image_url = image.image_url
- publish_logger.info(f" {log_prefix} πΌοΈ Featured image: {image.image_url[:80]}...")
- elif image.image_type == 'in_article' and image.image_url:
+ # Use image_path (local file) and convert to public URL
+ image_url = convert_image_path_to_url(image.image_path) if hasattr(image, 'image_path') and image.image_path else None
+
+ if image.image_type == 'featured' and image_url:
+ featured_image_url = image_url
+ publish_logger.info(f" {log_prefix} πΌοΈ Featured image: {image_url[:80]}...")
+ elif image.image_type == 'in_article' and image_url:
gallery_images.append({
- 'url': image.image_url,
- 'alt': image.alt_text or '',
- 'caption': image.caption or ''
+ 'url': image_url,
+ 'alt': getattr(image, 'alt', '') or '',
+ 'caption': getattr(image, 'caption', '') or ''
})
- publish_logger.info(f" {log_prefix} πΌοΈ Gallery image {len(gallery_images)}: {image.image_url[:60]}...")
+ publish_logger.info(f" {log_prefix} πΌοΈ Gallery image {len(gallery_images)}: {image_url[:60]}...")
if not featured_image_url:
publish_logger.warning(f" {log_prefix} β οΈ No featured image found")
diff --git a/backend/igny8_core/urls.py b/backend/igny8_core/urls.py
index 49529308..f03cdadc 100644
--- a/backend/igny8_core/urls.py
+++ b/backend/igny8_core/urls.py
@@ -39,7 +39,7 @@ urlpatterns = [
path('api/v1/auth/', include('igny8_core.auth.urls')), # Auth endpoints
path('api/v1/planner/', include('igny8_core.modules.planner.urls')),
path('api/v1/writer/', include('igny8_core.modules.writer.urls')),
- path('api/v1/site-builder/', include('igny8_core.modules.site_builder.urls')),
+ # Site Builder module removed - legacy blueprint functionality deprecated
path('api/v1/system/', include('igny8_core.modules.system.urls')),
path('api/v1/billing/', include('igny8_core.modules.billing.urls')), # Billing endpoints
path('api/v1/automation/', include('igny8_core.modules.automation.urls')), # Automation endpoints
diff --git a/docs/QUICK-REFERENCE-TAXONOMY.md b/docs/QUICK-REFERENCE-TAXONOMY.md
new file mode 100644
index 00000000..3c7340ab
--- /dev/null
+++ b/docs/QUICK-REFERENCE-TAXONOMY.md
@@ -0,0 +1,362 @@
+# Quick Reference: Content & Taxonomy After SiteBuilder Removal
+
+## Django Admin URLs
+
+```
+Content Management:
+http://your-domain/admin/writer/content/
+
+Taxonomy Management:
+http://your-domain/admin/writer/contenttaxonomy/
+
+Tasks Queue:
+http://your-domain/admin/writer/tasks/
+```
+
+## Common Django ORM Queries
+
+### Working with Content
+
+```python
+from igny8_core.business.content.models import Content, ContentTaxonomy
+
+# Get content with its taxonomy
+content = Content.objects.get(id=1)
+categories = content.taxonomy_terms.filter(taxonomy_type='category')
+tags = content.taxonomy_terms.filter(taxonomy_type='tag')
+
+# Create content with taxonomy
+content = Content.objects.create(
+ account=account,
+ site=site,
+ sector=sector,
+ cluster=cluster,
+ title="My Article",
+ content_html="Content here
",
+ content_type='post',
+ content_structure='article'
+)
+
+# Add categories and tags
+tech_cat = ContentTaxonomy.objects.get(name='Technology', taxonomy_type='category')
+tutorial_tag = ContentTaxonomy.objects.get(name='Tutorial', taxonomy_type='tag')
+content.taxonomy_terms.add(tech_cat, tutorial_tag)
+
+# Remove taxonomy
+content.taxonomy_terms.remove(tech_cat)
+
+# Clear all taxonomy
+content.taxonomy_terms.clear()
+```
+
+### Working with Taxonomy
+
+```python
+# Create category
+category = ContentTaxonomy.objects.create(
+ account=account,
+ site=site,
+ sector=sector,
+ name='Technology',
+ slug='technology',
+ taxonomy_type='category',
+ description='Tech-related content'
+)
+
+# Create tag
+tag = ContentTaxonomy.objects.create(
+ account=account,
+ site=site,
+ sector=sector,
+ name='Tutorial',
+ slug='tutorial',
+ taxonomy_type='tag'
+)
+
+# Get all content with this taxonomy
+tech_content = category.contents.all()
+
+# Get WordPress-synced taxonomy
+wp_category = ContentTaxonomy.objects.get(
+ external_id=5,
+ external_taxonomy='category',
+ site=site
+)
+```
+
+### WordPress Publishing
+
+```python
+from igny8_core.tasks.wordpress_publishing import publish_content_to_wordpress
+
+# Publish content (categories/tags extracted automatically)
+result = publish_content_to_wordpress.delay(
+ content_id=content.id,
+ site_url='https://example.com',
+ username='admin',
+ app_password='xxxx xxxx xxxx xxxx'
+)
+
+# The task automatically extracts:
+categories = [
+ term.name
+ for term in content.taxonomy_terms.filter(taxonomy_type='category')
+]
+tags = [
+ term.name
+ for term in content.taxonomy_terms.filter(taxonomy_type='tag')
+]
+```
+
+## API Endpoints (REST)
+
+```
+GET /api/v1/writer/content/ - List all content
+POST /api/v1/writer/content/ - Create content
+GET /api/v1/writer/content/{id}/ - Get content detail
+PATCH /api/v1/writer/content/{id}/ - Update content
+DELETE /api/v1/writer/content/{id}/ - Delete content
+
+GET /api/v1/writer/taxonomy/ - List all taxonomy
+POST /api/v1/writer/taxonomy/ - Create taxonomy
+GET /api/v1/writer/taxonomy/{id}/ - Get taxonomy detail
+PATCH /api/v1/writer/taxonomy/{id}/ - Update taxonomy
+DELETE /api/v1/writer/taxonomy/{id}/ - Delete taxonomy
+
+POST /api/v1/publisher/publish/ - Publish content
+```
+
+## Database Schema
+
+### Content Table (igny8_content)
+
+| Column | Type | Description |
+|--------|------|-------------|
+| id | PK | Primary key |
+| site_id | FK | Multi-tenant site |
+| sector_id | FK | Multi-tenant sector |
+| cluster_id | FK | Parent cluster (required) |
+| title | VARCHAR(255) | Content title |
+| content_html | TEXT | Final HTML content |
+| word_count | INTEGER | Calculated word count |
+| meta_title | VARCHAR(255) | SEO title |
+| meta_description | TEXT | SEO description |
+| primary_keyword | VARCHAR(255) | Primary SEO keyword |
+| secondary_keywords | JSON | Secondary keywords |
+| content_type | VARCHAR(50) | post, page, product, taxonomy |
+| content_structure | VARCHAR(50) | article, guide, review, etc. |
+| external_id | VARCHAR(255) | WordPress post ID |
+| external_url | URL | WordPress URL |
+| external_type | VARCHAR(100) | WordPress post type |
+| sync_status | VARCHAR(50) | Sync status |
+| source | VARCHAR(50) | igny8 or wordpress |
+| status | VARCHAR(50) | draft, review, published |
+
+### Taxonomy Table (igny8_content_taxonomy_terms)
+
+| Column | Type | Description |
+|--------|------|-------------|
+| id | PK | Primary key |
+| site_id | FK | Multi-tenant site |
+| sector_id | FK | Multi-tenant sector |
+| name | VARCHAR(255) | Term name |
+| slug | VARCHAR(255) | URL slug |
+| taxonomy_type | VARCHAR(50) | category or tag |
+| description | TEXT | Term description |
+| count | INTEGER | Usage count |
+| external_taxonomy | VARCHAR(100) | category, post_tag |
+| external_id | INTEGER | WordPress term_id |
+| metadata | JSON | Additional metadata |
+
+### Relation Table (igny8_content_taxonomy_relations)
+
+| Column | Type | Description |
+|--------|------|-------------|
+| id | PK | Primary key |
+| content_id | FK | Content reference |
+| taxonomy_id | FK | Taxonomy reference |
+| created_at | TIMESTAMP | Creation timestamp |
+| updated_at | TIMESTAMP | Update timestamp |
+
+**Constraints:**
+- UNIQUE(content_id, taxonomy_id)
+
+## Workflow Commands
+
+### 1. Run Migrations (When Ready)
+
+```bash
+# Apply blueprint removal migration
+docker exec -it igny8_backend python manage.py migrate
+
+# Check migration status
+docker exec -it igny8_backend python manage.py showmigrations
+```
+
+### 2. Create Test Data
+
+```bash
+# Django shell
+docker exec -it igny8_backend python manage.py shell
+
+# Then in shell:
+from igny8_core.auth.models import Account, Site, Sector
+from igny8_core.business.planning.models import Keywords, Clusters
+from igny8_core.business.content.models import Content, ContentTaxonomy
+
+# Get your site/sector
+account = Account.objects.first()
+site = account.sites.first()
+sector = site.sectors.first()
+cluster = Clusters.objects.filter(sector=sector).first()
+
+# Create taxonomy
+cat = ContentTaxonomy.objects.create(
+ account=account,
+ site=site,
+ sector=sector,
+ name='Tech',
+ slug='tech',
+ taxonomy_type='category'
+)
+
+tag = ContentTaxonomy.objects.create(
+ account=account,
+ site=site,
+ sector=sector,
+ name='Tutorial',
+ slug='tutorial',
+ taxonomy_type='tag'
+)
+
+# Create content
+content = Content.objects.create(
+ account=account,
+ site=site,
+ sector=sector,
+ cluster=cluster,
+ title='Test Article',
+ content_html='Test content
',
+ content_type='post',
+ content_structure='article'
+)
+
+# Add taxonomy
+content.taxonomy_terms.add(cat, tag)
+
+# Verify
+print(content.taxonomy_terms.all())
+```
+
+### 3. Test WordPress Publishing
+
+```bash
+# Check celery is running
+docker logs igny8_celery_worker --tail 50
+
+# Check publish logs
+tail -f backend/logs/publish-sync-logs/*.log
+
+# Manually trigger publish (Django shell)
+from igny8_core.tasks.wordpress_publishing import publish_content_to_wordpress
+result = publish_content_to_wordpress.delay(
+ content_id=1,
+ site_url='https://your-site.com',
+ username='admin',
+ app_password='xxxx xxxx xxxx xxxx'
+)
+```
+
+## Troubleshooting
+
+### Backend Won't Start
+
+```bash
+# Check logs
+docker logs igny8_backend --tail 100
+
+# Force recreate (clears Python bytecode cache)
+docker compose -f docker-compose.app.yml up -d --force-recreate igny8_backend
+
+# Check for import errors
+docker exec -it igny8_backend python manage.py check
+```
+
+### Celery Not Processing Tasks
+
+```bash
+# Check celery logs
+docker logs igny8_celery_worker --tail 100
+
+# Restart celery
+docker compose -f docker-compose.app.yml restart igny8_celery_worker
+
+# Test celery connection
+docker exec -it igny8_backend python manage.py shell
+>>> from celery import current_app
+>>> current_app.connection().ensure_connection(max_retries=3)
+```
+
+### Migration Issues
+
+```bash
+# Check current migrations
+docker exec -it igny8_backend python manage.py showmigrations
+
+# Create new migration (if needed)
+docker exec -it igny8_backend python manage.py makemigrations
+
+# Fake migration (if tables already dropped manually)
+docker exec -it igny8_backend python manage.py migrate site_building 0002 --fake
+```
+
+### WordPress Sync Not Working
+
+```bash
+# Check publish logs
+tail -f backend/logs/publish-sync-logs/*.log
+
+# Check WordPress plugin logs (on WordPress server)
+tail -f wp-content/plugins/igny8-bridge/logs/*.log
+
+# Test WordPress REST API manually
+curl -X GET https://your-site.com/wp-json/wp/v2/posts \
+ -u "username:app_password"
+```
+
+## File Locations Reference
+
+```
+Backend Code:
+ββ backend/igny8_core/business/content/models.py # Content & Taxonomy models
+ββ backend/igny8_core/business/publishing/models.py # Publishing records
+ββ backend/igny8_core/modules/publisher/views.py # Publisher API
+ββ backend/igny8_core/tasks/wordpress_publishing.py # WordPress publish task
+ββ backend/igny8_core/settings.py # Django settings
+
+Frontend Code:
+ββ frontend/src/services/api.ts # API client
+ββ frontend/src/modules/writer/ # Writer UI
+
+Documentation:
+ββ docs/SITEBUILDER-REMOVAL-SUMMARY.md # This removal summary
+ββ docs/TAXONOMY-RELATIONSHIP-DIAGRAM.md # Taxonomy diagrams
+ββ docs/02-PLANNER-WRITER-WORKFLOW-TECHNICAL-GUIDE.md # Workflow guide
+ββ docs/04-WORDPRESS-BIDIRECTIONAL-SYNC-REFERENCE.md # WordPress sync
+
+Migrations:
+ββ backend/igny8_core/business/site_building/migrations/0002_remove_blueprint_models.py
+
+Logs:
+ββ backend/logs/publish-sync-logs/*.log # Publishing logs
+ββ igny8-wp-plugin/logs/*.log # WordPress plugin logs
+```
+
+## Support Resources
+
+1. **Backend Logs:** `docker logs igny8_backend`
+2. **Celery Logs:** `docker logs igny8_celery_worker`
+3. **Publishing Logs:** `backend/logs/publish-sync-logs/`
+4. **Django Admin:** `http://your-domain/admin/`
+5. **API Docs:** `http://your-domain/api/v1/`
+6. **Workflow Guide:** `docs/02-PLANNER-WRITER-WORKFLOW-TECHNICAL-GUIDE.md`
diff --git a/docs/SITEBUILDER-REMOVAL-MIGRATION-GUIDE.md b/docs/SITEBUILDER-REMOVAL-MIGRATION-GUIDE.md
new file mode 100644
index 00000000..958e9384
--- /dev/null
+++ b/docs/SITEBUILDER-REMOVAL-MIGRATION-GUIDE.md
@@ -0,0 +1,351 @@
+# SiteBuilder Removal - Complete Migration Guide
+
+**Date:** December 1, 2025
+**Status:** Code changes completed, database migration pending
+
+---
+
+## Summary
+
+All SiteBuilder and Blueprint functionality has been removed from the IGNY8 system. The taxonomy system has been simplified to use the direct `Content.taxonomy_terms` many-to-many relationship with the `ContentTaxonomy` model.
+
+---
+
+## What Was Removed
+
+### Backend Models (Django)
+- β
`SiteBlueprint` - Site structure blueprints
+- β
`PageBlueprint` - Individual page definitions
+- β
`SiteBlueprintCluster` - Cluster to blueprint mappings
+- β
`SiteBlueprintTaxonomy` - Blueprint taxonomy definitions
+- β
`BusinessType`, `AudienceProfile`, `BrandPersonality`, `HeroImageryDirection` - SiteBuilder metadata options
+- β
`ContentTaxonomyMap` - Replaced by `Content.taxonomy_terms` M2M field
+
+### Backend Modules
+- β
Removed entire `igny8_core.modules.site_builder` module
+- β
Removed from `INSTALLED_APPS` in settings.py
+- β
Removed `/api/v1/site-builder/` URL patterns
+- β
Cleaned up `site_building/models.py`, `site_building/admin.py`
+
+### Backend Services & Views
+- β
Updated `PublisherService` - removed `publish_to_sites()` method
+- β
Updated `PublisherViewSet` - removed blueprint publishing actions
+- β
Updated `DeploymentRecordViewSet` - removed blueprint references
+- β
Updated `PublishingRecord` model - removed `site_blueprint` field
+- β
Updated `DeploymentRecord` model - removed `site_blueprint` field
+- β
Fixed `metadata_mapping_service.py` - removed ContentTaxonomyMap
+- β
Fixed `candidate_engine.py` - uses `Content.taxonomy_terms` now
+- β
Fixed `sites_renderer_adapter.py` - uses M2M taxonomy relationship
+- β
Fixed `planner/serializers.py` - removed SiteBlueprintTaxonomy import
+
+### Frontend
+- β
Removed `frontend/src/modules/siteBuilder/` directory
+- β
Removed `frontend/src/types/siteBuilder.ts`
+- β
Removed `frontend/src/services/siteBuilder.api.ts`
+- β
Removed SiteBlueprint API functions from `services/api.ts`
+- β
Removed SiteBuilder routes from navigation
+
+---
+
+## Current Taxonomy System
+
+### Simplified Architecture
+
+**Before (Complex - SiteBuilder era):**
+```
+Content β ContentTaxonomyMap β SiteBlueprintTaxonomy β Clusters
+ (through table) (blueprint planning)
+```
+
+**After (Simplified - Current):**
+```
+Content ββ ContentTaxonomy (many-to-many direct relationship)
+ β
+ Cluster (foreign key)
+```
+
+### ContentTaxonomy Model
+**Location:** `backend/igny8_core/business/content/models.py`
+
+**Fields:**
+- `name` - Term name (e.g., "Outdoor Living Design")
+- `slug` - URL-safe version
+- `taxonomy_type` - Choices: `category`, `tag`
+- `external_id` - WordPress term_id for sync
+- `external_taxonomy` - WordPress taxonomy slug (category, post_tag)
+- `description` - Term description
+- `account`, `site`, `sector` - Multi-tenancy fields
+
+**Relationship:**
+- `Content.taxonomy_terms` - ManyToManyField to ContentTaxonomy
+- Categories/tags are AI-generated and linked directly to Content
+
+---
+
+## How Content Taxonomy Works Now
+
+### Publishing Flow (IGNY8 β WordPress)
+
+```python
+# In wordpress_publishing.py task
+
+# STEP 4: Get categories from Content.taxonomy_terms
+categories = [
+ term.name
+ for term in content.taxonomy_terms.filter(taxonomy_type='category')
+]
+
+# Fallback to cluster if no taxonomy_terms
+if not categories and content.cluster:
+ categories.append(content.cluster.name)
+
+# STEP 5: Get tags from taxonomy_terms + keywords
+tags = [
+ term.name
+ for term in content.taxonomy_terms.filter(taxonomy_type='tag')
+]
+
+# Add keywords as tags
+if content.primary_keyword not in tags:
+ tags.append(content.primary_keyword)
+for keyword in content.secondary_keywords:
+ if keyword not in tags:
+ tags.append(keyword)
+```
+
+### Data Flow
+
+```
+Keywords β Clusters β Ideas β Tasks β Content
+ βββ taxonomy_terms (M2M)
+ β βββ Categories
+ β βββ Tags
+ βββ cluster (FK)
+ βββ primary_keyword
+ βββ secondary_keywords
+ β
+ WordPress Publish
+ βββ Categories (from taxonomy_terms + cluster)
+ βββ Tags (from taxonomy_terms + keywords)
+```
+
+---
+
+## Django Admin
+
+**ContentTaxonomy is registered** in `backend/igny8_core/modules/writer/admin.py`:
+- View at: `/admin/writer/contenttaxonomy/`
+- Shows: name, taxonomy_type, slug, external_id, external_taxonomy, site, sector
+- Searchable by: name, slug, external_taxonomy
+- Filterable by: taxonomy_type, site, sector
+
+---
+
+## Database Migration (PENDING)
+
+**Migration File:** `backend/igny8_core/business/site_building/migrations/0002_remove_blueprint_models.py`
+
+**Status:** Created but not applied (requires external PostgreSQL access)
+
+### Manual SQL Commands
+
+Run these SQL commands directly on your production PostgreSQL database:
+
+```sql
+-- Drop foreign key constraints first
+ALTER TABLE igny8_publishing_records
+ DROP CONSTRAINT IF EXISTS igny8_publishing_recor_site_blueprint_id_9f4e8c7a_fk_igny8_sit CASCADE;
+
+ALTER TABLE igny8_deployment_records
+ DROP CONSTRAINT IF EXISTS igny8_deployment_recor_site_blueprint_id_3a2b7c1d_fk_igny8_sit CASCADE;
+
+-- Drop blueprint tables
+DROP TABLE IF EXISTS igny8_site_blueprint_taxonomies CASCADE;
+DROP TABLE IF EXISTS igny8_site_blueprint_clusters CASCADE;
+DROP TABLE IF EXISTS igny8_page_blueprints CASCADE;
+DROP TABLE IF EXISTS igny8_site_blueprints CASCADE;
+
+-- Drop SiteBuilder metadata tables
+DROP TABLE IF EXISTS igny8_site_builder_business_types CASCADE;
+DROP TABLE IF EXISTS igny8_site_builder_audience_profiles CASCADE;
+DROP TABLE IF EXISTS igny8_site_builder_brand_personalities CASCADE;
+DROP TABLE IF EXISTS igny8_site_builder_hero_imagery CASCADE;
+
+-- Drop site_blueprint_id columns
+ALTER TABLE igny8_publishing_records DROP COLUMN IF EXISTS site_blueprint_id CASCADE;
+ALTER TABLE igny8_deployment_records DROP COLUMN IF EXISTS site_blueprint_id CASCADE;
+
+-- Drop indexes
+DROP INDEX IF EXISTS igny8_publishing_recor_site_blueprint_id_des_b7c4e5f8_idx;
+
+-- Mark migration as applied
+INSERT INTO django_migrations (app, name, applied)
+VALUES ('site_building', '0002_remove_blueprint_models', NOW())
+ON CONFLICT DO NOTHING;
+```
+
+---
+
+## Files Modified
+
+### Backend Python Files (24 files)
+1. `backend/igny8_core/business/site_building/models.py` - All models removed
+2. `backend/igny8_core/business/site_building/admin.py` - All admin classes removed
+3. `backend/igny8_core/business/publishing/models.py` - Removed site_blueprint FK
+4. `backend/igny8_core/business/publishing/services/publisher_service.py` - Removed publish_to_sites
+5. `backend/igny8_core/business/publishing/services/adapters/sites_renderer_adapter.py` - Updated taxonomy usage
+6. `backend/igny8_core/business/content/services/metadata_mapping_service.py` - Removed ContentTaxonomyMap
+7. `backend/igny8_core/business/linking/services/candidate_engine.py` - Updated to M2M taxonomy
+8. `backend/igny8_core/business/optimization/services/analyzer.py` - Updated taxonomy check
+9. `backend/igny8_core/modules/publisher/views.py` - Removed blueprint actions
+10. `backend/igny8_core/modules/planner/serializers.py` - Removed SiteBlueprintTaxonomy
+11. `backend/igny8_core/tasks/wordpress_publishing.py` - Uses Content.taxonomy_terms
+12. `backend/igny8_core/urls.py` - Removed site-builder URL
+13. `backend/igny8_core/settings.py` - Removed site_builder from INSTALLED_APPS
+
+### Frontend TypeScript Files (3 files)
+1. `frontend/src/services/api.ts` - Removed SiteBlueprint API functions
+2. Removed: `frontend/src/types/siteBuilder.ts`
+3. Removed: `frontend/src/services/siteBuilder.api.ts`
+4. Removed: `frontend/src/modules/siteBuilder/` directory
+
+### Migrations
+1. Created: `backend/igny8_core/business/site_building/migrations/0002_remove_blueprint_models.py`
+
+---
+
+## Testing Checklist
+
+### Backend Health Check
+```bash
+# Check if backend starts successfully
+docker compose -f docker-compose.app.yml logs igny8_backend | grep -i "error\|exception"
+
+# Should show no import errors
+```
+
+### Taxonomy Workflow Test
+
+1. **Check ContentTaxonomy in Admin**
+ - Go to `/admin/writer/contenttaxonomy/`
+ - Verify model is accessible
+ - Check existing taxonomy terms
+
+2. **Test Content Creation**
+ ```bash
+ # In Django shell
+ from igny8_core.business.content.models import Content, ContentTaxonomy
+
+ content = Content.objects.first()
+ print(f"Taxonomy terms: {content.taxonomy_terms.count()}")
+ print(f"Cluster: {content.cluster.name if content.cluster else 'None'}")
+ ```
+
+3. **Test Publishing to WordPress**
+ - Create/select content with taxonomy_terms
+ - Publish to WordPress from Review page
+ - Verify categories and tags appear in WordPress
+ - Check logs: `tail -f backend/logs/publish-sync-logs/publish-sync.log`
+
+### Expected Log Output
+```
+[2025-12-01 02:00:00] [INFO] [5-homeg8.com] STEP 4: Loading taxonomy terms...
+[2025-12-01 02:00:00] [INFO] [5-homeg8.com] Found 1 categories from taxonomy_terms
+[2025-12-01 02:00:00] [INFO] [5-homeg8.com] π Category: 'Outdoor Living Design'
+[2025-12-01 02:00:00] [INFO] [5-homeg8.com] β
TOTAL categories: 1
+
+[2025-12-01 02:00:00] [INFO] [5-homeg8.com] STEP 5: Loading tags...
+[2025-12-01 02:00:00] [INFO] [5-homeg8.com] π·οΈ Primary keyword: 'outdoor patio design'
+[2025-12-01 02:00:00] [INFO] [5-homeg8.com] π·οΈ Secondary keywords: ['outdoor living spaces', 'outdoor kitchen design']
+[2025-12-01 02:00:00] [INFO] [5-homeg8.com] β
TOTAL tags: 3
+```
+
+---
+
+## Remaining Legacy References
+
+These files contain SiteBlueprint references in **migrations only** (historical data, safe to ignore):
+- `backend/igny8_core/business/content/migrations/0002_stage1_refactor_task_content_taxonomy.py`
+- `backend/igny8_core/modules/writer/migrations/0001_initial.py`
+- `backend/igny8_core/modules/planner/migrations/0002_initial.py`
+- `backend/igny8_core/tasks/wordpress_publishing_backup.py` (backup file)
+- `backend/igny8_core/tasks/wordpress_publishing_new.py` (backup file)
+
+---
+
+## Benefits of Simplified System
+
+### Before (Complex)
+- 5 models: SiteBlueprint, PageBlueprint, SiteBlueprintCluster, SiteBlueprintTaxonomy, ContentTaxonomyMap
+- 3-level indirection: Content β ContentTaxonomyMap β SiteBlueprintTaxonomy
+- Blueprint planning layer for site building
+- Difficult to understand taxonomy relationships
+
+### After (Simple)
+- 1 model: ContentTaxonomy
+- Direct M2M: Content β ContentTaxonomy
+- AI-generated categories/tags linked directly
+- Clear taxonomy-content relationship in Django Admin
+
+### Performance Improvements
+- Fewer database queries (eliminated ContentTaxonomyMap joins)
+- Simpler ORM queries: `content.taxonomy_terms.filter(taxonomy_type='category')`
+- Easier debugging and maintenance
+
+---
+
+## Next Steps
+
+1. **Apply Database Migration**
+ - Run SQL commands on production PostgreSQL
+ - Mark migration as applied in django_migrations table
+
+2. **Test Publishing Workflow**
+ - Publish content from Review page
+ - Verify categories/tags in WordPress
+ - Check sync logs for any errors
+
+3. **Monitor Logs**
+ - Backend: `/data/app/igny8/backend/logs/publish-sync-logs/`
+ - WordPress: `/wp-content/plugins/igny8-ai-os/logs/publish-sync-logs/`
+
+4. **Update Documentation**
+ - Update SYSTEM-ARCHITECTURE doc to reflect simplified taxonomy
+ - Remove SiteBuilder references from workflow docs
+
+---
+
+## Rollback Plan (If Needed)
+
+If issues arise, you can restore SiteBuilder functionality by:
+
+1. Restore backup files:
+ ```bash
+ cd /data/app/igny8/backend/igny8_core/modules
+ mv site_builder.backup site_builder
+ ```
+
+2. Uncomment in settings.py:
+ ```python
+ 'igny8_core.modules.site_builder.apps.SiteBuilderConfig',
+ ```
+
+3. Restore URL pattern in urls.py:
+ ```python
+ path('api/v1/site-builder/', include('igny8_core.modules.site_builder.urls')),
+ ```
+
+4. Reverse database changes (restore from backup)
+
+---
+
+## Contact & Support
+
+If you encounter any issues during migration:
+- Check logs in `backend/logs/` and WordPress plugin `logs/`
+- Review Django admin for ContentTaxonomy model
+- Test publishing workflow step by step
+
+**Migration completed by:** GitHub Copilot
+**Date:** December 1, 2025
+**Version:** IGNY8 v1.0 - SiteBuilder Removal
diff --git a/docs/SITEBUILDER-REMOVAL-SUMMARY.md b/docs/SITEBUILDER-REMOVAL-SUMMARY.md
new file mode 100644
index 00000000..ebb2ea3c
--- /dev/null
+++ b/docs/SITEBUILDER-REMOVAL-SUMMARY.md
@@ -0,0 +1,364 @@
+# SiteBuilder/Blueprint Removal Summary
+
+**Date:** December 1, 2025
+**Status:** β
Complete - Backend Healthy
+
+## Overview
+
+Successfully removed all SiteBuilder and Blueprint functionality from IGNY8 system while maintaining the core planner-writer-publisher workflow and WordPress integration.
+
+---
+
+## What Was Removed
+
+### 1. Django Models (backend/igny8_core/business/site_building/models.py)
+- β `SiteBlueprint` - Legacy site planning model
+- β `PageBlueprint` - Legacy page planning model
+- β `SiteBlueprintCluster` - Blueprint-cluster relationship
+- β `SiteBlueprintTaxonomy` - Blueprint-taxonomy relationship
+- β `ContentTaxonomyMap` - Replaced with direct M2M
+
+### 2. Django Module
+- β `backend/igny8_core/business/site_builder/` - Entire module removed from INSTALLED_APPS (settings.py line 60)
+- β οΈ Directory still exists with tests/services but not loaded by Django
+
+### 3. Frontend Components
+- β `frontend/src/modules/siteBuilder/` - Entire directory removed
+- β `frontend/src/services/api.ts` - SiteBlueprint interfaces and functions removed (replaced with comment)
+
+### 4. API Endpoints (backend/igny8_core/modules/publisher/views.py)
+- β `PublisherViewSet.publish_to_sites()` - Blueprint publishing action
+- β `PublisherViewSet.deployment_readiness()` - Blueprint readiness check
+- β `PublisherViewSet.deploy()` - Blueprint deployment action
+
+### 5. Publishing Service Methods (backend/igny8_core/business/publishing/services/publisher_service.py)
+- β `PublisherService.publish_to_sites()` - Blueprint publishing
+- β `PublisherService.get_deployment_status()` - Blueprint deployment status
+
+### 6. Publishing Models Foreign Keys
+- β `PublishingRecord.site_blueprint` - FK removed
+- β `DeploymentRecord.site_blueprint` - FK removed
+
+---
+
+## What Was Simplified
+
+### Taxonomy Architecture Change
+
+**Before (Complex):**
+```
+Content β ContentTaxonomyMap β ContentTaxonomy
+ (through table with FK)
+```
+
+**After (Simple):**
+```
+Content β ContentTaxonomy
+(many-to-many via ContentTaxonomyRelation)
+```
+
+**Files Changed:**
+1. `backend/igny8_core/business/content/models.py`
+ - Added `Content.taxonomy_terms` M2M field
+ - Through model: `ContentTaxonomyRelation`
+
+2. `backend/igny8_core/tasks/wordpress_publishing.py`
+ - Updated to use `content.taxonomy_terms.filter(taxonomy_type='category')`
+ - Updated to use `content.taxonomy_terms.filter(taxonomy_type='tag')`
+
+3. `backend/igny8_core/business/planning/services/candidate_engine.py`
+ - Changed from `ContentTaxonomyMap.objects.filter(content=...)`
+ - To: `content.taxonomy_terms.values_list('id', flat=True)`
+
+---
+
+## Current System Architecture
+
+### β
Planner-Writer-Publisher Workflow (Intact)
+
+```
+1. PLANNER (Phase 1-3)
+ Keywords β Clusters β Ideas
+
+2. WRITER (Phase 4)
+ Ideas β Tasks β Content
+
+3. PUBLISHER (Phase 5)
+ Content β WordPress/Sites
+```
+
+### β
Content Taxonomy Model
+
+**Location:** `backend/igny8_core/business/content/models.py`
+
+**Model: ContentTaxonomy**
+```python
+class ContentTaxonomy(SiteSectorBaseModel):
+ # Core fields
+ name CharField(255) # "Technology", "Tutorial"
+ slug SlugField(255) # "technology", "tutorial"
+ taxonomy_type CharField(50) # "category" or "tag"
+ description TextField # Term description
+ count IntegerField # Usage count
+
+ # WordPress sync fields
+ external_taxonomy CharField(100) # "category", "post_tag"
+ external_id IntegerField # WordPress term_id
+
+ # Metadata
+ metadata JSONField # AI generation details
+
+ # Relationships
+ contents M2M(Content) # Related content
+```
+
+**Model: Content**
+```python
+class Content(SiteSectorBaseModel):
+ # Core fields
+ title CharField(255)
+ content_html TextField
+ word_count IntegerField
+
+ # SEO
+ meta_title CharField(255)
+ meta_description TextField
+ primary_keyword CharField(255)
+ secondary_keywords JSONField
+
+ # Relationships
+ cluster FK(Clusters) # Required parent cluster
+ taxonomy_terms M2M(ContentTaxonomy) # Categories & tags
+
+ # Type/Structure
+ content_type CharField(50) # post, page, product
+ content_structure CharField(50) # article, guide, review, etc.
+
+ # WordPress sync
+ external_id CharField(255) # WordPress post ID
+ external_url URLField # WordPress URL
+ external_type CharField(100) # WordPress post type
+ sync_status CharField(50) # Sync status
+
+ # Source & Status
+ source CharField(50) # igny8 or wordpress
+ status CharField(50) # draft, review, published
+```
+
+**Through Model: ContentTaxonomyRelation**
+```python
+class ContentTaxonomyRelation(models.Model):
+ content FK(Content)
+ taxonomy FK(ContentTaxonomy)
+ created_at DateTimeField
+ updated_at DateTimeField
+
+ unique_together = [['content', 'taxonomy']]
+```
+
+---
+
+## WordPress Integration (Unchanged)
+
+### Bidirectional Sync Still Works
+
+**From IGNY8 β WordPress:**
+```python
+# File: backend/igny8_core/tasks/wordpress_publishing.py
+categories = [
+ term.name
+ for term in content.taxonomy_terms.filter(taxonomy_type='category')
+]
+tags = [
+ term.name
+ for term in content.taxonomy_terms.filter(taxonomy_type='tag')
+]
+```
+
+**From WordPress β IGNY8:**
+- WordPress plugin continues to sync content back
+- External IDs maintained in `Content.external_id` and `ContentTaxonomy.external_id`
+- Logging system intact: `[5-homeg8.com] [POST] ...`
+
+---
+
+## Database Migration
+
+### Migration File Created
+**Location:** `backend/igny8_core/business/site_building/migrations/0002_remove_blueprint_models.py`
+
+**Operations:**
+1. Drop table: `site_building_siteblueprint`
+2. Drop table: `site_building_pageblueprint`
+3. Drop table: `site_building_siteblueprintcluster`
+4. Drop table: `site_building_siteblueprinttaxonomy`
+5. Remove FK: `publishing_publishingrecord.site_blueprint_id`
+6. Remove FK: `publishing_deploymentrecord.site_blueprint_id`
+
+### β οΈ Migration Status: NOT YET APPLIED
+
+**Reason:** PostgreSQL database is external (not in docker-compose)
+
+**To Apply Manually:**
+```bash
+# Connect to your PostgreSQL database and run:
+cd /data/app/igny8/backend
+docker exec -it igny8_backend python manage.py migrate
+```
+
+**Or if manual SQL preferred:**
+```sql
+-- Drop blueprint tables
+DROP TABLE IF EXISTS site_building_siteblueprinttaxonomy CASCADE;
+DROP TABLE IF EXISTS site_building_siteblueprintcluster CASCADE;
+DROP TABLE IF EXISTS site_building_pageblueprint CASCADE;
+DROP TABLE IF EXISTS site_building_siteblueprint CASCADE;
+
+-- Remove foreign keys from publishing tables
+ALTER TABLE publishing_publishingrecord DROP COLUMN IF EXISTS site_blueprint_id;
+ALTER TABLE publishing_deploymentrecord DROP COLUMN IF EXISTS site_blueprint_id;
+```
+
+---
+
+## Files Modified (24 Backend + 3 Frontend)
+
+### Backend Core
+1. `backend/igny8_core/settings.py` - Removed site_builder from INSTALLED_APPS
+2. `backend/igny8_core/business/site_building/models.py` - Emptied (placeholder comments only)
+3. `backend/igny8_core/business/site_building/admin.py` - Emptied
+4. `backend/igny8_core/business/publishing/models.py` - Removed site_blueprint FK
+5. `backend/igny8_core/business/publishing/services/publisher_service.py` - Removed blueprint methods
+6. `backend/igny8_core/modules/publisher/views.py` - Removed blueprint actions
+7. `backend/igny8_core/modules/publisher/serializers.py` - Fixed to use exclude=[]
+8. `backend/igny8_core/tasks/wordpress_publishing.py` - Updated to use M2M taxonomy
+
+### Backend Services
+9. `backend/igny8_core/business/planning/services/metadata_mapping_service.py` - Removed ContentTaxonomyMap import
+10. `backend/igny8_core/business/planning/services/candidate_engine.py` - Updated to use M2M taxonomy
+
+### Frontend
+11. `frontend/src/services/api.ts` - Removed SiteBlueprint interfaces/functions
+12. `frontend/src/modules/siteBuilder/` - **DELETED DIRECTORY**
+
+---
+
+## Verification Steps Completed
+
+### β
Backend Health Check
+```bash
+$ docker ps --filter "name=igny8_backend"
+igny8_backend Up 27 seconds (healthy)
+```
+
+### β
Celery Worker Health
+```bash
+$ docker ps --filter "name=igny8_celery"
+igny8_celery_worker Up About a minute
+igny8_celery_beat Up 3 hours
+```
+
+### β
Backend Startup Logs
+```
+[2025-12-01 02:03:31] [INFO] Starting gunicorn 23.0.0
+[2025-12-01 02:03:31] [INFO] Listening at: http://0.0.0.0:8010
+[2025-12-01 02:03:31] [INFO] Using worker: sync
+[2025-12-01 02:03:31] [INFO] Booting worker with pid: 7
+[2025-12-01 02:03:31] [INFO] Booting worker with pid: 8
+[2025-12-01 02:03:31] [INFO] Booting worker with pid: 9
+[2025-12-01 02:03:31] [INFO] Booting worker with pid: 10
+```
+
+### β
No Import Errors
+No `NameError: name 'SiteBlueprint' is not defined` errors
+
+---
+
+## Remaining References (Harmless)
+
+**Where:** site_building tests and services (not loaded)
+- `backend/igny8_core/business/site_building/tests/` - Test files (not executed)
+- `backend/igny8_core/business/site_building/services/` - Service files (not imported)
+- `backend/igny8_core/business/planning/models.py` - Comment only
+
+**Why Harmless:** site_builder module removed from INSTALLED_APPS so Django never loads these files.
+
+---
+
+## Django Admin Access
+
+### β
ContentTaxonomy Available
+**URL:** `http://your-domain/admin/writer/contenttaxonomy/`
+
+**Features:**
+- Create/edit categories and tags
+- Set taxonomy_type (category/tag)
+- Configure WordPress sync (external_id, external_taxonomy)
+- View count and metadata
+
+### β
Content Available
+**URL:** `http://your-domain/admin/writer/content/`
+
+**Features:**
+- View all content
+- Edit taxonomy_terms M2M relationships
+- See cluster relationships
+- WordPress sync status
+
+---
+
+## Next Steps (Optional)
+
+### 1. Apply Database Migration
+```bash
+docker exec -it igny8_backend python manage.py migrate
+```
+
+### 2. Clean Up Remaining Files (Optional)
+```bash
+# Remove site_building directory entirely if not needed for history
+rm -rf backend/igny8_core/business/site_building/
+
+# Or keep for git history but add .gitignore
+echo "backend/igny8_core/business/site_building/" >> .gitignore
+```
+
+### 3. Test WordPress Publishing
+1. Create test content in Django admin
+2. Assign categories/tags via taxonomy_terms M2M
+3. Publish to WordPress
+4. Verify categories/tags appear correctly
+5. Check sync logs: `backend/logs/publish-sync-logs/*.log`
+
+---
+
+## Technical Notes
+
+### Python Bytecode Cache Issue Resolved
+**Problem:** Docker container cached old .pyc files with SiteBlueprint references
+**Solution:** Used `docker compose up -d --force-recreate igny8_backend` to clear cache
+
+### Import Structure Clean
+- No circular imports
+- No missing dependencies
+- All type hints cleaned from removed models
+
+### Multi-Tenant Architecture Intact
+```
+Account β Site β Sector β Content/Taxonomy
+```
+All models inherit from `SiteSectorBaseModel` with proper filtering.
+
+---
+
+## Support
+
+For issues or questions about this removal:
+1. Check backend logs: `docker logs igny8_backend`
+2. Check celery logs: `docker logs igny8_celery_worker`
+3. Check publish logs: `backend/logs/publish-sync-logs/`
+4. Refer to: `docs/02-PLANNER-WRITER-WORKFLOW-TECHNICAL-GUIDE.md`
+
+---
+
+**Summary:** All SiteBuilder/Blueprint functionality successfully removed. Backend healthy. WordPress publishing intact. Taxonomy simplified to direct M2M relationship. Migration file created but not yet applied.
diff --git a/docs/TAXONOMY-RELATIONSHIP-DIAGRAM.md b/docs/TAXONOMY-RELATIONSHIP-DIAGRAM.md
new file mode 100644
index 00000000..46ae0326
--- /dev/null
+++ b/docs/TAXONOMY-RELATIONSHIP-DIAGRAM.md
@@ -0,0 +1,258 @@
+# Content Taxonomy Relationship Diagram
+
+## Current Architecture (Simplified)
+
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β IGNY8 Content System β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Multi-Tenant Hierarchy β
+β β
+β Account βββ¬ββ Site βββ¬ββ Sector βββ¬ββ Keywords β
+β β β βββ Clusters β
+β β β βββ Ideas β
+β β β βββ Tasks β
+β β β βββ Content β
+β β β βββ ContentTaxonomy β
+β β βββ Sector 2 β
+β βββ Site 2 β
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Planner β Writer β Publisher Workflow β
+β β
+β Phase 1-3: PLANNER β
+β ββββββββββββ ββββββββββββ ββββββββββββ β
+β β Keywords β ββ> β Clusters β ββ> β Ideas β β
+β ββββββββββββ ββββββββββββ ββββββββββββ β
+β β β β β
+β β β β β
+β Phase 4: WRITER β
+β β β β β
+β βββββββββββββββββββ΄ββββββββββββββββββ β
+β β β
+β v β
+β ββββββββββββ ββββββββββββ β
+β β Tasks β ββ> β Content β β
+β ββββββββββββ ββββββββββββ β
+β β β
+β β β
+β Phase 5: PUBLISHER β β
+β v β
+β βββββββββββββββ β
+β β WordPress β β
+β β Shopify β β
+β β Sites β β
+β βββββββββββββββ β
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Content β Taxonomy Relationship (Many-to-Many) β
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+βββββββββββββββββββββββββββββββββββ
+β Content Model β
+βββββββββββββββββββββββββββββββββββ
+β PK: id β
+β FK: site_id βββββββ β
+β FK: sector_id β β
+β FK: cluster_id β β
+β β β
+β title β β
+β content_html β β
+β word_count β β
+β meta_title β β
+β meta_description β β
+β β β
+β content_type β β
+β content_structure β β
+β β β
+β external_id β β
+β external_url β β
+β sync_status β β
+β β β
+β source β β
+β status β β
+βββββββββββββ¬ββββββββ β
+ β β
+ β Many-to-Many β
+ β (via ContentTaxonomyRelation)
+ β β
+ v β
+ββββββββββββββββββββββββββββββββββββ
+β ContentTaxonomyRelation Model ββ
+ββββββββββββββββββββββββββββββββββββ
+β PK: id ββ
+β FK: content_id βββββββββββββββββββ
+β FK: taxonomy_id βββββββββββββ
+β β
+β created_at β
+β updated_at β
+β β
+β UNIQUE(content, taxonomy) β
+βββββββββββββββββββββββββββββββ
+ β
+ β
+ v
+ βββββββββββββββββββββββββββββββββββ
+ β ContentTaxonomy Model β
+ βββββββββββββββββββββββββββββββββββ
+ β PK: id β
+ β FK: site_id ββββββ β
+ β FK: sector_id β β
+ β β β
+ β name β β
+ β slug β β
+ β taxonomy_type β ββββ "category" or "tag"
+ β description β β
+ β count β β
+ β β β
+ β WordPress Sync: β β
+ β external_taxonomyβ ββββ "category", "post_tag"
+ β external_id β ββββ WordPress term_id
+ β β β
+ β metadata (JSON) β β
+ ββββββββββββββββββββ β
+ β β
+ β β
+ UNIQUE(site, slug, taxonomy_type)β
+ UNIQUE(site, external_id, external_taxonomy)
+ β β
+ ββββββββββββββββ
+
+
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Usage Example β
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+# Get all categories for a content piece
+categories = content.taxonomy_terms.filter(taxonomy_type='category')
+
+# Get all tags for a content piece
+tags = content.taxonomy_terms.filter(taxonomy_type='tag')
+
+# Add a category to content
+tech_category = ContentTaxonomy.objects.get(name='Technology')
+content.taxonomy_terms.add(tech_category)
+
+# Get all content with a specific tag
+tutorial_tag = ContentTaxonomy.objects.get(name='Tutorial')
+contents = tutorial_tag.contents.all()
+
+# WordPress publishing (automatic)
+wp_categories = [term.name for term in content.taxonomy_terms.filter(taxonomy_type='category')]
+wp_tags = [term.name for term in content.taxonomy_terms.filter(taxonomy_type='tag')]
+
+
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β WordPress Integration (Bidirectional Sync) β
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+IGNY8 β WordPress:
+βββββββββββββββββ
+1. Content created in IGNY8
+2. Categories/tags assigned via taxonomy_terms M2M
+3. Publishing task created
+4. wordpress_publishing.py extracts:
+ - categories = content.taxonomy_terms.filter(taxonomy_type='category')
+ - tags = content.taxonomy_terms.filter(taxonomy_type='tag')
+5. REST API creates WordPress post with terms
+6. external_id saved back to Content model
+7. Log: [5-homeg8.com] [POST] Published: "Article Title" (ID: 123)
+
+WordPress β IGNY8:
+βββββββββββββββββ
+1. WordPress plugin detects post update
+2. REST API sends post data + terms to IGNY8
+3. Content updated/created with external_id
+4. ContentTaxonomy created/updated with external_id
+5. ContentTaxonomyRelation created linking them
+6. Log: [5-homeg8.com] [SYNC] Imported: "Article Title"
+
+
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Database Tables Summary β
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+igny8_content
+ββ id (PK)
+ββ site_id (FK β sites)
+ββ sector_id (FK β sectors)
+ββ cluster_id (FK β clusters)
+ββ title, content_html, word_count
+ββ meta_title, meta_description, keywords
+ββ content_type, content_structure
+ββ external_id, external_url, sync_status
+ββ source, status, timestamps
+
+igny8_content_taxonomy_relations (Through Table)
+ββ id (PK)
+ββ content_id (FK β igny8_content)
+ββ taxonomy_id (FK β igny8_content_taxonomy_terms)
+ββ timestamps
+ UNIQUE(content_id, taxonomy_id)
+
+igny8_content_taxonomy_terms
+ββ id (PK)
+ββ site_id (FK β sites)
+ββ sector_id (FK β sectors)
+ββ name, slug
+ββ taxonomy_type ('category' | 'tag')
+ββ description, count
+ββ external_taxonomy ('category' | 'post_tag')
+ββ external_id (WordPress term_id)
+ββ metadata (JSON)
+ββ timestamps
+ UNIQUE(site_id, slug, taxonomy_type)
+ UNIQUE(site_id, external_id, external_taxonomy)
+
+
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Before vs After Comparison β
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+BEFORE (Complex - with SiteBlueprint):
+ββββββββββββββββββββββββββββββββββββ
+SiteBlueprint βββ¬ββ SiteBlueprintCluster ββ> Clusters
+ βββ SiteBlueprintTaxonomy ββ> ContentTaxonomy
+ βββ PageBlueprint ββ> Content
+
+Content ββ> ContentTaxonomyMap ββ> ContentTaxonomy
+ (separate table with FK)
+
+PublishingRecord βββ¬ββ content_id
+ βββ site_blueprint_id
+
+DeploymentRecord βββ¬ββ content_id
+ βββ site_blueprint_id
+
+
+AFTER (Simple - SiteBlueprint Removed):
+ββββββββββββββββββββββββββββββββββββββ
+Keywords ββ> Clusters ββ> Ideas ββ> Tasks ββ> Content
+
+Content β ContentTaxonomy
+ (M2M via ContentTaxonomyRelation)
+
+PublishingRecord ββ> content_id
+
+DeploymentRecord ββ> content_id
+
+
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Key Benefits β
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+β
Simpler architecture - removed 4 models
+β
Direct M2M relationship - easier to query
+β
Less database joins - better performance
+β
Clear taxonomy model in Django admin
+β
WordPress sync unchanged - still works perfectly
+β
Planner-Writer-Publisher workflow intact
+β
Multi-tenant security maintained
+β
No circular dependencies
+β
Clean codebase - no legacy blueprint code
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
index 3c100514..6f8eed9d 100644
--- a/frontend/src/services/api.ts
+++ b/frontend/src/services/api.ts
@@ -2297,104 +2297,8 @@ export async function deleteTaxonomy(id: number): Promise {
});
}
-// Site Builder API
-export interface SiteBlueprint {
- id: number;
- name: string;
- description?: string;
- config_json: Record;
- structure_json: Record;
- status: string;
- hosting_type: string;
- version: number;
- deployed_version?: number;
- account_id?: number;
- site_id?: number;
- sector_id?: number;
- created_at: string;
- updated_at: string;
- pages?: PageBlueprint[];
- gating_messages?: string[];
-}
-
-export interface PageBlueprint {
- id: number;
- site_blueprint_id: number;
- site_blueprint?: number;
- slug: string;
- title: string;
- type: string;
- blocks_json: any[];
- status: string;
- order: number;
- created_at: string;
- updated_at: string;
-}
-
-
-export async function fetchSiteBlueprints(filters?: {
- site_id?: number;
- sector_id?: number;
- status?: string;
- page?: number;
- page_size?: number;
-}): Promise<{ count: number; next: string | null; previous: string | null; results: SiteBlueprint[] }> {
- const params = new URLSearchParams();
- if (filters?.site_id) params.append('site_id', filters.site_id.toString());
- if (filters?.sector_id) params.append('sector_id', filters.sector_id.toString());
- if (filters?.status) params.append('status', filters.status);
- if (filters?.page) params.append('page', filters.page.toString());
- if (filters?.page_size) params.append('page_size', filters.page_size.toString());
-
- const queryString = params.toString();
- const endpoint = queryString
- ? `/v1/site-builder/blueprints/?${queryString}`
- : `/v1/site-builder/blueprints/`;
- return fetchAPI(endpoint);
-}
-
-export async function fetchSiteBlueprintById(id: number): Promise {
- return fetchAPI(`/v1/site-builder/blueprints/${id}/`);
-}
-
-// Stage 3: Site Progress API
-export interface SiteProgress {
- blueprint_id: number;
- blueprint_name: string;
- overall_status: 'in_progress' | 'complete' | 'blocked';
- cluster_coverage?: {
- total_clusters: number;
- covered_clusters: number;
- details: Array<{
- cluster_id: number;
- cluster_name: string;
- role: string;
- coverage_status: string;
- validation_messages: string[];
- tasks_count: number;
- content_count: number;
- hub_pages: number;
- supporting_pages: number;
- attribute_pages: number;
- is_complete: boolean;
- }>;
- };
- taxonomy_coverage?: {
- total_taxonomies: number;
- defined_taxonomies: number;
- details: any[];
- };
- validation_flags?: {
- clusters_attached: boolean;
- taxonomies_defined: boolean;
- sitemap_generated: boolean;
- all_pages_generated: boolean;
- };
-}
-
-export async function fetchSiteProgress(blueprintId: number): Promise {
- return fetchAPI(`/v1/site-builder/blueprints/${blueprintId}/progress/`);
-}
+// Legacy: Site Builder API removed
+// SiteBlueprint, PageBlueprint, and related functions deprecated
// Stage 4: Sync Health API
export interface SyncStatus {
diff --git a/frontend/src/services/siteBuilder.api.ts b/frontend/src/services/siteBuilder.api.ts
deleted file mode 100644
index 01e6f6cd..00000000
--- a/frontend/src/services/siteBuilder.api.ts
+++ /dev/null
@@ -1,173 +0,0 @@
-/**
- * Site Builder API Service
- * Uses fetchAPI pattern (not axios) - handles authentication automatically
- */
-import { fetchAPI } from './api';
-import type {
- SiteBlueprint,
- PageBlueprint,
- SiteStructure,
- BuilderFormData,
- SiteBuilderMetadata,
-} 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;
- },
-
- /**
- * Load dropdown metadata for wizard fields
- */
- async getMetadata(): Promise {
- return fetchAPI('/v1/site-builder/metadata/');
- },
-
- /**
- * Delete a blueprint
- */
- async deleteBlueprint(id: number): Promise {
- return fetchAPI(`/v1/site-builder/blueprints/${id}/`, {
- method: 'DELETE',
- });
- },
-
- /**
- * Delete a page blueprint
- */
- async deletePage(id: number): Promise {
- return fetchAPI(`/v1/site-builder/pages/${id}/`, {
- method: 'DELETE',
- });
- },
-
- /**
- * Bulk delete blueprints
- */
- async bulkDeleteBlueprints(ids: number[]): Promise<{ deleted_count: number }> {
- return fetchAPI('/v1/site-builder/blueprints/bulk_delete/', {
- method: 'POST',
- body: JSON.stringify({ ids }),
- });
- },
-
- /**
- * Deploy a blueprint to Sites renderer
- */
- async deployBlueprint(blueprintId: number): Promise<{ success: boolean; deployment_url?: string; deployment_id?: number }> {
- // PublisherViewSet is now registered with empty prefix, so URL is /publisher/deploy/{id}/
- const response = await fetchAPI(`/v1/publisher/deploy/${blueprintId}/`, {
- method: 'POST',
- });
- // Handle unified response format
- return response?.data || response;
- },
-};
-
diff --git a/frontend/src/types/siteBuilder.ts b/frontend/src/types/siteBuilder.ts
deleted file mode 100644
index 146136a4..00000000
--- a/frontend/src/types/siteBuilder.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-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;
- sectorIds: number[];
- siteName: string;
- businessTypeId: number | null;
- businessType: string;
- customBusinessType?: string;
- industry: string;
- targetAudienceIds: number[];
- targetAudience: string;
- customTargetAudience?: string;
- hostingType: HostingType;
- businessBrief: string;
- objectives: string[];
- brandPersonalityIds: number[];
- customBrandPersonality?: string;
- heroImageryDirectionId: number | null;
- customHeroImageryDirection?: 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;
-}
-
-export interface SiteBuilderMetadataOption {
- id: number;
- name: string;
- description?: string;
-}
-
-export interface SiteBuilderMetadata {
- business_types: SiteBuilderMetadataOption[];
- audience_profiles: SiteBuilderMetadataOption[];
- brand_personalities: SiteBuilderMetadataOption[];
- hero_imagery_directions: SiteBuilderMetadataOption[];
-}
-
diff --git a/igny8-wp-plugin/sync/igny8-to-wp.php b/igny8-wp-plugin/sync/igny8-to-wp.php
index a3b2f1d7..7571a852 100644
--- a/igny8-wp-plugin/sync/igny8-to-wp.php
+++ b/igny8-wp-plugin/sync/igny8-to-wp.php
@@ -1167,36 +1167,51 @@ function igny8_cron_sync_from_igny8() {
* @param string $post_status WordPress post status
*/
function igny8_send_status_webhook($post_id, $content_data, $post_status) {
+ // Get site information for logging
+ $site_id = get_option('igny8_site_id', 'unknown');
+ $site_domain = parse_url(home_url(), PHP_URL_HOST);
+ $log_prefix = "[{$site_id}-{$site_domain}]";
+
+ Igny8_Logger::separator("{$log_prefix} π€ SENDING STATUS WEBHOOK TO IGNY8");
+
// Only send webhook if connection is enabled
if (!igny8_is_connection_enabled()) {
+ Igny8_Logger::warning("{$log_prefix} β οΈ Connection not enabled, skipping webhook", 'webhooks');
return;
}
// Get required data
$content_id = $content_data['content_id'] ?? get_post_meta($post_id, '_igny8_content_id', true);
if (!$content_id) {
- error_log('IGNY8: Cannot send status webhook - no content_id');
+ Igny8_Logger::error("{$log_prefix} β Cannot send status webhook - no content_id", 'webhooks');
return;
}
+ Igny8_Logger::info("{$log_prefix} Content ID: {$content_id}", 'webhooks');
+ Igny8_Logger::info("{$log_prefix} Post ID: {$post_id}", 'webhooks');
+ Igny8_Logger::info("{$log_prefix} Post Status: {$post_status}", 'webhooks');
+
// Get API endpoint from settings
$api = new Igny8API();
$api_base = $api->get_api_base();
if (!$api_base) {
- error_log('IGNY8: Cannot send status webhook - no API base URL');
+ Igny8_Logger::error("{$log_prefix} β Cannot send status webhook - no API base URL", 'webhooks');
return;
}
$webhook_url = rtrim($api_base, '/') . '/integration/webhooks/wordpress/status/';
+ Igny8_Logger::info("{$log_prefix} Webhook URL: {$webhook_url}", 'webhooks');
// Get API key
$api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key');
if (!$api_key) {
- error_log('IGNY8: Cannot send status webhook - no API key');
+ Igny8_Logger::error("{$log_prefix} β Cannot send status webhook - no API key", 'webhooks');
return;
}
+ Igny8_Logger::info("{$log_prefix} API Key: ***" . substr($api_key, -4), 'webhooks');
+
// Prepare webhook payload
$payload = array(
'post_id' => $post_id,
@@ -1207,7 +1222,10 @@ function igny8_send_status_webhook($post_id, $content_data, $post_status) {
'site_url' => get_site_url(),
);
+ Igny8_Logger::info("{$log_prefix} Payload: " . json_encode($payload), 'webhooks');
+
// Send webhook asynchronously
+ Igny8_Logger::info("{$log_prefix} Sending POST request to IGNY8...", 'webhooks');
$response = wp_remote_post($webhook_url, array(
'headers' => array(
'Content-Type' => 'application/json',
@@ -1219,9 +1237,11 @@ function igny8_send_status_webhook($post_id, $content_data, $post_status) {
));
if (is_wp_error($response)) {
- error_log('IGNY8: Status webhook failed: ' . $response->get_error_message());
+ Igny8_Logger::error("{$log_prefix} β Status webhook failed: " . $response->get_error_message(), 'webhooks');
} else {
- error_log("IGNY8: Status webhook sent for content {$content_id}, post {$post_id}, status {$post_status}");
+ Igny8_Logger::info("{$log_prefix} β
Status webhook sent successfully for content {$content_id}, post {$post_id}, status {$post_status}", 'webhooks');
}
+
+ Igny8_Logger::separator("{$log_prefix} β
WEBHOOK SEND COMPLETED");
}