--- title: Architecture description: Technical architecture of the core-content package updated: 2026-01-29 --- # Architecture The `core-content` package provides headless CMS functionality for the Host UK platform. It handles content management, AI-powered generation, revision history, webhooks for external CMS integration, and search capabilities. ## Package Overview **Namespace:** `Core\Mod\Content\` **Entry Point:** `Boot.php` (Laravel Service Provider) **Dependencies:** - `core-php` (Foundation framework, events) - `core-tenant` (Workspaces, users, entitlements) - Optional: `core-agentic` (AI services for content generation) - Optional: `core-mcp` (MCP tool handlers) ## Directory Structure ``` core-content/ ├── Boot.php # Service provider with event listeners ├── config.php # Package configuration ├── Models/ # Eloquent models (10 models) ├── Services/ # Business logic services ├── Controllers/ # API and web controllers │ └── Api/ # REST API controllers ├── Jobs/ # Queue jobs ├── Mcp/ # MCP tool handlers │ └── Handlers/ # Individual MCP tools ├── Concerns/ # Traits ├── Console/ # Artisan commands │ └── Commands/ # Command implementations ├── Enums/ # PHP enums ├── Migrations/ # Database migrations ├── Observers/ # Model observers ├── routes/ # Route definitions ├── View/ # Livewire components and Blade views │ ├── Modal/ # Livewire components │ └── Blade/ # Blade templates ├── tests/ # Test suite └── docs/ # Documentation ``` ## Core Concepts ### Content Items The primary content model. Supports multiple content types and sources: ```php // Content types (where content originates) enum ContentType: string { case NATIVE = 'native'; // Created in Host Hub editor case HOSTUK = 'hostuk'; // Alias for native (backwards compat) case SATELLITE = 'satellite'; // Per-service content case WORDPRESS = 'wordpress'; // Legacy synced content } ``` Content items belong to workspaces and have: - Title, slug, excerpt, content (HTML/Markdown/JSON) - Status (draft, publish, future, private, pending) - Author and last editor tracking - Revision history - Taxonomy (categories, tags) - SEO metadata - Preview tokens for sharing unpublished content - CDN cache invalidation tracking ### Content Briefs Briefs drive AI-powered content generation. They define what content to create: ```php // Brief content types (what to generate) enum BriefContentType: string { case HELP_ARTICLE = 'help_article'; // Documentation case BLOG_POST = 'blog_post'; // Blog articles case LANDING_PAGE = 'landing_page'; // Marketing pages case SOCIAL_POST = 'social_post'; // Social media } ``` Brief workflow: `pending` -> `queued` -> `generating` -> `review` -> `published` ### Revisions Every content change creates an immutable revision snapshot. Revisions support: - Change type tracking (edit, autosave, restore, publish) - Word/character count tracking - Side-by-side diff comparison with LCS algorithm - Configurable retention policies (max count, max age) ## Service Layer ### AIGatewayService Orchestrates two-stage AI content generation: 1. **Stage 1: Draft (Gemini)** - Fast, cost-effective initial generation 2. **Stage 2: Refine (Claude)** - Quality refinement and brand voice alignment ```php $gateway = app(AIGatewayService::class); // Two-stage pipeline $result = $gateway->generateAndRefine($brief); // Or individual stages $draft = $gateway->generateDraft($brief); $refined = $gateway->refineDraft($brief, $draftContent); // Direct Claude generation (skip Gemini) $content = $gateway->generateDirect($brief); ``` ### ContentSearchService Full-text search with multiple backend support: ```php // Backends (configured via CONTENT_SEARCH_BACKEND) const BACKEND_DATABASE = 'database'; // LIKE queries with relevance const BACKEND_SCOUT_DATABASE = 'scout_database'; // Laravel Scout const BACKEND_MEILISEARCH = 'meilisearch'; // Laravel Scout + Meilisearch ``` Features: - Relevance scoring (title > slug > excerpt > content) - Filters: type, status, category, tag, date range, content_type - Autocomplete suggestions - Re-indexing support for Scout backends ### WebhookRetryService Handles failed webhook processing with exponential backoff: ``` Retry intervals: 1m, 5m, 15m, 1h, 4h Max retries: 5 (configurable per webhook) ``` ### ContentRender Public-facing content renderer with caching: - Homepage, blog listing, post, page rendering - Cache TTL: 1 hour production, 1 minute development - Cache key sanitisation for special characters ### CdnPurgeService CDN cache invalidation via Bunny CDN: - Triggered by ContentItemObserver on publish/update - URL-based and tag-based purging - Workspace-level cache clearing ## Event-Driven Architecture The package uses the event-driven module loading pattern from `core-php`: ```php class Boot extends ServiceProvider { public static array $listens = [ WebRoutesRegistering::class => 'onWebRoutes', ApiRoutesRegistering::class => 'onApiRoutes', ConsoleBooting::class => 'onConsole', McpToolsRegistering::class => 'onMcpTools', ]; } ``` Handlers register: - **Web Routes:** Public blog, help pages, content preview - **API Routes:** REST API for briefs, media, search, generation - **Console:** Artisan commands for scheduling, pruning - **MCP Tools:** AI agent content management tools ## API Structure ### Authenticated Endpoints (Session or API Key) ``` # Content Briefs GET /api/content/briefs # List briefs POST /api/content/briefs # Create brief GET /api/content/briefs/{id} # Get brief PUT /api/content/briefs/{id} # Update brief DELETE /api/content/briefs/{id} # Delete brief POST /api/content/briefs/bulk # Bulk create GET /api/content/briefs/next # Next ready for processing # AI Generation (rate limited: 10/min) POST /api/content/generate/draft # Generate draft (Gemini) POST /api/content/generate/refine # Refine draft (Claude) POST /api/content/generate/full # Full pipeline POST /api/content/generate/social # Social posts from content # Content Search (rate limited: 60/min) GET /api/content/search # Full-text search GET /api/content/search/suggest # Autocomplete GET /api/content/search/info # Backend info POST /api/content/search/reindex # Trigger re-index # Revisions GET /api/content/items/{id}/revisions # List revisions GET /api/content/revisions/{id} # Get revision POST /api/content/revisions/{id}/restore # Restore revision GET /api/content/revisions/{id}/compare/{other} # Compare # Preview POST /api/content/items/{id}/preview/generate # Generate preview link DELETE /api/content/items/{id}/preview/revoke # Revoke preview link ``` ### Public Endpoints ``` # Webhooks (signature verified, no auth) POST /api/content/webhooks/{endpoint} # Receive external webhooks # Web Routes GET /blog # Blog listing GET /blog/{slug} # Blog post GET /help # Help centre GET /help/{slug} # Help article GET /content/preview/{id} # Preview content ``` ## Rate Limiting Defined in `Boot::configureRateLimiting()`: | Limiter | Authenticated | Unauthenticated | |---------|---------------|-----------------| | `content-generate` | 10/min per user/workspace | 2/min per IP | | `content-briefs` | 30/min per user | 5/min per IP | | `content-webhooks` | 60/min per endpoint | 30/min per IP | | `content-search` | Configurable (default 60/min) | 20/min per IP | ## MCP Tools Seven MCP tools for AI agent integration: | Tool | Description | |------|-------------| | `content_list` | List content items with filters | | `content_read` | Read content by ID or slug | | `content_search` | Full-text search | | `content_create` | Create new content | | `content_update` | Update existing content | | `content_delete` | Soft delete content | | `content_taxonomies` | List categories and tags | All tools: - Require workspace resolution - Check entitlements (`content.mcp_access`, `content.items`) - Log actions to MCP session - Return structured responses ## Data Flow ### Content Creation via MCP ``` Agent Request ↓ ContentCreateHandler::handle() ↓ resolveWorkspace() → Workspace model ↓ checkEntitlement() → EntitlementService ↓ ContentItem::create() ↓ createRevision() → ContentRevision ↓ recordUsage() → EntitlementService ↓ Response with content ID ``` ### Webhook Processing ``` External CMS ↓ POST /api/content/webhooks/{endpoint} ↓ ContentWebhookController::receive() ↓ Verify signature → ContentWebhookEndpoint::verifySignature() ↓ Check type allowed → ContentWebhookEndpoint::isTypeAllowed() ↓ Create ContentWebhookLog ↓ Dispatch ProcessContentWebhook job ↓ Job::handle() ↓ Process based on event type (wordpress.*, cms.*, generic.*) ↓ Create/Update/Delete ContentItem ↓ Mark log completed ``` ### AI Generation Pipeline ``` ContentBrief ↓ GenerateContentJob dispatched ↓ Stage 1: AIGatewayService::generateDraft() ↓ GeminiService::generate() → Draft content ↓ Brief::markDraftComplete() ↓ Stage 2: AIGatewayService::refineDraft() ↓ ClaudeService::generate() → Refined content ↓ Brief::markRefined() ↓ AIUsage records created for each stage ``` ## Configuration Key settings in `config.php`: ```php return [ 'generation' => [ 'default_timeout' => env('CONTENT_GENERATION_TIMEOUT', 300), 'timeouts' => [ 'help_article' => 180, 'blog_post' => 240, 'landing_page' => 300, 'social_post' => 60, ], 'max_retries' => 3, 'backoff' => [30, 60, 120], ], 'revisions' => [ 'max_per_item' => env('CONTENT_MAX_REVISIONS', 50), 'max_age_days' => 180, 'preserve_published' => true, ], 'cache' => [ 'ttl' => env('CONTENT_CACHE_TTL', 3600), 'prefix' => 'content:render', ], 'search' => [ 'backend' => env('CONTENT_SEARCH_BACKEND', 'database'), 'min_query_length' => 2, 'max_per_page' => 50, 'default_per_page' => 20, 'rate_limit' => 60, ], ]; ``` ## Database Schema ### Primary Tables | Table | Purpose | |-------|---------| | `content_items` | Content storage (posts, pages) | | `content_revisions` | Version history | | `content_taxonomies` | Categories and tags | | `content_item_taxonomy` | Pivot table | | `content_media` | Media attachments | | `content_authors` | Author profiles | | `content_briefs` | AI generation briefs | | `content_tasks` | Scheduled content tasks | | `content_webhook_endpoints` | Webhook configurations | | `content_webhook_logs` | Webhook processing logs | | `ai_usage` | AI API usage tracking | | `prompts` | AI prompt templates | | `prompt_versions` | Prompt version history | ### Key Indexes - `content_items`: Composite indexes on `(workspace_id, slug, type)`, `(workspace_id, status, type)`, `(workspace_id, status, content_type)` - `content_revisions`: Index on `(content_item_id, revision_number)` - `content_webhook_logs`: Index on `(workspace_id, status)`, `(status, created_at)` ## Extension Points ### Adding New Content Types 1. Add value to `ContentType` enum 2. Update `ContentType::isNative()` if applicable 3. Add any type-specific scopes to `ContentItem` ### Adding New AI Generation Types 1. Add value to `BriefContentType` enum 2. Add timeout to `config.php` generation.timeouts 3. Add prompt in `AIGatewayService::getDraftSystemPrompt()` ### Adding New Webhook Event Types 1. Add to `ContentWebhookEndpoint::ALLOWED_TYPES` 2. Add handler in `ProcessContentWebhook::processWordPress()` or `processCms()` 3. Add event type mapping in `ContentWebhookController::normaliseEventType()` ### Adding New MCP Tools 1. Create handler in `Mcp/Handlers/` implementing `McpToolHandler` 2. Define `schema()` with tool name, description, input schema 3. Implement `handle()` with workspace resolution and entitlement checks 4. Register in `Boot::onMcpTools()`