diff --git a/Boot.php b/Boot.php index a0d54c0..80a6173 100644 --- a/Boot.php +++ b/Boot.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content; +namespace Core\Mod\Content; use Core\Events\ApiRoutesRegistering; use Core\Events\ConsoleBooting; diff --git a/Concerns/SearchableContent.php b/Concerns/SearchableContent.php index 964fd7a..f0d7b76 100644 --- a/Concerns/SearchableContent.php +++ b/Concerns/SearchableContent.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Concerns; +namespace Core\Mod\Content\Concerns; /** * Trait for making ContentItem searchable with Laravel Scout. diff --git a/Console/Commands/ContentBatch.php b/Console/Commands/ContentBatch.php index b5a7fc0..9088e91 100644 --- a/Console/Commands/ContentBatch.php +++ b/Console/Commands/ContentBatch.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Console\Commands; +namespace Core\Mod\Content\Console\Commands; use Mod\Agentic\Services\ContentService; use Illuminate\Console\Command; diff --git a/Console/Commands/ContentGenerate.php b/Console/Commands/ContentGenerate.php index c125ab9..0ac63eb 100644 --- a/Console/Commands/ContentGenerate.php +++ b/Console/Commands/ContentGenerate.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Console\Commands; +namespace Core\Mod\Content\Console\Commands; use Mod\Agentic\Services\ContentService; use Illuminate\Console\Command; diff --git a/Console/Commands/ContentImportWordPress.php b/Console/Commands/ContentImportWordPress.php index 696765d..8e139f7 100644 --- a/Console/Commands/ContentImportWordPress.php +++ b/Console/Commands/ContentImportWordPress.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace Core\Content\Console\Commands; +namespace Core\Mod\Content\Console\Commands; -use Core\Content\Enums\ContentType; -use Core\Content\Models\ContentAuthor; -use Core\Content\Models\ContentItem; -use Core\Content\Models\ContentMedia; -use Core\Content\Models\ContentTaxonomy; +use Core\Mod\Content\Enums\ContentType; +use Core\Mod\Content\Models\ContentAuthor; +use Core\Mod\Content\Models\ContentItem; +use Core\Mod\Content\Models\ContentMedia; +use Core\Mod\Content\Models\ContentTaxonomy; use Core\Mod\Tenant\Models\Workspace; use Carbon\Carbon; use Illuminate\Console\Command; diff --git a/Console/Commands/ContentValidate.php b/Console/Commands/ContentValidate.php index 90cc1fd..7b124e1 100644 --- a/Console/Commands/ContentValidate.php +++ b/Console/Commands/ContentValidate.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Console\Commands; +namespace Core\Mod\Content\Console\Commands; use Mod\Agentic\Services\ContentService; use Illuminate\Console\Command; diff --git a/Console/Commands/ProcessPendingWebhooks.php b/Console/Commands/ProcessPendingWebhooks.php index 36713e5..67be26e 100644 --- a/Console/Commands/ProcessPendingWebhooks.php +++ b/Console/Commands/ProcessPendingWebhooks.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Core\Content\Console\Commands; +namespace Core\Mod\Content\Console\Commands; use Illuminate\Console\Command; use Illuminate\Support\Facades\Log; -use Core\Content\Services\WebhookRetryService; +use Core\Mod\Content\Services\WebhookRetryService; /** * ProcessPendingWebhooks diff --git a/Console/Commands/PruneContentRevisions.php b/Console/Commands/PruneContentRevisions.php index b9514e4..966df45 100644 --- a/Console/Commands/PruneContentRevisions.php +++ b/Console/Commands/PruneContentRevisions.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Core\Content\Console\Commands; +namespace Core\Mod\Content\Console\Commands; use Illuminate\Console\Command; -use Core\Content\Models\ContentRevision; +use Core\Mod\Content\Models\ContentRevision; /** * Prune old content revisions based on retention policy. diff --git a/Console/Commands/PublishScheduledContent.php b/Console/Commands/PublishScheduledContent.php index cf5eee0..b6a0998 100644 --- a/Console/Commands/PublishScheduledContent.php +++ b/Console/Commands/PublishScheduledContent.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Core\Content\Console\Commands; +namespace Core\Mod\Content\Console\Commands; use Illuminate\Console\Command; use Illuminate\Support\Facades\Log; -use Core\Content\Models\ContentItem; +use Core\Mod\Content\Models\ContentItem; /** * PublishScheduledContent diff --git a/Controllers/Api/ContentBriefController.php b/Controllers/Api/ContentBriefController.php index 26334c0..2c353fb 100644 --- a/Controllers/Api/ContentBriefController.php +++ b/Controllers/Api/ContentBriefController.php @@ -2,15 +2,15 @@ declare(strict_types=1); -namespace Core\Content\Controllers\Api; +namespace Core\Mod\Content\Controllers\Api; use Core\Front\Controller; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Core\Mod\Api\Concerns\HasApiResponses; use Core\Mod\Api\Concerns\ResolvesWorkspace; -use Core\Content\Models\ContentBrief; -use Core\Content\Resources\ContentBriefResource; +use Core\Mod\Content\Models\ContentBrief; +use Core\Mod\Content\Resources\ContentBriefResource; /** * Content Brief API Controller diff --git a/Controllers/Api/ContentMediaController.php b/Controllers/Api/ContentMediaController.php index c03adb9..8a95787 100644 --- a/Controllers/Api/ContentMediaController.php +++ b/Controllers/Api/ContentMediaController.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Controllers\Api; +namespace Core\Mod\Content\Controllers\Api; use Core\Front\Controller; use Core\Mod\Api\Concerns\HasApiResponses; @@ -11,7 +11,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; -use Core\Content\Models\ContentMedia; +use Core\Mod\Content\Models\ContentMedia; /** * Content Media API Controller diff --git a/Controllers/Api/ContentRevisionController.php b/Controllers/Api/ContentRevisionController.php index 840c2f1..eb44a36 100644 --- a/Controllers/Api/ContentRevisionController.php +++ b/Controllers/Api/ContentRevisionController.php @@ -2,16 +2,16 @@ declare(strict_types=1); -namespace Core\Content\Controllers\Api; +namespace Core\Mod\Content\Controllers\Api; use Core\Front\Controller; use Core\Mod\Api\Concerns\HasApiResponses; use Core\Mod\Api\Concerns\ResolvesWorkspace; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Core\Content\Models\ContentItem; -use Core\Content\Models\ContentRevision; -use Core\Content\Resources\ContentRevisionResource; +use Core\Mod\Content\Models\ContentItem; +use Core\Mod\Content\Models\ContentRevision; +use Core\Mod\Content\Resources\ContentRevisionResource; /** * Content Revision API Controller diff --git a/Controllers/Api/ContentSearchController.php b/Controllers/Api/ContentSearchController.php index 70b2c5b..43abf2e 100644 --- a/Controllers/Api/ContentSearchController.php +++ b/Controllers/Api/ContentSearchController.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace Core\Content\Controllers\Api; +namespace Core\Mod\Content\Controllers\Api; use Core\Front\Controller; use Core\Mod\Api\Concerns\HasApiResponses; use Core\Mod\Api\Concerns\ResolvesWorkspace; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Core\Content\Services\ContentSearchService; +use Core\Mod\Content\Services\ContentSearchService; /** * Content Search API Controller diff --git a/Controllers/Api/ContentWebhookController.php b/Controllers/Api/ContentWebhookController.php index 74e8f07..d87eeb8 100644 --- a/Controllers/Api/ContentWebhookController.php +++ b/Controllers/Api/ContentWebhookController.php @@ -2,15 +2,15 @@ declare(strict_types=1); -namespace Core\Content\Controllers\Api; +namespace Core\Mod\Content\Controllers\Api; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Routing\Controller; use Illuminate\Support\Facades\Log; -use Core\Content\Jobs\ProcessContentWebhook; -use Core\Content\Models\ContentWebhookEndpoint; -use Core\Content\Models\ContentWebhookLog; +use Core\Mod\Content\Jobs\ProcessContentWebhook; +use Core\Mod\Content\Models\ContentWebhookEndpoint; +use Core\Mod\Content\Models\ContentWebhookLog; /** * Controller for receiving external content webhooks. diff --git a/Controllers/Api/GenerationController.php b/Controllers/Api/GenerationController.php index 381276b..0f881d5 100644 --- a/Controllers/Api/GenerationController.php +++ b/Controllers/Api/GenerationController.php @@ -2,18 +2,18 @@ declare(strict_types=1); -namespace Core\Content\Controllers\Api; +namespace Core\Mod\Content\Controllers\Api; use Core\Front\Controller; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Core\Mod\Api\Concerns\HasApiResponses; use Core\Mod\Api\Concerns\ResolvesWorkspace; -use Core\Content\Jobs\GenerateContentJob; -use Core\Content\Models\AIUsage; -use Core\Content\Models\ContentBrief; -use Core\Content\Resources\ContentBriefResource; -use Core\Content\Services\AIGatewayService; +use Core\Mod\Content\Jobs\GenerateContentJob; +use Core\Mod\Content\Models\AIUsage; +use Core\Mod\Content\Models\ContentBrief; +use Core\Mod\Content\Resources\ContentBriefResource; +use Core\Mod\Content\Services\AIGatewayService; /** * Content Generation API Controller diff --git a/Controllers/ContentPreviewController.php b/Controllers/ContentPreviewController.php index 2dc201d..56db255 100644 --- a/Controllers/ContentPreviewController.php +++ b/Controllers/ContentPreviewController.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace Core\Content\Controllers; +namespace Core\Mod\Content\Controllers; use Core\Mod\Tenant\Models\Workspace; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Routing\Controller; -use Core\Content\Models\ContentItem; +use Core\Mod\Content\Models\ContentItem; /** * ContentPreviewController - Preview draft content before publishing. diff --git a/Enums/BriefContentType.php b/Enums/BriefContentType.php index f2e6729..c6adc0d 100644 --- a/Enums/BriefContentType.php +++ b/Enums/BriefContentType.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Enums; +namespace Core\Mod\Content\Enums; /** * Content type for ContentBrief. diff --git a/Enums/ContentType.php b/Enums/ContentType.php index 38db96d..e0f00f7 100644 --- a/Enums/ContentType.php +++ b/Enums/ContentType.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Enums; +namespace Core\Mod\Content\Enums; /** * Content source type for ContentItem. diff --git a/Jobs/GenerateContentJob.php b/Jobs/GenerateContentJob.php index dad1fdf..be24fe0 100644 --- a/Jobs/GenerateContentJob.php +++ b/Jobs/GenerateContentJob.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Jobs; +namespace Core\Mod\Content\Jobs; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -10,8 +10,8 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Log; -use Core\Content\Models\ContentBrief; -use Core\Content\Services\AIGatewayService; +use Core\Mod\Content\Models\ContentBrief; +use Core\Mod\Content\Services\AIGatewayService; /** * GenerateContentJob diff --git a/Jobs/ProcessContentWebhook.php b/Jobs/ProcessContentWebhook.php index c70b3a0..b2a75dc 100644 --- a/Jobs/ProcessContentWebhook.php +++ b/Jobs/ProcessContentWebhook.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Jobs; +namespace Core\Mod\Content\Jobs; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -10,12 +10,12 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Log; -use Core\Content\Enums\ContentType; -use Core\Content\Models\ContentItem; -use Core\Content\Models\ContentMedia; -use Core\Content\Models\ContentTaxonomy; -use Core\Content\Models\ContentWebhookEndpoint; -use Core\Content\Models\ContentWebhookLog; +use Core\Mod\Content\Enums\ContentType; +use Core\Mod\Content\Models\ContentItem; +use Core\Mod\Content\Models\ContentMedia; +use Core\Mod\Content\Models\ContentTaxonomy; +use Core\Mod\Content\Models\ContentWebhookEndpoint; +use Core\Mod\Content\Models\ContentWebhookLog; /** * Process incoming content webhooks. diff --git a/Mcp/Handlers/ContentCreateHandler.php b/Mcp/Handlers/ContentCreateHandler.php index 6340104..5f736f3 100644 --- a/Mcp/Handlers/ContentCreateHandler.php +++ b/Mcp/Handlers/ContentCreateHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Mcp\Handlers; +namespace Core\Mod\Content\Mcp\Handlers; use Carbon\Carbon; use Core\Front\Mcp\Contracts\McpToolHandler; @@ -11,10 +11,10 @@ use Core\Mod\Tenant\Models\Workspace; use Core\Mod\Tenant\Services\EntitlementService; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Str; -use Core\Content\Enums\ContentType; -use Core\Content\Models\ContentItem; -use Core\Content\Models\ContentRevision; -use Core\Content\Models\ContentTaxonomy; +use Core\Mod\Content\Enums\ContentType; +use Core\Mod\Content\Models\ContentItem; +use Core\Mod\Content\Models\ContentRevision; +use Core\Mod\Content\Models\ContentTaxonomy; /** * MCP tool handler for creating content items. diff --git a/Mcp/Handlers/ContentDeleteHandler.php b/Mcp/Handlers/ContentDeleteHandler.php index 39cf7f6..d883a5e 100644 --- a/Mcp/Handlers/ContentDeleteHandler.php +++ b/Mcp/Handlers/ContentDeleteHandler.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace Core\Content\Mcp\Handlers; +namespace Core\Mod\Content\Mcp\Handlers; use Core\Front\Mcp\Contracts\McpToolHandler; use Core\Front\Mcp\McpContext; use Core\Mod\Tenant\Models\Workspace; use Illuminate\Support\Facades\Auth; -use Core\Content\Models\ContentItem; -use Core\Content\Models\ContentRevision; +use Core\Mod\Content\Models\ContentItem; +use Core\Mod\Content\Models\ContentRevision; /** * MCP tool handler for deleting content items. diff --git a/Mcp/Handlers/ContentListHandler.php b/Mcp/Handlers/ContentListHandler.php index dabcbfd..ac23c40 100644 --- a/Mcp/Handlers/ContentListHandler.php +++ b/Mcp/Handlers/ContentListHandler.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace Core\Content\Mcp\Handlers; +namespace Core\Mod\Content\Mcp\Handlers; use Core\Front\Mcp\Contracts\McpToolHandler; use Core\Front\Mcp\McpContext; use Core\Mod\Tenant\Models\Workspace; use Illuminate\Support\Str; -use Core\Content\Models\ContentItem; +use Core\Mod\Content\Models\ContentItem; /** * MCP tool handler for listing content items. diff --git a/Mcp/Handlers/ContentReadHandler.php b/Mcp/Handlers/ContentReadHandler.php index e769114..f9c760f 100644 --- a/Mcp/Handlers/ContentReadHandler.php +++ b/Mcp/Handlers/ContentReadHandler.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Core\Content\Mcp\Handlers; +namespace Core\Mod\Content\Mcp\Handlers; use Core\Front\Mcp\Contracts\McpToolHandler; use Core\Front\Mcp\McpContext; use Core\Mod\Tenant\Models\Workspace; -use Core\Content\Models\ContentItem; +use Core\Mod\Content\Models\ContentItem; /** * MCP tool handler for reading content items. diff --git a/Mcp/Handlers/ContentSearchHandler.php b/Mcp/Handlers/ContentSearchHandler.php index 07adc19..b3bc8ed 100644 --- a/Mcp/Handlers/ContentSearchHandler.php +++ b/Mcp/Handlers/ContentSearchHandler.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace Core\Content\Mcp\Handlers; +namespace Core\Mod\Content\Mcp\Handlers; use Core\Front\Mcp\Contracts\McpToolHandler; use Core\Front\Mcp\McpContext; use Core\Mod\Tenant\Models\Workspace; use Illuminate\Support\Str; -use Core\Content\Services\ContentSearchService; +use Core\Mod\Content\Services\ContentSearchService; /** * MCP tool handler for searching content. diff --git a/Mcp/Handlers/ContentTaxonomiesHandler.php b/Mcp/Handlers/ContentTaxonomiesHandler.php index 9f27772..7e3bd2a 100644 --- a/Mcp/Handlers/ContentTaxonomiesHandler.php +++ b/Mcp/Handlers/ContentTaxonomiesHandler.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Core\Content\Mcp\Handlers; +namespace Core\Mod\Content\Mcp\Handlers; use Core\Front\Mcp\Contracts\McpToolHandler; use Core\Front\Mcp\McpContext; use Core\Mod\Tenant\Models\Workspace; -use Core\Content\Models\ContentTaxonomy; +use Core\Mod\Content\Models\ContentTaxonomy; /** * MCP tool handler for listing content taxonomies. diff --git a/Mcp/Handlers/ContentUpdateHandler.php b/Mcp/Handlers/ContentUpdateHandler.php index 2f221ff..dd79d46 100644 --- a/Mcp/Handlers/ContentUpdateHandler.php +++ b/Mcp/Handlers/ContentUpdateHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Mcp\Handlers; +namespace Core\Mod\Content\Mcp\Handlers; use Carbon\Carbon; use Core\Front\Mcp\Contracts\McpToolHandler; @@ -10,9 +10,9 @@ use Core\Front\Mcp\McpContext; use Core\Mod\Tenant\Models\Workspace; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Str; -use Core\Content\Models\ContentItem; -use Core\Content\Models\ContentRevision; -use Core\Content\Models\ContentTaxonomy; +use Core\Mod\Content\Models\ContentItem; +use Core\Mod\Content\Models\ContentRevision; +use Core\Mod\Content\Models\ContentTaxonomy; /** * MCP tool handler for updating content items. diff --git a/Middleware/WorkspaceRouter.php b/Middleware/WorkspaceRouter.php index 648fe2d..ac262a7 100644 --- a/Middleware/WorkspaceRouter.php +++ b/Middleware/WorkspaceRouter.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Core\Content\Middleware; +namespace Core\Mod\Content\Middleware; -use Core\Content\Services\ContentRender; +use Core\Mod\Content\Services\ContentRender; use Core\Mod\Tenant\Models\Workspace; use Closure; use Illuminate\Http\Request; diff --git a/Models/AIUsage.php b/Models/AIUsage.php index 2d3558c..00a9009 100644 --- a/Models/AIUsage.php +++ b/Models/AIUsage.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Models; +namespace Core\Mod\Content\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -20,9 +20,9 @@ class AIUsage extends Model { use HasFactory; - protected static function newFactory(): \Core\Content\Database\Factories\AIUsageFactory + protected static function newFactory(): \Core\Mod\Content\Database\Factories\AIUsageFactory { - return \Core\Content\Database\Factories\AIUsageFactory::new(); + return \Core\Mod\Content\Database\Factories\AIUsageFactory::new(); } protected $table = 'ai_usage'; diff --git a/Models/ContentAuthor.php b/Models/ContentAuthor.php index a881792..572c7fd 100644 --- a/Models/ContentAuthor.php +++ b/Models/ContentAuthor.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Models; +namespace Core\Mod\Content\Models; use Core\Mod\Tenant\Models\Workspace; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -14,9 +14,9 @@ class ContentAuthor extends Model { use HasFactory; - protected static function newFactory(): \Core\Content\Database\Factories\ContentAuthorFactory + protected static function newFactory(): \Core\Mod\Content\Database\Factories\ContentAuthorFactory { - return \Core\Content\Database\Factories\ContentAuthorFactory::new(); + return \Core\Mod\Content\Database\Factories\ContentAuthorFactory::new(); } protected $fillable = [ diff --git a/Models/ContentBrief.php b/Models/ContentBrief.php index 2117334..eadf6cf 100644 --- a/Models/ContentBrief.php +++ b/Models/ContentBrief.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace Core\Content\Models; +namespace Core\Mod\Content\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; -use Core\Content\Enums\BriefContentType; +use Core\Mod\Content\Enums\BriefContentType; use Core\Mod\Tenant\Models\Workspace; /** @@ -23,9 +23,9 @@ class ContentBrief extends Model { use HasFactory; - protected static function newFactory(): \Core\Content\Database\Factories\ContentBriefFactory + protected static function newFactory(): \Core\Mod\Content\Database\Factories\ContentBriefFactory { - return \Core\Content\Database\Factories\ContentBriefFactory::new(); + return \Core\Mod\Content\Database\Factories\ContentBriefFactory::new(); } public const STATUS_PENDING = 'pending'; diff --git a/Models/ContentItem.php b/Models/ContentItem.php index fe501a2..84bfc76 100644 --- a/Models/ContentItem.php +++ b/Models/ContentItem.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Models; +namespace Core\Mod\Content\Models; use Core\Mod\Tenant\Models\User; use Core\Mod\Tenant\Models\Workspace; @@ -14,17 +14,17 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; -use Core\Content\Enums\ContentType; -use Core\Content\Observers\ContentItemObserver; +use Core\Mod\Content\Enums\ContentType; +use Core\Mod\Content\Observers\ContentItemObserver; #[ObservedBy([ContentItemObserver::class])] class ContentItem extends Model { use HasFactory, HasSeoMetadata, SoftDeletes; - protected static function newFactory(): \Core\Content\Database\Factories\ContentItemFactory + protected static function newFactory(): \Core\Mod\Content\Database\Factories\ContentItemFactory { - return \Core\Content\Database\Factories\ContentItemFactory::new(); + return \Core\Mod\Content\Database\Factories\ContentItemFactory::new(); } protected $fillable = [ diff --git a/Models/ContentMedia.php b/Models/ContentMedia.php index 5ce1b50..a6e9008 100644 --- a/Models/ContentMedia.php +++ b/Models/ContentMedia.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Models; +namespace Core\Mod\Content\Models; use Core\Mod\Tenant\Models\Workspace; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -13,9 +13,9 @@ class ContentMedia extends Model { use HasFactory; - protected static function newFactory(): \Core\Content\Database\Factories\ContentMediaFactory + protected static function newFactory(): \Core\Mod\Content\Database\Factories\ContentMediaFactory { - return \Core\Content\Database\Factories\ContentMediaFactory::new(); + return \Core\Mod\Content\Database\Factories\ContentMediaFactory::new(); } protected $table = 'content_media'; diff --git a/Models/ContentRevision.php b/Models/ContentRevision.php index 7b62840..2aa8120 100644 --- a/Models/ContentRevision.php +++ b/Models/ContentRevision.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Models; +namespace Core\Mod\Content\Models; use Core\Mod\Tenant\Models\User; use Illuminate\Database\Eloquent\Factories\HasFactory; diff --git a/Models/ContentTask.php b/Models/ContentTask.php index 01d5b84..82a4f5a 100644 --- a/Models/ContentTask.php +++ b/Models/ContentTask.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Models; +namespace Core\Mod\Content\Models; use Mod\Agentic\Models\Prompt; use Core\Mod\Tenant\Models\Workspace; diff --git a/Models/ContentTaxonomy.php b/Models/ContentTaxonomy.php index c74f3cf..4b9c29e 100644 --- a/Models/ContentTaxonomy.php +++ b/Models/ContentTaxonomy.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Models; +namespace Core\Mod\Content\Models; use Core\Mod\Tenant\Models\Workspace; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -14,9 +14,9 @@ class ContentTaxonomy extends Model { use HasFactory; - protected static function newFactory(): \Core\Content\Database\Factories\ContentTaxonomyFactory + protected static function newFactory(): \Core\Mod\Content\Database\Factories\ContentTaxonomyFactory { - return \Core\Content\Database\Factories\ContentTaxonomyFactory::new(); + return \Core\Mod\Content\Database\Factories\ContentTaxonomyFactory::new(); } protected $fillable = [ diff --git a/Models/ContentWebhookEndpoint.php b/Models/ContentWebhookEndpoint.php index 331d941..5e8624f 100644 --- a/Models/ContentWebhookEndpoint.php +++ b/Models/ContentWebhookEndpoint.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Models; +namespace Core\Mod\Content\Models; use Core\Mod\Tenant\Models\Workspace; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -35,9 +35,9 @@ class ContentWebhookEndpoint extends Model { use HasFactory; - protected static function newFactory(): \Core\Content\Database\Factories\ContentWebhookEndpointFactory + protected static function newFactory(): \Core\Mod\Content\Database\Factories\ContentWebhookEndpointFactory { - return \Core\Content\Database\Factories\ContentWebhookEndpointFactory::new(); + return \Core\Mod\Content\Database\Factories\ContentWebhookEndpointFactory::new(); } protected $table = 'content_webhook_endpoints'; diff --git a/Models/ContentWebhookLog.php b/Models/ContentWebhookLog.php index fb7289d..284d1e7 100644 --- a/Models/ContentWebhookLog.php +++ b/Models/ContentWebhookLog.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Models; +namespace Core\Mod\Content\Models; use Core\Mod\Tenant\Models\Workspace; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -13,9 +13,9 @@ class ContentWebhookLog extends Model { use HasFactory; - protected static function newFactory(): \Core\Content\Database\Factories\ContentWebhookLogFactory + protected static function newFactory(): \Core\Mod\Content\Database\Factories\ContentWebhookLogFactory { - return \Core\Content\Database\Factories\ContentWebhookLogFactory::new(); + return \Core\Mod\Content\Database\Factories\ContentWebhookLogFactory::new(); } protected $fillable = [ diff --git a/Observers/ContentItemObserver.php b/Observers/ContentItemObserver.php index e617c01..223f255 100644 --- a/Observers/ContentItemObserver.php +++ b/Observers/ContentItemObserver.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Core\Content\Observers; +namespace Core\Mod\Content\Observers; use Illuminate\Support\Facades\Log; -use Core\Content\Models\ContentItem; -use Core\Content\Services\CdnPurgeService; +use Core\Mod\Content\Models\ContentItem; +use Core\Mod\Content\Services\CdnPurgeService; /** * Content Item Observer - handles CDN cache purging on content changes. diff --git a/Services/AIGatewayService.php b/Services/AIGatewayService.php index 5ac276a..10d4b7b 100644 --- a/Services/AIGatewayService.php +++ b/Services/AIGatewayService.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace Core\Content\Services; +namespace Core\Mod\Content\Services; use Mod\Agentic\Services\AgenticResponse; use Mod\Agentic\Services\ClaudeService; use Mod\Agentic\Services\GeminiService; -use Core\Content\Models\AIUsage; -use Core\Content\Models\ContentBrief; +use Core\Mod\Content\Models\AIUsage; +use Core\Mod\Content\Models\ContentBrief; use RuntimeException; /** diff --git a/Services/CdnPurgeService.php b/Services/CdnPurgeService.php index 47891ef..7ae107c 100644 --- a/Services/CdnPurgeService.php +++ b/Services/CdnPurgeService.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Core\Content\Services; +namespace Core\Mod\Content\Services; use Illuminate\Support\Facades\Log; -use Core\Content\Models\ContentItem; +use Core\Mod\Content\Models\ContentItem; use Plug\Cdn\CdnManager; use Plug\Response; diff --git a/Services/ContentProcessingService.php b/Services/ContentProcessingService.php index 7d62249..40613bf 100644 --- a/Services/ContentProcessingService.php +++ b/Services/ContentProcessingService.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Services; +namespace Core\Mod\Content\Services; use DOMDocument; use DOMElement; diff --git a/Services/ContentRender.php b/Services/ContentRender.php index 1e599df..6ec724c 100644 --- a/Services/ContentRender.php +++ b/Services/ContentRender.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace Core\Content\Services; +namespace Core\Mod\Content\Services; use Core\Front\Controller; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; use Illuminate\View\View; -use Core\Content\Models\ContentItem; +use Core\Mod\Content\Models\ContentItem; use Core\Mod\Tenant\Models\Workspace; /** diff --git a/Services/ContentSearchService.php b/Services/ContentSearchService.php index 856b6ac..01cc2cf 100644 --- a/Services/ContentSearchService.php +++ b/Services/ContentSearchService.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Content\Services; +namespace Core\Mod\Content\Services; use Carbon\Carbon; use Core\Mod\Tenant\Models\Workspace; @@ -10,7 +10,7 @@ use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; use Illuminate\Support\Str; -use Core\Content\Models\ContentItem; +use Core\Mod\Content\Models\ContentItem; /** * Content Search Service diff --git a/Services/WebhookRetryService.php b/Services/WebhookRetryService.php index 95ee916..886c507 100644 --- a/Services/WebhookRetryService.php +++ b/Services/WebhookRetryService.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Core\Content\Services; +namespace Core\Mod\Content\Services; use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Facades\Log; -use Core\Content\Models\ContentWebhookLog; +use Core\Mod\Content\Models\ContentWebhookLog; /** * WebhookRetryService diff --git a/View/Modal/Admin/ContentSearch.php b/View/Modal/Admin/ContentSearch.php index 01229a0..9c6a5fb 100644 --- a/View/Modal/Admin/ContentSearch.php +++ b/View/Modal/Admin/ContentSearch.php @@ -2,16 +2,16 @@ declare(strict_types=1); -namespace Core\Content\View\Modal\Admin; +namespace Core\Mod\Content\View\Modal\Admin; use Livewire\Attributes\Computed; use Livewire\Attributes\Layout; use Livewire\Attributes\Url; use Livewire\Component; use Livewire\WithPagination; -use Core\Content\Models\ContentItem; -use Core\Content\Models\ContentTaxonomy; -use Core\Content\Services\ContentSearchService; +use Core\Mod\Content\Models\ContentItem; +use Core\Mod\Content\Models\ContentTaxonomy; +use Core\Mod\Content\Services\ContentSearchService; /** * Content Search Livewire Component diff --git a/View/Modal/Admin/WebhookManager.php b/View/Modal/Admin/WebhookManager.php index 5c6d578..93987a8 100644 --- a/View/Modal/Admin/WebhookManager.php +++ b/View/Modal/Admin/WebhookManager.php @@ -2,15 +2,15 @@ declare(strict_types=1); -namespace Core\Content\View\Modal\Admin; +namespace Core\Mod\Content\View\Modal\Admin; use Livewire\Attributes\Computed; use Livewire\Attributes\Layout; use Livewire\Attributes\Url; use Livewire\Component; use Livewire\WithPagination; -use Core\Content\Models\ContentWebhookEndpoint; -use Core\Content\Models\ContentWebhookLog; +use Core\Mod\Content\Models\ContentWebhookEndpoint; +use Core\Mod\Content\Models\ContentWebhookLog; /** * Livewire component for managing content webhook endpoints. diff --git a/View/Modal/Web/Blog.php b/View/Modal/Web/Blog.php index 1ca243c..686ff2d 100644 --- a/View/Modal/Web/Blog.php +++ b/View/Modal/Web/Blog.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Core\Content\View\Modal\Web; +namespace Core\Mod\Content\View\Modal\Web; use Livewire\Component; -use Core\Content\Services\ContentRender; +use Core\Mod\Content\Services\ContentRender; use Core\Mod\Tenant\Models\Workspace; use Core\Mod\Tenant\Services\WorkspaceService; diff --git a/View/Modal/Web/Help.php b/View/Modal/Web/Help.php index 8d9a9c9..541b967 100644 --- a/View/Modal/Web/Help.php +++ b/View/Modal/Web/Help.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Core\Content\View\Modal\Web; +namespace Core\Mod\Content\View\Modal\Web; use Livewire\Component; -use Core\Content\Models\ContentItem; +use Core\Mod\Content\Models\ContentItem; use Core\Mod\Tenant\Models\Workspace; use Core\Mod\Tenant\Services\WorkspaceService; diff --git a/View/Modal/Web/HelpArticle.php b/View/Modal/Web/HelpArticle.php index 283e694..cfe040e 100644 --- a/View/Modal/Web/HelpArticle.php +++ b/View/Modal/Web/HelpArticle.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Core\Content\View\Modal\Web; +namespace Core\Mod\Content\View\Modal\Web; use Livewire\Component; -use Core\Content\Services\ContentRender; +use Core\Mod\Content\Services\ContentRender; use Core\Mod\Tenant\Models\Workspace; use Core\Mod\Tenant\Services\WorkspaceService; diff --git a/View/Modal/Web/Post.php b/View/Modal/Web/Post.php index 931b376..d43b53d 100644 --- a/View/Modal/Web/Post.php +++ b/View/Modal/Web/Post.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Core\Content\View\Modal\Web; +namespace Core\Mod\Content\View\Modal\Web; use Livewire\Component; -use Core\Content\Services\ContentRender; +use Core\Mod\Content\Services\ContentRender; use Core\Mod\Tenant\Models\Workspace; use Core\Mod\Tenant\Services\WorkspaceService; diff --git a/View/Modal/Web/Preview.php b/View/Modal/Web/Preview.php index 1cffdff..7399a30 100644 --- a/View/Modal/Web/Preview.php +++ b/View/Modal/Web/Preview.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Core\Content\View\Modal\Web; +namespace Core\Mod\Content\View\Modal\Web; use Core\Mod\Tenant\Models\Workspace; use Livewire\Component; -use Core\Content\Models\ContentItem; +use Core\Mod\Content\Models\ContentItem; /** * Preview - Render draft/unpublished content with preview token. diff --git a/changelog/2026/jan/TASK-004-native-cms-wordpress-removal.md b/changelog/2026/jan/TASK-004-native-cms-wordpress-removal.md new file mode 100644 index 0000000..1c9f845 --- /dev/null +++ b/changelog/2026/jan/TASK-004-native-cms-wordpress-removal.md @@ -0,0 +1,677 @@ +# TASK-004: Native CMS and WordPress Removal + +**Status:** ✅ complete (verified) +**Created:** 2026-01-02 +**Last Updated:** 2026-01-02 16:30 by Claude Opus 4.5 (Implementation Agent) +**Assignee:** Claude Opus 4.5 (Implementation Agent) +**Verifier:** Claude Opus 4.5 (Verification Agent) + +--- + +## Critical Context (READ FIRST) + +**WordPress served its purpose. Now we own the content layer.** + +### The Current State + +WordPress (hestia.host.uk.com) has been the content backend for satellite sites: +- Blog posts and help articles synced via REST API +- ContentItem model stores local copies with sync metadata +- SatelliteService uses "local-first" pattern (check DB, fallback to WP API) +- Webhook integration for real-time sync + +This worked for bootstrapping, but creates: +- **Operational overhead** — two systems to maintain +- **Sync complexity** — race conditions, stale content, webhook failures +- **Stack mismatch** — PHP ↔ WordPress when we could be pure Laravel +- **Future friction** — MCP integration wants native content, not WP bridges + +### The Vision + +A fully native content management system where: +- ContentItem is the **source of truth** (not a sync cache) +- Content Editor at `/hub/content-editor/{workspace}/new/{contentType}` is the primary authoring tool +- AI assistance via MCP for content generation, SEO, translation +- Satellite sites serve directly from Laravel (no WordPress dependency) +- WordPress can re-integrate later as *one option among many* (headless CMS, not *the* CMS) + +### Why Now + +1. Content Editor already exists and works well +2. SatelliteService already has local-first logic +3. MCP Portal (mcp.host.uk.com) needs native content APIs +4. Phase 42 content generation uses native workflows +5. WordPress hosting is an unnecessary cost + +--- + +## Objective + +Remove WordPress as a required dependency for satellite site content. Make the native Content Editor the primary authoring tool, with ContentItem as the source of truth. Prepare the content layer for MCP integration (AI-assisted content creation, semantic search, agent access). + +**"Done" looks like:** +- Satellite sites (blog, help) serve content entirely from Laravel database +- Content Editor is the only place to create/edit content +- No WordPress API calls in normal operation +- MCP tools can read/write content natively +- WordPress integration exists as an optional import/export feature + +--- + +## Acceptance Criteria + +### Phase 1: Remove WordPress Dependency ✅ VERIFIED + +- [x] AC1: SatelliteService never calls WordPressService in normal operation +- [x] AC2: ContentItem has `content_type = 'native'` (new value, distinct from 'wordpress') +- [x] AC3: Blog pages (`/blog`, `/blog/{slug}`) render from native content only +- [x] AC4: Help pages (`/help`, `/help/{slug}`) render from native content only +- [x] AC5: WordPress sync code is moved to `app/Legacy/` (not deleted, for future import) +- [x] AC6: `WORDPRESS_URL` env var is optional, not required for app boot + +### Phase 2: Content Editor Enhancements + +- [x] AC7: Content Editor supports rich text with Flux Editor (not just textarea) +- [x] AC8: Content Editor has media upload (not WordPress media library) +- [x] AC9: Content Editor has category/tag management +- [x] AC10: Content Editor has SEO fields (meta title, description, OG image) +- [x] AC11: Content Editor has scheduling (publish_at datetime) +- [x] AC12: Content Editor has revision history + +### Phase 3: MCP Integration ✅ COMPLETE + +- [x] AC13: MCP tool `content_list` returns content items for a workspace +- [x] AC14: MCP tool `content_read` returns full content by ID or slug +- [x] AC15: MCP tool `content_create` creates new content (respects entitlements) +- [x] AC16: MCP tool `content_update` updates existing content +- [x] AC17: MCP resource `content://workspace/slug` provides content for context +- [x] AC18: AI content generation uses MCP tools (not direct OpenAI/Claude calls) + +### Phase 4: Optional WordPress Import ✅ COMPLETE + +- [x] AC19: Artisan command `content:import-wordpress` imports from WP REST API +- [x] AC20: Import preserves original WordPress IDs in `wp_id` field +- [x] AC21: Import handles media, categories, tags, authors +- [x] AC22: Import is idempotent (re-running updates, doesn't duplicate) + +--- + +## Implementation Checklist + +### Phase 1: WordPress Removal + +- [x] Create new enum value `ContentType::NATIVE` in `app/Enums/ContentType.php` +- [x] Update `ContentItem` model to default to `content_type = 'native'` +- [x] Refactor `SatelliteService` to remove WordPress fallback: + - [x] `getHomepage()` — native only + - [x] `getPosts()` — native only + - [x] `getPost()` — native only + - [x] `getPage()` — native only +- [x] Move WordPress integration files to `app/Legacy/`: + - [x] `app/Services/WordPressService.php` → `app/Legacy/WordPress/WordPressService.php` + - [x] `app/Services/ContentSyncService.php` → `app/Legacy/WordPress/ContentSyncService.php` + - [x] `app/Http/Controllers/Api/ContentWebhookController.php` → `app/Legacy/WordPress/` + - [x] `app/Jobs/ProcessContentWebhook.php` → `app/Legacy/WordPress/` +- [x] Update `config/services.php` to make WordPress optional +- [x] Make WordPress routes conditional in `routes/api.php` (enabled via `CONTENT_WORDPRESS_ENABLED`) +- [x] Update satellite views to not expect WordPress-specific fields (added native filter option) +- [x] Add feature flag `CONTENT_SOURCE=native` (default) vs `CONTENT_SOURCE=wordpress` +- [x] Write migration to update existing `content_type = 'wordpress'` → `'native'` + +### Phase 2: Content Editor + +- [x] Implement Flux Editor integration in `ContentEditor.php` +- [x] Create media upload in Content Editor (WithFileUploads trait, Livewire) +- [x] Add media library modal to Content Editor (featured image selection) +- [x] Add taxonomy management UI in Content Editor sidebar (categories/tags) +- [x] Add SEO fields component in Content Editor sidebar (meta title, description, keywords, preview) +- [x] Implement `publish_at` scheduling with datetime picker in sidebar +- [x] Create `ContentRevision` model for version history +- [x] Add revision history panel with restore functionality +- [x] Add autosave functionality (60-second interval) +- [x] Add Ctrl+S keyboard shortcut for save + +### Phase 3: MCP Tools + +- [x] Create `app/Mcp/Tools/ContentTools.php`: + - [x] `content_list` — paginated, filterable by type/status/search + - [x] `content_read` — by ID or slug, returns markdown + - [x] `content_create` — with entitlement check and taxonomy support + - [x] `content_update` — with revision creation + - [x] `content_delete` — soft delete + - [x] `taxonomies` — list categories and tags +- [x] Create `app/Mcp/Resources/ContentResource.php`: + - [x] URI format: `content://{workspace}/{slug}` + - [x] Returns markdown with YAML frontmatter for AI context + - [x] Includes categories, tags, SEO meta, publish_at +- [x] Register tools in MCP server (`app/Mcp/Servers/HostHub.php`) +- [x] Add entitlement features: `content.mcp_access`, `content.items`, `content.ai_generation` +- [x] Write MCP tool tests (48 tests passing) + +### Phase 4: WordPress Import ✅ COMPLETE + +- [x] Create `app/Console/Commands/ContentImportWordPress.php` +- [x] Implement batch import with progress bar +- [x] Map WordPress fields to ContentItem fields +- [x] Download and store media files locally +- [x] Create authors from WordPress users +- [x] Handle categories and tags +- [x] Add `--dry-run` flag for preview +- [x] Add `--since` flag for incremental imports +- [x] Add `--limit` flag for controlling import count +- [x] Add `--skip-media` flag to skip file downloads +- [x] Add `--types` flag to select what to import +- [x] Support JWT and Basic Auth for private content +- [x] Handle HTML entities and smart quotes +- [x] Extract SEO metadata from Yoast/RankMath + +### Testing + +- [x] Feature test: Satellite blog renders native content +- [x] Feature test: Content Editor creates native content +- [x] Feature test: Content Editor updates native content +- [x] Feature test: Media upload works +- [x] Feature test: MCP tools require authentication +- [x] Feature test: MCP tools respect entitlements +- [x] Unit test: ContentItem has correct relationships +- [x] Unit test: SatelliteService returns native content +- [x] Integration test: WordPress import processes all content types (21 tests) + +--- + +## Migration Strategy + +### Zero Downtime Approach + +1. **Phase 1a: Add native support alongside WordPress** + - Keep WordPress sync running + - Add `content_type = 'native'` capability + - Content Editor creates native content + - SatelliteService prefers native, falls back to WordPress + +2. **Phase 1b: Migrate existing content** + - Run one-time migration to copy `wordpress` → `native` + - Verify all satellite pages render correctly + - Monitor for 1 week + +3. **Phase 1c: Disable WordPress sync** + - Remove webhook registration + - Set `CONTENT_SOURCE=native` + - Keep WordPress running (read-only backup) + +4. **Phase 1d: Remove WordPress** + - Move sync code to `app/Legacy/` + - Remove WordPress infrastructure + - Update documentation + +### Rollback Plan + +If issues arise: +1. Set `CONTENT_SOURCE=wordpress` +2. SatelliteService reverts to WordPress fallback +3. Re-enable webhook sync +4. Content continues serving from WordPress + +--- + +## Data Model Changes + +### ContentItem Updates + +```php +// New enum value +enum ContentType: string +{ + case WORDPRESS = 'wordpress'; // Legacy synced content + case NATIVE = 'native'; // Native Host UK content + case HOSTUK = 'hostuk'; // Keep for backwards compat, alias to native + case SATELLITE = 'satellite'; // Per-satellite site content +} + +// New fields (if not present) +Schema::table('content_items', function (Blueprint $table) { + $table->timestamp('publish_at')->nullable()->after('status'); + $table->unsignedBigInteger('revision_of')->nullable()->after('id'); + $table->foreign('revision_of')->references('id')->on('content_items'); +}); +``` + +### New Table: content_revisions + +```php +Schema::create('content_revisions', function (Blueprint $table) { + $table->id(); + $table->foreignId('content_item_id')->constrained()->cascadeOnDelete(); + $table->foreignId('user_id')->nullable()->constrained(); + $table->string('title'); + $table->longText('content_json'); + $table->longText('content_html'); + $table->text('change_summary')->nullable(); + $table->timestamps(); +}); +``` + +--- + +## MCP Tool Specifications + +### content_list + +```json +{ + "name": "content_list", + "description": "List content items for a workspace. Use to find articles, blog posts, or help pages.", + "parameters": { + "workspace": "Workspace slug (main, bio, social, etc.)", + "type": "Filter by type: post, page, or all", + "status": "Filter by status: draft, published, scheduled", + "limit": "Max items to return (default 20)", + "search": "Search title and content" + }, + "use_when": [ + "Need to find existing content", + "Want to see what's published on a satellite site", + "Looking for content to update or reference" + ] +} +``` + +### content_read + +```json +{ + "name": "content_read", + "description": "Read full content of an article or page. Returns markdown for easy processing.", + "parameters": { + "workspace": "Workspace slug", + "identifier": "Content slug or ID" + }, + "returns": "Full content as markdown with frontmatter (title, author, date, categories)" +} +``` + +### content_create + +```json +{ + "name": "content_create", + "description": "Create new content. Requires workspace write permission and ai.credits entitlement.", + "parameters": { + "workspace": "Target workspace", + "type": "post or page", + "title": "Content title", + "content": "Markdown content", + "status": "draft (default), published, or scheduled", + "publish_at": "ISO datetime for scheduled publishing", + "categories": "Array of category slugs", + "tags": "Array of tag strings" + } +} +``` + +--- + +## Dependencies + +- Flux Editor component (already in Flux Pro) +- S3 or local storage for media +- MCP server infrastructure (existing) +- Entitlement system (existing) + +--- + +## Risks and Mitigations + +| Risk | Impact | Mitigation | +|------|--------|------------| +| Content loss during migration | High | Backup before migration, keep WordPress running until verified | +| SEO impact from URL changes | Medium | Keep same URL structure, 301 redirects if needed | +| Editor bugs causing data loss | High | Implement autosave, revision history from day 1 | +| MCP tools expose sensitive content | Medium | Workspace permission checks, entitlement gating | + +--- + +## Verification Results + +### Phase 1 Verification: 2026-01-02 by Claude Opus 4.5 (Verification Agent) + +| Criterion | Status | Evidence | +|-----------|--------|----------| +| AC1: SatelliteService never calls WordPressService | ✅ PASS | `grep WordPressService app/Services/SatelliteService.php` returns no matches. File uses only `ContentItem` queries with `->native()` scope. Lines 38-44, 76-80, 104-111, 128-134 all use `ContentItem::forWorkspace()->native()`. | +| AC2: ContentItem has `content_type = 'native'` (new default) | ✅ PASS | `ContentType::NATIVE` enum exists at `app/Enums/ContentType.php:16`. Model's `booted()` method at lines 444-452 sets default via `ContentType::default()` which returns `NATIVE`. | +| AC3: Blog pages render from native content only | ✅ PASS | `app/Livewire/Satellite/Blog.php` uses `SatelliteService::getPosts()` which queries `ContentItem::native()`. No WordPressService import. | +| AC4: Help pages render from native content only | ✅ PASS | `app/Livewire/Satellite/Help.php` queries `ContentItem::forWorkspace()->native()->pages()` directly at lines 38-44. No WordPressService import. | +| AC5: WordPress sync code moved to `app/Legacy/` | ✅ PASS | Files exist at `app/Legacy/WordPress/`: `WordPressService.php`, `ContentSyncService.php`, `ContentWebhookController.php`, `ProcessContentWebhook.php`. Original locations (`app/Services/WordPressService.php`, `app/Services/ContentSyncService.php`) return "No such file or directory". | +| AC6: `WORDPRESS_URL` env var is optional | ✅ PASS | `config/services.php:48-54` shows `content.source` defaults to `native` and `content.wordpress_enabled` defaults to `false`. WordPress config at lines 67-74 only read if enabled. `routes/api.php:41` gates WordPress routes behind `config('services.content.wordpress_enabled')`. | + +**Additional Verification:** +- Migration `2026_01_02_140456_update_wordpress_content_to_native.php` exists and converts `wordpress`/`hostuk` to `native` +- Tests: `./vendor/bin/pest --filter="Satellite|Content"` — 67 passed (2 unrelated SocialProof failures) + +**Verdict:** ✅ PASS — Phase 1 acceptance criteria AC1-AC6 all met. Ready for Phase 2 implementation. + +--- + +### Phase 2 Verification: 2026-01-02 by Claude Opus 4.5 (Verification Agent) + +| Criterion | Status | Evidence | +|-----------|--------|----------| +| AC7: Content Editor supports rich text with Flux Editor | ✅ PASS | ` $wpId` and `'wp_guid' => $item['guid']['rendered']`. Test: "preserves original WordPress IDs" at line 313 verifies `$post->wp_id->toBe(12345)` and `$post->wp_guid->toBe('https://example.com/?p=12345')`. | +| AC21: Import handles media, categories, tags, authors | ✅ PASS | `importAuthors()` line 231, `importCategories()` line 326, `importTags()` line 381, `importMedia()` line 480. Each has dedicated import method with progress bars. Tests verify all types: "imports WordPress users as authors", "imports WordPress categories", "imports WordPress tags", "imports WordPress media items" all pass. | +| AC22: Import is idempotent (re-running updates, doesn't duplicate) | ✅ PASS | Each import method checks for existing records first (e.g., `ContentItem::forWorkspace()->where('wp_id', $wpId)->first()` at line 729). If exists, updates instead of creates (line 791-793). Test: "idempotency → updates existing content instead of duplicating" verifies `ContentItem::count()->toBe(1)` after re-import and fields are updated. | + +**Additional Verification:** +- Tests: `./vendor/bin/pest tests/Feature/Console/ContentImportWordPressTest.php` — 21 passed (90 assertions) +- Dry run mode: `--dry-run` flag prevents database changes (test at line 522) +- Incremental import: `--since` flag filters by modification date (test at line 569) +- Limit option: `--limit` flag caps items per type (test at line 647) +- SEO extraction: Yoast/RankMath metadata extracted (test at line 717) +- Scheduled posts: Future status and publish_at preserved (test at line 762) +- HTML entities: Smart quotes normalised to ASCII (test at line 348) + +**Verdict:** ✅ PASS — Phase 4 acceptance criteria AC19-AC22 all met. TASK-004 is complete. + +--- + +## Notes + +### Why Keep WordPress Import + +Even though we're removing WordPress as a dependency, keeping import capability: +1. Allows migration from other WordPress sites +2. Useful for clients moving from WP to Host Hub +3. Reference for future CMS integrations (Ghost, Strapi, etc.) + +### MCP as the Future + +The content MCP tools are strategic: +- Agents can create content without UI +- Enables automated content pipelines +- Positions Host UK as "AI-stabilised hosting" leader +- Content becomes programmable infrastructure + +### Content Type Clarification + +- `wordpress` — Legacy, synced from WordPress (to be migrated) +- `native` — Created in Host Hub (new default) +- `hostuk` — Alias for native (backwards compat) +- `satellite` — Per-service content (e.g., BioHost-specific help) + +--- + +*This task transforms content from a synced dependency to owned infrastructure.* diff --git a/changelog/2026/jan/TASK-content-api-backend.md b/changelog/2026/jan/TASK-content-api-backend.md new file mode 100644 index 0000000..8fd0456 --- /dev/null +++ b/changelog/2026/jan/TASK-content-api-backend.md @@ -0,0 +1,1012 @@ +# TASK: Content API Backend for AI Pipeline + +**Status:** Ready for Implementation +**Priority:** P1 +**Estimated Effort:** 2-3 weeks +**Dependencies:** Mod/Content (native CMS), external HTTP clients + +--- + +## Overview + +Build Laravel API endpoints at `host.uk.com` to orchestrate the AI content pipeline. This serves as the bridge between external requests, AI services, and the native CMS (Mod/Content). + +--- + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ External Requests │ +│ (Webhooks, API Calls, Scheduled Jobs) │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ host.uk.com Laravel API │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │ +│ │ Content │ │ AI Gateway │ │ Mod/Content │ │ Mod/Social │ │ +│ │ Briefs │ │ (Gemini/ │ │ Native CMS │ │ Scheduler │ │ +│ │ Queue │ │ Claude) │ │ │ │ │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ └────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ │ │ │ + ▼ ▼ ▼ ▼ +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Database │ │ Gemini/ │ │ Mod/Content │ │ Mod/Social │ +│ (Briefs, │ │ Claude │ │ ContentItem │ │ Posts & │ +│ Queue) │ │ APIs │ │ Model │ │ Accounts │ +└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ +``` + +--- + +## Database Schema + +### Migration 1: Content Briefs + +```php +// database/migrations/xxxx_create_content_briefs_table.php + +Schema::create('content_briefs', function (Blueprint $table) { + $table->id(); + $table->string('service'); // social, link, analytics, trust, notify + $table->string('content_type'); // help_article, blog_post, landing_page + $table->string('title'); + $table->string('slug')->nullable(); + $table->text('description')->nullable(); + $table->json('keywords')->nullable(); + $table->string('category')->nullable(); + $table->string('difficulty')->nullable(); // beginner, intermediate, advanced + $table->integer('target_word_count')->default(1000); + $table->json('prompt_variables')->nullable(); // Additional context + $table->string('status')->default('pending'); // pending, queued, generating, review, published + $table->integer('priority')->default(50); // 1-100, higher = more urgent + $table->timestamp('scheduled_for')->nullable(); + $table->timestamps(); + + $table->index(['service', 'status']); + $table->index(['status', 'priority']); +}); +``` + +### Migration 2: Content Queue + +```php +// database/migrations/xxxx_create_content_queue_table.php + +Schema::create('content_queue', function (Blueprint $table) { + $table->id(); + $table->foreignId('brief_id')->constrained('content_briefs')->onDelete('cascade'); + $table->string('stage'); // draft, refine, review, publish + $table->text('gemini_output')->nullable(); + $table->text('claude_output')->nullable(); + $table->text('final_content')->nullable(); + $table->json('metadata')->nullable(); // frontmatter, seo data + $table->integer('content_item_id')->nullable(); + $table->string('content_status')->nullable(); + $table->json('generation_log')->nullable(); // Track AI calls, costs + $table->timestamp('generated_at')->nullable(); + $table->timestamp('refined_at')->nullable(); + $table->timestamp('published_at')->nullable(); + $table->timestamps(); + + $table->index('stage'); +}); +``` + +### Migration 3: Social Queue + +```php +// database/migrations/xxxx_create_social_queue_table.php + +Schema::create('social_queue', function (Blueprint $table) { + $table->id(); + $table->foreignId('content_id')->nullable()->constrained('content_queue')->onDelete('set null'); + $table->string('platform'); // twitter, linkedin, instagram, facebook + $table->text('content'); + $table->string('media_url')->nullable(); + $table->json('metadata')->nullable(); + $table->timestamp('scheduled_for'); + $table->string('status')->default('pending'); // pending, scheduled, published, failed + $table->string('social_post_id')->nullable(); + $table->timestamps(); + + $table->index(['platform', 'status']); + $table->index('scheduled_for'); +}); +``` + +### Migration 4: AI Usage Tracking + +```php +// database/migrations/xxxx_create_ai_usage_table.php + +Schema::create('ai_usage', function (Blueprint $table) { + $table->id(); + $table->string('provider'); // gemini, claude, openai + $table->string('model'); + $table->string('purpose'); // draft, refine, social, image + $table->integer('input_tokens')->default(0); + $table->integer('output_tokens')->default(0); + $table->decimal('cost_estimate', 10, 6)->default(0); + $table->foreignId('brief_id')->nullable()->constrained('content_briefs')->onDelete('set null'); + $table->json('metadata')->nullable(); + $table->timestamps(); + + $table->index(['provider', 'created_at']); +}); +``` + +--- + +## API Endpoints + +### Content Briefs API + +```php +// routes/api.php + +Route::prefix('content')->middleware(['auth:sanctum'])->group(function () { + // Briefs + Route::get('briefs', [ContentBriefController::class, 'index']); + Route::post('briefs', [ContentBriefController::class, 'store']); + Route::get('briefs/{brief}', [ContentBriefController::class, 'show']); + Route::put('briefs/{brief}', [ContentBriefController::class, 'update']); + Route::delete('briefs/{brief}', [ContentBriefController::class, 'destroy']); + Route::post('briefs/bulk', [ContentBriefController::class, 'bulkStore']); + Route::get('briefs/next', [ContentBriefController::class, 'next']); // For external callers + + // Queue + Route::get('queue', [ContentQueueController::class, 'index']); + Route::get('queue/{item}', [ContentQueueController::class, 'show']); + Route::post('queue/{item}/generate', [ContentQueueController::class, 'generate']); + Route::post('queue/{item}/refine', [ContentQueueController::class, 'refine']); + Route::post('queue/{item}/publish', [ContentQueueController::class, 'publish']); + Route::post('queue/{item}/approve', [ContentQueueController::class, 'approve']); + Route::post('queue/{item}/reject', [ContentQueueController::class, 'reject']); + + // Generation (for external callers) + Route::post('generate/draft', [GenerationController::class, 'draft']); + Route::post('generate/refine', [GenerationController::class, 'refine']); + Route::post('generate/social', [GenerationController::class, 'socialPosts']); +}); +``` + +### Content Integration API (Mod/Content) + +```php +Route::prefix('content-items')->middleware(['auth:sanctum'])->group(function () { + Route::get('sites', [ContentController::class, 'sites']); + Route::get('items/{site}', [ContentController::class, 'items']); + Route::post('items/{site}', [ContentController::class, 'createItem']); + Route::put('items/{site}/{id}', [ContentController::class, 'updateItem']); + Route::post('items/{site}/{id}/publish', [ContentController::class, 'publishItem']); + Route::delete('items/{site}/{id}', [ContentController::class, 'deleteItem']); + Route::get('categories/{site}', [ContentController::class, 'categories']); + Route::get('media/{site}', [ContentController::class, 'media']); +}); +``` + +### Social Scheduling API + +```php +Route::prefix('social')->middleware(['auth:sanctum'])->group(function () { + Route::get('queue', [SocialQueueController::class, 'index']); + Route::post('queue', [SocialQueueController::class, 'store']); + Route::post('queue/bulk', [SocialQueueController::class, 'bulkStore']); + Route::put('queue/{item}', [SocialQueueController::class, 'update']); + Route::delete('queue/{item}', [SocialQueueController::class, 'destroy']); + Route::post('queue/{item}/schedule', [SocialQueueController::class, 'schedule']); +}); +``` + +### Analytics/Stats API + +```php +Route::prefix('stats')->middleware(['auth:sanctum'])->group(function () { + Route::get('content', [StatsController::class, 'contentStats']); + Route::get('ai-usage', [StatsController::class, 'aiUsage']); + Route::get('pipeline', [StatsController::class, 'pipelineStatus']); +}); +``` + +--- + +## Services + +### AI Gateway Service + +```php +// app/Services/AIGatewayService.php + + 'application/json', + ])->post("https://generativelanguage.googleapis.com/v1/models/gemini-pro:generateContent?key={$this->geminiApiKey}", [ + 'contents' => [ + ['parts' => [['text' => $prompt]]] + ], + 'generationConfig' => [ + 'temperature' => $options['temperature'] ?? 0.7, + 'maxOutputTokens' => $options['max_tokens'] ?? 4096, + ] + ]); + + $result = $response->json(); + + // Log usage + $this->logUsage('gemini', 'gemini-pro', 'draft', $prompt, $result, $options['brief_id'] ?? null); + + return [ + 'success' => $response->successful(), + 'content' => $result['candidates'][0]['content']['parts'][0]['text'] ?? null, + 'raw' => $result, + 'duration' => microtime(true) - $startTime, + ]; + } + + /** + * Refine content using Claude + */ + public function refineContent(string $content, string $refinementPrompt, array $options = []): array + { + $startTime = microtime(true); + + $response = Http::withHeaders([ + 'x-api-key' => $this->claudeApiKey, + 'anthropic-version' => '2024-01-01', + 'Content-Type' => 'application/json', + ])->post('https://api.anthropic.com/v1/messages', [ + 'model' => $options['model'] ?? 'claude-3-opus-20240229', + 'max_tokens' => $options['max_tokens'] ?? 4096, + 'messages' => [ + [ + 'role' => 'user', + 'content' => $refinementPrompt . "\n\n---\n\n" . $content + ] + ] + ]); + + $result = $response->json(); + + // Log usage + $this->logUsage('claude', $options['model'] ?? 'claude-3-opus', 'refine', $refinementPrompt, $result, $options['brief_id'] ?? null); + + return [ + 'success' => $response->successful(), + 'content' => $result['content'][0]['text'] ?? null, + 'raw' => $result, + 'duration' => microtime(true) - $startTime, + ]; + } + + /** + * Generate social media posts from content + */ + public function generateSocialPosts(string $content, string $title, string $url): array + { + $prompt = <<generateDraft($prompt, ['temperature' => 0.8]); + } + + protected function logUsage(string $provider, string $model, string $purpose, string $input, array $result, ?int $briefId): void + { + // Estimate tokens (rough calculation) + $inputTokens = (int) (strlen($input) / 4); + $outputTokens = isset($result['content'][0]['text']) + ? (int) (strlen($result['content'][0]['text']) / 4) + : (isset($result['candidates'][0]['content']['parts'][0]['text']) + ? (int) (strlen($result['candidates'][0]['content']['parts'][0]['text']) / 4) + : 0); + + // Cost estimates (approximate) + $costPerInputToken = match($provider) { + 'gemini' => 0.000001, + 'claude' => 0.000015, + default => 0.00001, + }; + $costPerOutputToken = match($provider) { + 'gemini' => 0.000002, + 'claude' => 0.000075, + default => 0.00003, + }; + + AIUsage::create([ + 'provider' => $provider, + 'model' => $model, + 'purpose' => $purpose, + 'input_tokens' => $inputTokens, + 'output_tokens' => $outputTokens, + 'cost_estimate' => ($inputTokens * $costPerInputToken) + ($outputTokens * $costPerOutputToken), + 'brief_id' => $briefId, + 'metadata' => [ + 'usage' => $result['usage'] ?? null, + ], + ]); + } +} +``` + +### Content Client Service (Mod/Content) + +```php +// app/Mod/Content/Services/ContentClientService.php + + 'social.host.uk.com', + 'link' => 'link.host.uk.com', + 'analytics' => 'analytics.host.uk.com', + 'trust' => 'trust.host.uk.com', + 'notify' => 'notify.host.uk.com', + ]; + + public function getItems(string $site, array $params = []): array + { + $cacheKey = "content_items_{$site}_" . md5(serialize($params)); + + return Cache::remember($cacheKey, 300, function () use ($site, $params) { + return ContentItem::query() + ->where('site', $site) + ->when($params['status'] ?? null, fn($q, $s) => $q->where('status', $s)) + ->limit($params['per_page'] ?? 20) + ->get() + ->toArray(); + }); + } + + public function createItem(string $site, array $data): array + { + $item = ContentItem::create([ + 'site' => $site, + 'title' => $data['title'], + 'content' => $data['content'], + 'excerpt' => $data['excerpt'] ?? '', + 'status' => $data['status'] ?? 'draft', + 'categories' => $data['categories'] ?? [], + 'tags' => $data['tags'] ?? [], + 'meta' => $data['meta'] ?? [], + ]); + + // Clear cache + Cache::forget("content_items_{$site}_*"); + + return [ + 'success' => true, + 'item' => $item->toArray(), + 'id' => $item->id, + ]; + } + + public function updateItem(string $site, int $itemId, array $data): array + { + $item = ContentItem::where('site', $site)->findOrFail($itemId); + $item->update($data); + + Cache::forget("content_items_{$site}_*"); + + return [ + 'success' => true, + 'item' => $item->fresh()->toArray(), + ]; + } + + public function publishItem(string $site, int $itemId): array + { + return $this->updateItem($site, $itemId, ['status' => 'published']); + } + + public function getCategories(string $site): array + { + return Cache::remember("content_categories_{$site}", 3600, function () use ($site) { + return ContentItem::where('site', $site) + ->distinct('category') + ->pluck('category') + ->toArray(); + }); + } +} +``` + +### Social Scheduler Service (Mod/Social) + +```php +// app/Mod/Social/Services/SocialSchedulerService.php + + $data['content'], + 'media' => $data['media'] ?? [], + 'accounts' => $data['accounts'], // Platform account IDs + 'scheduled_at' => $data['scheduled_at'], + 'status' => 'scheduled', + ]); + + return [ + 'success' => true, + 'post' => $post->toArray(), + 'id' => $post->id, + ]; + } + + public function getAccounts(): array + { + return Account::all()->toArray(); + } + + public function getScheduledPosts(array $params = []): array + { + return Post::query() + ->where('status', 'scheduled') + ->when($params['account_id'] ?? null, fn($q, $id) => $q->where('account_id', $id)) + ->orderBy('scheduled_at') + ->get() + ->toArray(); + } +} +``` + +--- + +## Controllers + +### Content Brief Controller + +```php +// app/Http/Controllers/Api/ContentBriefController.php + +when($request->service, fn($q, $s) => $q->where('service', $s)) + ->when($request->status, fn($q, $s) => $q->where('status', $s)) + ->when($request->type, fn($q, $t) => $q->where('content_type', $t)) + ->orderBy('priority', 'desc') + ->orderBy('created_at', 'asc') + ->paginate($request->per_page ?? 20); + + return response()->json($briefs); + } + + public function store(ContentBriefRequest $request) + { + $brief = ContentBrief::create($request->validated()); + + return response()->json($brief, 201); + } + + public function bulkStore(Request $request) + { + $request->validate([ + 'briefs' => 'required|array', + 'briefs.*.service' => 'required|string', + 'briefs.*.content_type' => 'required|string', + 'briefs.*.title' => 'required|string', + ]); + + $briefs = collect($request->briefs)->map(function ($data) { + return ContentBrief::create($data); + }); + + return response()->json([ + 'created' => $briefs->count(), + 'briefs' => $briefs, + ], 201); + } + + /** + * Get next brief to process (for external callers) + */ + public function next(Request $request) + { + $brief = ContentBrief::query() + ->where('status', 'pending') + ->when($request->service, fn($q, $s) => $q->where('service', $s)) + ->orderBy('priority', 'desc') + ->orderBy('scheduled_for', 'asc') + ->orderBy('created_at', 'asc') + ->first(); + + if (!$brief) { + return response()->json(['message' => 'No briefs pending'], 404); + } + + // Mark as queued + $brief->update(['status' => 'queued']); + + return response()->json($brief); + } + + public function show(ContentBrief $brief) + { + return response()->json($brief->load('queueItem')); + } + + public function update(ContentBriefRequest $request, ContentBrief $brief) + { + $brief->update($request->validated()); + + return response()->json($brief); + } + + public function destroy(ContentBrief $brief) + { + $brief->delete(); + + return response()->json(null, 204); + } +} +``` + +### Generation Controller (for external callers) + +```php +// app/Http/Controllers/Api/GenerationController.php + +validate([ + 'brief_id' => 'required|exists:content_briefs,id', + ]); + + $brief = ContentBrief::findOrFail($request->brief_id); + + // Load prompt template + $promptTemplate = $this->getPromptTemplate($brief->content_type); + $prompt = $this->buildPrompt($promptTemplate, $brief); + + // Generate with Gemini + $result = $this->ai->generateDraft($prompt, [ + 'brief_id' => $brief->id, + ]); + + if (!$result['success']) { + return response()->json(['error' => 'Generation failed', 'details' => $result], 500); + } + + // Create or update queue item + $queueItem = ContentQueue::updateOrCreate( + ['brief_id' => $brief->id], + [ + 'stage' => 'draft', + 'gemini_output' => $result['content'], + 'generated_at' => now(), + 'generation_log' => [ + 'gemini' => [ + 'duration' => $result['duration'], + 'timestamp' => now()->toIso8601String(), + ] + ], + ] + ); + + $brief->update(['status' => 'generating']); + + return response()->json([ + 'success' => true, + 'queue_item' => $queueItem, + 'content' => $result['content'], + ]); + } + + /** + * Refine content (Claude) + */ + public function refine(Request $request) + { + $request->validate([ + 'queue_id' => 'required|exists:content_queue,id', + ]); + + $queueItem = ContentQueue::with('brief')->findOrFail($request->queue_id); + + if (!$queueItem->gemini_output) { + return response()->json(['error' => 'No draft to refine'], 400); + } + + // Load refinement prompt + $refinementPrompt = $this->getRefinementPrompt($queueItem->brief->content_type); + + // Refine with Claude + $result = $this->ai->refineContent( + $queueItem->gemini_output, + $refinementPrompt, + ['brief_id' => $queueItem->brief_id] + ); + + if (!$result['success']) { + return response()->json(['error' => 'Refinement failed', 'details' => $result], 500); + } + + // Update queue item + $queueItem->update([ + 'stage' => 'review', + 'claude_output' => $result['content'], + 'final_content' => $result['content'], + 'refined_at' => now(), + 'generation_log' => array_merge($queueItem->generation_log ?? [], [ + 'claude' => [ + 'duration' => $result['duration'], + 'timestamp' => now()->toIso8601String(), + ] + ]), + ]); + + $queueItem->brief->update(['status' => 'review']); + + return response()->json([ + 'success' => true, + 'queue_item' => $queueItem->fresh(), + 'content' => $result['content'], + ]); + } + + /** + * Generate social posts for content + */ + public function socialPosts(Request $request) + { + $request->validate([ + 'queue_id' => 'required|exists:content_queue,id', + 'url' => 'required|url', + ]); + + $queueItem = ContentQueue::with('brief')->findOrFail($request->queue_id); + + $result = $this->ai->generateSocialPosts( + substr($queueItem->final_content, 0, 2000), // Summary + $queueItem->brief->title, + $request->url + ); + + if (!$result['success']) { + return response()->json(['error' => 'Social generation failed'], 500); + } + + $posts = json_decode($result['content'], true); + + return response()->json([ + 'success' => true, + 'posts' => $posts, + ]); + } + + protected function getPromptTemplate(string $type): string + { + $templates = [ + 'help_article' => Storage::disk('local')->get('prompts/help-article.txt'), + 'blog_post' => Storage::disk('local')->get('prompts/blog-post.txt'), + 'landing_page' => Storage::disk('local')->get('prompts/landing-page.txt'), + ]; + + return $templates[$type] ?? $templates['help_article']; + } + + protected function getRefinementPrompt(string $type): string + { + return Storage::disk('local')->get('prompts/refinement.txt'); + } + + protected function buildPrompt(string $template, ContentBrief $brief): string + { + $variables = array_merge([ + 'SERVICE_NAME' => $this->getServiceName($brief->service), + 'SERVICE_URL' => $this->getServiceUrl($brief->service), + 'TITLE' => $brief->title, + 'DESCRIPTION' => $brief->description ?? '', + 'KEYWORDS' => implode(', ', $brief->keywords ?? []), + 'CATEGORY' => $brief->category ?? '', + 'DIFFICULTY' => $brief->difficulty ?? 'beginner', + 'WORD_COUNT' => $brief->target_word_count, + ], $brief->prompt_variables ?? []); + + foreach ($variables as $key => $value) { + $template = str_replace('{{' . $key . '}}', $value, $template); + } + + return $template; + } + + protected function getServiceName(string $service): string + { + return match($service) { + 'social' => 'Host Social', + 'link' => 'Host Link', + 'analytics' => 'Host Analytics', + 'trust' => 'Host Trust', + 'notify' => 'Host Notify', + default => 'Host UK', + }; + } + + protected function getServiceUrl(string $service): string + { + return match($service) { + 'social' => 'social.host.uk.com', + 'link' => 'link.host.uk.com', + 'analytics' => 'analytics.host.uk.com', + 'trust' => 'trust.host.uk.com', + 'notify' => 'notify.host.uk.com', + default => 'host.uk.com', + }; + } +} +``` + +--- + +## Configuration + +### Environment Variables + +```env +# AI Services +GEMINI_API_KEY=your_gemini_api_key +ANTHROPIC_API_KEY=your_claude_api_key + +# External Webhook Secret (for validation) +CONTENT_WEBHOOK_SECRET=your_webhook_secret +``` + +### Config File + +```php +// config/content.php + +return [ + 'ai' => [ + 'gemini' => [ + 'api_key' => env('GEMINI_API_KEY'), + 'model' => 'gemini-pro', + 'max_tokens' => 4096, + ], + 'claude' => [ + 'api_key' => env('ANTHROPIC_API_KEY'), + 'model' => 'claude-3-opus-20240229', + 'max_tokens' => 4096, + ], + ], + + 'sites' => [ + 'social' => 'social.host.uk.com', + 'link' => 'link.host.uk.com', + 'analytics' => 'analytics.host.uk.com', + 'trust' => 'trust.host.uk.com', + 'notify' => 'notify.host.uk.com', + ], + + 'webhook' => [ + 'secret' => env('CONTENT_WEBHOOK_SECRET'), + ], + + 'defaults' => [ + 'word_count' => 1000, + 'priority' => 50, + ], +]; +``` + +--- + +## Implementation Checklist + +### Phase 1: Core Infrastructure (Week 1) + +- [ ] **Database** + - [ ] Create migrations + - [ ] Create models with relationships + - [ ] Create factories for testing + +- [ ] **Services** + - [ ] AIGatewayService (Gemini + Claude) + - [ ] ContentClientService (Mod/Content) + - [ ] SocialSchedulerService (Mod/Social) + +- [ ] **Basic API** + - [ ] ContentBrief CRUD + - [ ] ContentQueue management + - [ ] Authentication (Sanctum) + +### Phase 2: Generation Pipeline (Week 2) + +- [ ] **Generation Controller** + - [ ] Draft endpoint (Gemini) + - [ ] Refine endpoint (Claude) + - [ ] Social posts endpoint + +- [ ] **Prompt Management** + - [ ] Store prompt templates + - [ ] Variable substitution + - [ ] Service-specific prompts + +- [ ] **Content Publishing (Mod/Content)** + - [ ] Create draft items + - [ ] Update existing items + - [ ] Publish workflow + +### Phase 3: Integration (Week 3) + +- [ ] **External Webhooks** + - [ ] Webhook endpoints for external callers + - [ ] Secret validation + - [ ] Response formatting + +- [ ] **Social Integration (Mod/Social)** + - [ ] Schedule social posts + - [ ] Account management + - [ ] Post status tracking + +- [ ] **Monitoring** + - [ ] AI usage tracking + - [ ] Cost reporting + - [ ] Error logging + +### Phase 4: Polish + +- [ ] **Admin Dashboard** (optional) + - [ ] Brief management UI + - [ ] Queue status view + - [ ] Analytics dashboard + +- [ ] **Testing** + - [ ] Unit tests for services + - [ ] Feature tests for API + - [ ] Integration tests + +- [ ] **Documentation** + - [ ] API documentation + - [ ] External integration examples + - [ ] Deployment guide + +--- + +## External Integration Endpoints Summary + +| Endpoint | Method | Purpose | +|----------------------------------|--------|----------------------------| +| `/api/content/briefs/next` | GET | Get next brief to process | +| `/api/content/generate/draft` | POST | Generate draft with Gemini | +| `/api/content/generate/refine` | POST | Refine with Claude | +| `/api/content/generate/social` | POST | Generate social posts | +| `/api/content/queue/{id}/publish`| POST | Publish to Mod/Content | +| `/api/social/queue` | POST | Schedule via Mod/Social | + +--- + +## File Structure + +``` +app/ +├── Http/ +│ ├── Controllers/ +│ │ └── Api/ +│ │ ├── ContentBriefController.php +│ │ ├── ContentQueueController.php +│ │ ├── GenerationController.php +│ │ ├── SocialQueueController.php +│ │ └── StatsController.php +│ └── Requests/ +│ └── ContentBriefRequest.php +├── Models/ +│ ├── ContentBrief.php +│ ├── ContentQueue.php +│ ├── SocialQueue.php +│ └── AIUsage.php +├── Mod/ +│ ├── Content/ +│ │ └── Services/ +│ │ └── ContentClientService.php +│ └── Social/ +│ └── Services/ +│ └── SocialSchedulerService.php +└── Services/ + └── AIGatewayService.php + +database/ +└── migrations/ + ├── xxxx_create_content_briefs_table.php + ├── xxxx_create_content_queue_table.php + ├── xxxx_create_social_queue_table.php + └── xxxx_create_ai_usage_table.php + +storage/ +└── app/ + └── prompts/ + ├── help-article.txt + ├── blog-post.txt + ├── landing-page.txt + └── refinement.txt + +config/ +└── content.php +``` diff --git a/changelog/2026/jan/code-review.md b/changelog/2026/jan/code-review.md new file mode 100644 index 0000000..bdde8be --- /dev/null +++ b/changelog/2026/jan/code-review.md @@ -0,0 +1,130 @@ +# Content Module Review + +**Updated:** 2026-01-21 - All recommended improvements implemented + +## Overview + +The Content module manages the content system for Host UK, providing: + +1. **Native Content Management** - ContentItem, ContentAuthor, ContentMedia, ContentTaxonomy, ContentRevision models for storing and managing content natively +2. **AI Content Generation** - Two-stage pipeline using Gemini (draft) + Claude (refinement) via ContentBrief and AIGatewayService +3. **WordPress Import** - One-time import from WordPress sites via REST API (ContentImportWordPress command) +4. **Public Rendering** - Satellite pages for blog/help content via Livewire components +5. **Content Processing** - HTML cleaning and JSON block parsing for headless rendering + +The module is transitioning away from WordPress sync (deprecated) to a fully native content system with AI-assisted content generation. + +## Production Readiness Score: 92/100 (was 85/100 - All recommended improvements implemented 2026-01-21) + +## Critical Issues (Must Fix) + +- [x] **Missing database migrations for core tables** - FIXED: Created `2026_01_14_000001_create_content_core_tables.php` migration documenting all 7 content tables with `Schema::hasTable()` guards. + +- [x] **ContentBrief model missing factory** - FIXED: Created `ContentBriefFactory.php` with states for status, content types, and priorities. + +- [x] **AIUsage model missing factory** - FIXED: Created `AIUsageFactory.php` with states for providers, purposes, and usage amounts. + +- [x] **Content rendering XSS vulnerability** - FIXED: All blade templates now use `{{ }}` for titles. Added `getSanitisedContent()` method using HTMLPurifier for body content. + +- [x] **Waitlist stored in cache only** - FIXED: `ContentRender` now uses `WaitlistEntry` model from Tenant module for persistent database storage. + +## Recommended Improvements + +- [x] **Add rate limiting to API endpoints** - FIXED: Rate limiting added to `/api/v1/content/generate/*` endpoints. + +- [x] **Add validation for content_type in ContentBrief** - FIXED: BriefContentType enum added for type safety. + +- [x] **ContentProcessingService error handling** - FIXED: DOM parsing errors now logged for debugging. + +- [x] **Cache key collision potential** - FIXED: Cache key sanitisation added to ContentRender to handle special characters in slugs. + +- [x] **Add indexes to ContentBrief table** - FIXED: Index added for `scheduled_for` column. + +- [x] **Job timeout configuration** - FIXED: `GenerateContentJob` timeout now configurable for different content types. + +- [x] **AIGatewayService constructor signature** - FIXED: Refactored to read config() fresh in getGemini()/getClaude() methods. Constructor now only stores optional overrides. Added resetServices() method for runtime config changes. Verified implementation shows clean separation of override vs config-based configuration. + +- [x] **Help component queries pages not help articles** - FIXED: Help.php now uses `helpArticles` scope for proper content filtering. + +- [x] **ContentRevision lacks pruning mechanism** - FIXED: Revision pruning command added with configurable retention policy. + +- [x] **Deprecated commands still registered** - VERIFIED: ContentSync and ContentPublish already have proper deprecation warnings via trigger_error(E_USER_DEPRECATED) and console output. + +## Missing Features (Future) + +- [x] **Content scheduling system** - FIXED: Created `PublishScheduledContent` command (`content:publish-scheduled`) that uses `readyToPublish()` scope. Supports `--dry-run` and `--limit` options. Publishes 'future' status items whose `publish_at` has passed. Scheduled to run every minute via routes/console.php. + +- [ ] **Media upload endpoint** - No API endpoint for uploading media. Only WordPress import creates ContentMedia records. + +- [ ] **Content search** - No full-text search capability. Consider adding a search endpoint with elasticsearch/meilisearch integration. + +- [ ] **Revision comparison/diff view** - `ContentRevision::getDiffSummary()` exists but returns boolean changed flags, not actual diff content. + +- [ ] **Webhook handler** - ContentWebhookLog model exists but no webhook endpoint or processing job to receive/handle webhooks. + +- [ ] **CDN cache purge integration** - `getCdnUrlsForPurgeAttribute()` generates purge URLs but no integration to actually call Bunny CDN purge API. + +- [ ] **Content versioning API** - No API endpoints for listing/restoring revisions. + +- [ ] **MCP tools** - `onMcpTools()` is stubbed out with commented examples. MCP integration incomplete. + +- [ ] **Bulk operations** - No bulk publish, bulk delete, or bulk status change operations. + +- [ ] **Content preview** - No preview endpoint for draft content before publishing. + +## Test Coverage Assessment + +**Current Coverage: Good for models, sparse for services** + +Existing tests: +- `ContentRenderTest.php` - Tests ContentRender service, waitlist, workspace resolution (7 tests) +- `FactoriesTest.php` - Tests all model factories and computed properties (31 tests) +- `ContentManagerTest.php` - Tests model relationships, scopes, and queries (29 tests) + +Missing test coverage: +- [ ] No tests for `AIGatewayService` - critical service, needs mocked tests +- [ ] No tests for `ContentProcessingService` - HTML parsing/cleaning needs test cases +- [ ] No tests for API controllers (`ContentBriefController`, `GenerationController`) +- [ ] No tests for `GenerateContentJob` queue job +- [ ] No tests for `ContentImportWordPress` command +- [ ] No integration tests for the full AI generation pipeline +- [ ] No tests for `ContentBrief` model (has no factory) +- [ ] No tests for `ContentRevision::createFromContentItem()` or `restoreToContentItem()` + +## Security Concerns + +1. **XSS in blade templates** - FIXED: Raw HTML output now uses sanitised content methods. + +2. **No CSRF on subscribe endpoint** - The `WorkspaceRouter` handles POST /subscribe but the route bypasses middleware that would normally apply CSRF. Verify CSRF is checked. + +3. **API authentication relies on middleware** - Routes use `auth` and `api.auth` middleware. Verify these are properly configured and cannot be bypassed. + +4. **WordPress import stores arbitrary content** - `ContentImportWordPress` imports HTML content as-is. Could contain malicious scripts. Should sanitise on import. + +5. **No authorisation checks on ContentBrief** - Controllers check workspace access but no policy/gate for fine-grained permissions (e.g., only editors can approve, only admins can delete). + +6. **AI API keys in memory** - `AIGatewayService` stores API keys as instance properties. Not a major concern but consider using Laravel's encrypted config if storing in database. + +## Notes + +1. **Architecture clarity** - Good separation of concerns: Models handle data, Services handle business logic, Controllers handle HTTP, Jobs handle async. Clean module structure. + +2. **WordPress transition** - The module is mid-transition from WordPress to native content. Some WordPress-specific code remains (ContentType::WORDPRESS, wp_id fields, import command). Deprecation notices are good. + +3. **AI pipeline design** - The two-stage Gemini->Claude pipeline is well-designed for cost optimisation. Good use of usage tracking. + +4. **ContentType enum** - Well-implemented enum with utility methods (label, color, icon, isNative). Good pattern. + +5. **Content processing** - `ContentProcessingService` is comprehensive with proper DOM handling. Could be extracted to a shared package. + +6. **Livewire components** - Follow consistent pattern. Consider extracting common workspace resolution logic to a trait. + +7. **Hardcoded paths** - `ContentValidate` and `ContentGenerate` commands reference `base_path('doc/phase42/drafts')`. Should be configurable. + +8. **Dependencies on other modules** - Module depends on: + - `Mod\Tenant` (Workspace, User) + - `Mod\Agentic` (ContentService, Prompt, ClaudeService, GeminiService, AgenticResponse) + - `Mod\Api` (HasApiResponses, ResolvesWorkspace) + - `Core` (Controller, events, HasSeoMetadata) + +9. **Model pricing outdated** - `AIUsage::$pricing` will need regular updates as AI providers change pricing. diff --git a/changelog/2026/jan/features.md b/changelog/2026/jan/features.md new file mode 100644 index 0000000..049a367 --- /dev/null +++ b/changelog/2026/jan/features.md @@ -0,0 +1,71 @@ +# Core-Content - January 2026 + +## Features Implemented + +### Native CMS (TASK-004) + +Complete content management system replacing WordPress dependency. + +**Features:** +- Post/page content types +- Rich text editor +- Media management +- Categories and tags +- SEO metadata +- Scheduling + +**Models:** +- `ContentItem` - posts, pages +- `ContentCategory` +- `ContentTag` +- `ContentMedia` + +--- + +### Bulk Operations + +Multi-select content management. + +**Features:** +- Bulk publish/unpublish +- Bulk delete +- ContentManager UI + +--- + +### CDN Cache Purge + +Automatic CDN invalidation on publish. + +**Files:** +- `Services/CdnPurgeService.php` +- `Observers/ContentItemObserver.php` + +--- + +### Content Preview + +Preview unpublished content with time-limited tokens. + +**Files:** +- Preview endpoint +- Preview UI in editor + +--- + +### Versioning API + +Content revision history. + +**Files:** +- `Controllers/ContentRevisionController.php` +- List, show, restore, compare endpoints + +--- + +### Media Upload API + +Full REST API for content media. + +**Files:** +- `Controllers/ContentMediaController.php` diff --git a/composer.json b/composer.json index 4deb1f4..2d22fa3 100644 --- a/composer.json +++ b/composer.json @@ -14,18 +14,18 @@ }, "autoload": { "psr-4": { - "Core\\Content\\": "" + "Core\\Mod\\Content\\": "" } }, "autoload-dev": { "psr-4": { - "Core\\Content\\Tests\\": "Tests/" + "Core\\Mod\\Content\\Tests\\": "Tests/" } }, "extra": { "laravel": { "providers": [ - "Core\\Content\\Boot" + "Core\\Mod\\Content\\Boot" ] } }, diff --git a/routes/api.php b/routes/api.php index e58c4bc..31edce4 100644 --- a/routes/api.php +++ b/routes/api.php @@ -10,13 +10,13 @@ declare(strict_types=1); */ use Illuminate\Support\Facades\Route; -use Core\Content\Controllers\Api\ContentBriefController; -use Core\Content\Controllers\Api\ContentMediaController; -use Core\Content\Controllers\Api\ContentRevisionController; -use Core\Content\Controllers\Api\ContentSearchController; -use Core\Content\Controllers\Api\ContentWebhookController; -use Core\Content\Controllers\Api\GenerationController; -use Core\Content\Controllers\ContentPreviewController; +use Core\Mod\Content\Controllers\Api\ContentBriefController; +use Core\Mod\Content\Controllers\Api\ContentMediaController; +use Core\Mod\Content\Controllers\Api\ContentRevisionController; +use Core\Mod\Content\Controllers\Api\ContentSearchController; +use Core\Mod\Content\Controllers\Api\ContentWebhookController; +use Core\Mod\Content\Controllers\Api\GenerationController; +use Core\Mod\Content\Controllers\ContentPreviewController; /* |-------------------------------------------------------------------------- diff --git a/routes/web.php b/routes/web.php index ee7371e..1e99874 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,11 +3,11 @@ declare(strict_types=1); use Illuminate\Support\Facades\Route; -use Core\Content\View\Modal\Web\Blog; -use Core\Content\View\Modal\Web\Help; -use Core\Content\View\Modal\Web\HelpArticle; -use Core\Content\View\Modal\Web\Post; -use Core\Content\View\Modal\Web\Preview; +use Core\Mod\Content\View\Modal\Web\Blog; +use Core\Mod\Content\View\Modal\Web\Help; +use Core\Mod\Content\View\Modal\Web\HelpArticle; +use Core\Mod\Content\View\Modal\Web\Post; +use Core\Mod\Content\View\Modal\Web\Preview; /* |-------------------------------------------------------------------------- diff --git a/tests/Feature/ContentManagerTest.php b/tests/Feature/ContentManagerTest.php index a7c4be2..1007b13 100644 --- a/tests/Feature/ContentManagerTest.php +++ b/tests/Feature/ContentManagerTest.php @@ -1,10 +1,10 @@