# Sites Integration Plan - Content Types Structure **Date**: November 22, 2025 **Status**: 📋 **PLANNING** --- ## Overview Integrate the new unified content architecture (ContentTaxonomy, ContentAttribute, entity_type, content_format) with the Sites module and SiteIntegration model to enable WordPress content type discovery, configuration, and sync. --- ## Current State Analysis ### ✅ What We Have **1. Unified Content Architecture (COMPLETE)** - `Content` model with `entity_type`, `content_format`, `cluster_role` - `ContentTaxonomy` model for categories, tags, product attributes - `ContentAttribute` model for product specs, service modifiers - WordPress sync fields (`external_id`, `external_taxonomy`, `sync_status`) **2. Site Model** - Basic site information (name, domain, industry) - `site_type` field (marketing, ecommerce, blog, portfolio, corporate) - `hosting_type` field (igny8_sites, wordpress, shopify, multi) - Legacy WP fields (`wp_url`, `wp_username`, `wp_api_key`) **3. SiteIntegration Model** - Platform-specific integrations (wordpress, shopify, custom) - `config_json` for configuration - `credentials_json` for API keys/tokens - `sync_enabled` flag for two-way sync **4. WordPress Plugin** - `/wp-json/igny8/v1/site-metadata/` endpoint - Returns post types, taxonomies, and counts - API key authentication support ### ❌ What's Missing 1. **Content Type Configuration Storage** - No place to store which post types/taxonomies are enabled - No fetch limits per content type - No sync preferences per taxonomy 2. **Site → Integration Connection** - No clear link between Site.site_type and available content types - No mapping of WP post types to IGNY8 entity types 3. **Frontend UI** - No "Content Types" tab in Site Settings - No interface to enable/disable content types - No way to set fetch limits 4. **Backend Service Methods** - No method to fetch WP structure and store in `config_json` - No method to import taxonomies - No method to import content titles --- ## Proposed Solution ### Phase 1: Extend SiteIntegration.config_json Structure Store WordPress content type configuration in `SiteIntegration.config_json`: ```json { "url": "https://example.com", "api_version": "v1", "plugin_version": "1.0.0", "content_types": { "post_types": { "post": { "label": "Posts", "count": 123, "enabled": true, "fetch_limit": 100, "entity_type": "post", "content_format": "article", "last_synced": "2025-11-22T10:00:00Z" }, "page": { "label": "Pages", "count": 12, "enabled": true, "fetch_limit": 50, "entity_type": "page", "content_format": null, "last_synced": null }, "product": { "label": "Products", "count": 456, "enabled": true, "fetch_limit": 200, "entity_type": "product", "content_format": null, "last_synced": null } }, "taxonomies": { "category": { "label": "Categories", "count": 25, "enabled": true, "fetch_limit": 100, "taxonomy_type": "category", "last_synced": "2025-11-22T10:00:00Z" }, "post_tag": { "label": "Tags", "count": 102, "enabled": true, "fetch_limit": 200, "taxonomy_type": "tag", "last_synced": null }, "product_cat": { "label": "Product Categories", "count": 15, "enabled": true, "fetch_limit": 50, "taxonomy_type": "product_cat", "last_synced": null }, "pa_color": { "label": "Color", "count": 10, "enabled": true, "fetch_limit": 50, "taxonomy_type": "product_attr", "attribute_name": "Color", "last_synced": null } } }, "plugin_connection_enabled": true, "two_way_sync_enabled": true, "last_structure_fetch": "2025-11-22T10:00:00Z" } ``` --- ### Phase 2: Backend Service Methods #### 1. **IntegrationService.fetch_content_structure()** ```python def fetch_content_structure(self, integration_id: int) -> Dict[str, Any]: """ Fetch content structure from WordPress plugin and store in config_json. Steps: 1. GET /wp-json/igny8/v1/site-metadata/ 2. Parse response 3. Update integration.config_json['content_types'] 4. Return structure """ integration = SiteIntegration.objects.get(id=integration_id) # Call WordPress plugin wp_url = integration.config_json.get('url') api_key = integration.credentials_json.get('api_key') response = requests.get( f"{wp_url}/wp-json/igny8/v1/site-metadata/", headers={'X-IGNY8-API-KEY': api_key} ) if response.status_code == 200: data = response.json()['data'] # Transform to our structure content_types = { 'post_types': {}, 'taxonomies': {} } # Map post types for wp_type, info in data['post_types'].items(): content_types['post_types'][wp_type] = { 'label': info['label'], 'count': info['count'], 'enabled': False, # Default disabled 'fetch_limit': 100, # Default limit 'entity_type': self._map_wp_type_to_entity(wp_type), 'content_format': None, 'last_synced': None } # Map taxonomies for wp_tax, info in data['taxonomies'].items(): content_types['taxonomies'][wp_tax] = { 'label': info['label'], 'count': info['count'], 'enabled': False, # Default disabled 'fetch_limit': 100, # Default limit 'taxonomy_type': self._map_wp_tax_to_type(wp_tax), 'last_synced': None } # Update config if 'content_types' not in integration.config_json: integration.config_json['content_types'] = {} integration.config_json['content_types'] = content_types integration.config_json['last_structure_fetch'] = timezone.now().isoformat() integration.save() return content_types else: raise Exception(f"Failed to fetch structure: {response.status_code}") def _map_wp_type_to_entity(self, wp_type: str) -> str: """Map WordPress post type to IGNY8 entity_type""" mapping = { 'post': 'post', 'page': 'page', 'product': 'product', 'service': 'service', } return mapping.get(wp_type, 'post') def _map_wp_tax_to_type(self, wp_tax: str) -> str: """Map WordPress taxonomy to ContentTaxonomy type""" mapping = { 'category': 'category', 'post_tag': 'tag', 'product_cat': 'product_cat', 'product_tag': 'product_tag', } # Product attributes start with pa_ if wp_tax.startswith('pa_'): return 'product_attr' return mapping.get(wp_tax, 'category') ``` #### 2. **IntegrationService.import_taxonomies()** ```python def import_taxonomies( self, integration_id: int, taxonomy_type: str = None, limit: int = None ) -> int: """ Import taxonomy terms from WordPress to ContentTaxonomy. Args: integration_id: SiteIntegration ID taxonomy_type: Specific taxonomy to import (e.g., 'category', 'post_tag') limit: Max terms to import per taxonomy Returns: Number of terms imported """ integration = SiteIntegration.objects.get(id=integration_id) site = integration.site # Get enabled taxonomies from config content_types = integration.config_json.get('content_types', {}) taxonomies = content_types.get('taxonomies', {}) imported_count = 0 for wp_tax, config in taxonomies.items(): # Skip if not enabled or not requested if not config.get('enabled'): continue if taxonomy_type and wp_tax != taxonomy_type: continue # Fetch from WordPress fetch_limit = limit or config.get('fetch_limit', 100) wp_url = integration.config_json.get('url') api_key = integration.credentials_json.get('api_key') # Map taxonomy endpoint endpoint = self._get_wp_taxonomy_endpoint(wp_tax) response = requests.get( f"{wp_url}/wp-json/wp/v2/{endpoint}?per_page={fetch_limit}", headers={'X-IGNY8-API-KEY': api_key} ) if response.status_code == 200: terms = response.json() for term in terms: # Create or update ContentTaxonomy taxonomy, created = ContentTaxonomy.objects.update_or_create( site=site, external_id=term['id'], external_taxonomy=wp_tax, defaults={ 'name': term['name'], 'slug': term['slug'], 'taxonomy_type': config['taxonomy_type'], 'description': term.get('description', ''), 'count': term.get('count', 0), 'sync_status': 'imported', 'account': site.account, 'sector': site.sectors.first(), # Default to first sector } ) if created: imported_count += 1 # Update last_synced config['last_synced'] = timezone.now().isoformat() integration.save() return imported_count def _get_wp_taxonomy_endpoint(self, wp_tax: str) -> str: """Get WordPress REST endpoint for taxonomy""" mapping = { 'category': 'categories', 'post_tag': 'tags', 'product_cat': 'products/categories', 'product_tag': 'products/tags', } # Product attributes if wp_tax.startswith('pa_'): attr_id = wp_tax.replace('pa_', '') return f'products/attributes/{attr_id}/terms' return mapping.get(wp_tax, wp_tax) ``` #### 3. **IntegrationService.import_content_titles()** ```python def import_content_titles( self, integration_id: int, post_type: str = None, limit: int = None ) -> int: """ Import content titles (not full content) from WordPress. Args: integration_id: SiteIntegration ID post_type: Specific post type to import (e.g., 'post', 'product') limit: Max items to import per type Returns: Number of content items imported """ integration = SiteIntegration.objects.get(id=integration_id) site = integration.site # Get enabled post types from config content_types = integration.config_json.get('content_types', {}) post_types = content_types.get('post_types', {}) imported_count = 0 for wp_type, config in post_types.items(): # Skip if not enabled or not requested if not config.get('enabled'): continue if post_type and wp_type != post_type: continue # Fetch from WordPress fetch_limit = limit or config.get('fetch_limit', 100) wp_url = integration.config_json.get('url') api_key = integration.credentials_json.get('api_key') # Determine endpoint endpoint = 'products' if wp_type == 'product' else wp_type + 's' response = requests.get( f"{wp_url}/wp-json/wp/v2/{endpoint}?per_page={fetch_limit}", headers={'X-IGNY8-API-KEY': api_key} ) if response.status_code == 200: items = response.json() for item in items: # Create or update Content (title only, no html_content yet) content, created = Content.objects.update_or_create( site=site, external_id=item['id'], external_type=wp_type, defaults={ 'title': item['title']['rendered'] if isinstance(item['title'], dict) else item['title'], 'entity_type': config['entity_type'], 'content_format': config.get('content_format'), 'external_url': item.get('link', ''), 'source': 'wordpress', 'sync_status': 'imported', 'account': site.account, 'sector': site.sectors.first(), } ) # Map taxonomies if 'categories' in item: for cat_id in item['categories']: try: taxonomy = ContentTaxonomy.objects.get( site=site, external_id=cat_id, taxonomy_type='category' ) content.taxonomies.add(taxonomy) except ContentTaxonomy.DoesNotExist: pass if 'tags' in item: for tag_id in item['tags']: try: taxonomy = ContentTaxonomy.objects.get( site=site, external_id=tag_id, taxonomy_type='tag' ) content.taxonomies.add(taxonomy) except ContentTaxonomy.DoesNotExist: pass if created: imported_count += 1 # Update last_synced config['last_synced'] = timezone.now().isoformat() integration.save() return imported_count ``` --- ### Phase 3: Backend API Endpoints Add new actions to `IntegrationViewSet`: ```python @action(detail=True, methods=['post'], url_path='fetch-structure') def fetch_structure(self, request, pk=None): """ POST /api/v1/integration/integrations/{id}/fetch-structure/ Fetch content type structure from WordPress and store in config. """ integration = self.get_object() service = IntegrationService() try: structure = service.fetch_content_structure(integration.id) return success_response( data=structure, message="Content structure fetched successfully", request=request ) except Exception as e: return error_response( error=str(e), status_code=status.HTTP_400_BAD_REQUEST, request=request ) @action(detail=True, methods=['post'], url_path='import-taxonomies') def import_taxonomies(self, request, pk=None): """ POST /api/v1/integration/integrations/{id}/import-taxonomies/ { "taxonomy_type": "category", // optional "limit": 100 // optional } Import taxonomy terms from WordPress. """ integration = self.get_object() service = IntegrationService() taxonomy_type = request.data.get('taxonomy_type') limit = request.data.get('limit') try: count = service.import_taxonomies(integration.id, taxonomy_type, limit) return success_response( data={'imported_count': count}, message=f"Imported {count} taxonomy terms", request=request ) except Exception as e: return error_response( error=str(e), status_code=status.HTTP_400_BAD_REQUEST, request=request ) @action(detail=True, methods=['post'], url_path='import-content') def import_content(self, request, pk=None): """ POST /api/v1/integration/integrations/{id}/import-content/ { "post_type": "post", // optional "limit": 100 // optional } Import content titles from WordPress. """ integration = self.get_object() service = IntegrationService() post_type = request.data.get('post_type') limit = request.data.get('limit') try: count = service.import_content_titles(integration.id, post_type, limit) return success_response( data={'imported_count': count}, message=f"Imported {count} content items", request=request ) except Exception as e: return error_response( error=str(e), status_code=status.HTTP_400_BAD_REQUEST, request=request ) @action(detail=True, methods=['patch'], url_path='update-content-types') def update_content_types(self, request, pk=None): """ PATCH /api/v1/integration/integrations/{id}/update-content-types/ { "post_types": { "post": {"enabled": true, "fetch_limit": 200} }, "taxonomies": { "category": {"enabled": true, "fetch_limit": 150} } } Update content type configuration. """ integration = self.get_object() post_types = request.data.get('post_types', {}) taxonomies = request.data.get('taxonomies', {}) # Update config if 'content_types' not in integration.config_json: integration.config_json['content_types'] = {'post_types': {}, 'taxonomies': {}} for wp_type, updates in post_types.items(): if wp_type in integration.config_json['content_types']['post_types']: integration.config_json['content_types']['post_types'][wp_type].update(updates) for wp_tax, updates in taxonomies.items(): if wp_tax in integration.config_json['content_types']['taxonomies']: integration.config_json['content_types']['taxonomies'][wp_tax].update(updates) integration.save() return success_response( data=integration.config_json['content_types'], message="Content types configuration updated", request=request ) ``` --- ### Phase 4: Frontend UI - "Content Types" Tab **Location:** Site Settings → Content Types **Features:** 1. Display fetched content types from `config_json` 2. Enable/disable toggles per type 3. Fetch limit inputs 4. Last synced timestamps 5. Sync buttons (Fetch Structure, Import Taxonomies, Import Content) **API Calls:** ```javascript // Fetch structure POST /api/v1/integration/integrations/{id}/fetch-structure/ // Update configuration PATCH /api/v1/integration/integrations/{id}/update-content-types/ { "post_types": { "post": {"enabled": true, "fetch_limit": 200} } } // Import taxonomies POST /api/v1/integration/integrations/{id}/import-taxonomies/ // Import content POST /api/v1/integration/integrations/{id}/import-content/ ``` --- ## Implementation Steps ### Step 1: Backend Service Methods ✅ READY TO IMPLEMENT - [ ] Add `fetch_content_structure()` to IntegrationService - [ ] Add `import_taxonomies()` to IntegrationService - [ ] Add `import_content_titles()` to IntegrationService - [ ] Add helper methods for WP type mapping ### Step 2: Backend API Endpoints ✅ READY TO IMPLEMENT - [ ] Add `fetch_structure` action to IntegrationViewSet - [ ] Add `import_taxonomies` action to IntegrationViewSet - [ ] Add `import_content` action to IntegrationViewSet - [ ] Add `update_content_types` action to IntegrationViewSet ### Step 3: Frontend UI ⏳ PENDING - [ ] Create "Content Types" tab component - [ ] Add post types list with toggles - [ ] Add taxonomies list with toggles - [ ] Add fetch limit inputs - [ ] Add sync buttons - [ ] Add last synced timestamps ### Step 4: Testing ⏳ PENDING - [ ] Test structure fetch from WP plugin - [ ] Test taxonomy import - [ ] Test content title import - [ ] Test configuration updates - [ ] Test UI interactions --- ## Migration Status ### ✅ Database Ready - All tables exist - All fields exist - All migrations applied ### ✅ Models Ready - ContentTaxonomy model complete - ContentAttribute model complete - Content model enhanced - SiteIntegration model ready ### ✅ Admin Ready - All admin interfaces updated - All filters configured ### ⏳ Services Pending - IntegrationService methods need implementation ### ⏳ API Endpoints Pending - IntegrationViewSet actions need implementation ### ⏳ Frontend Pending - Content Types tab needs creation --- ## Next Actions **IMMEDIATE:** 1. Implement IntegrationService methods (fetch_structure, import_taxonomies, import_content_titles) 2. Add API endpoints to IntegrationViewSet 3. Test with WordPress plugin **SOON:** 4. Create frontend "Content Types" tab 5. Test end-to-end workflow 6. Add AI semantic mapping endpoint --- ## Summary **We are going in the RIGHT direction!** ✅ The unified content architecture is complete and production-ready. Now we need to: 1. **Store WP structure** in `SiteIntegration.config_json` 2. **Add service methods** to fetch and import from WP 3. **Add API endpoints** for frontend to trigger imports 4. **Build frontend UI** to manage content types The deleted migration file was incorrect (wrong location, wrong approach). The correct approach is to use `SiteIntegration.config_json` to store content type configuration, not database migrations. **Status: Ready to implement backend service methods!**