Skip to content

Latest commit

 

History

History
466 lines (395 loc) · 11.3 KB

File metadata and controls

466 lines (395 loc) · 11.3 KB

WordPress Management System - Solution Proposal

A Laravel 12 application for centralized WordPress site management, leveraging the jooservices/wordpress-sdk package.


🎯 Project Overview

Core Objectives

  1. Centralized Management - Manage multiple WordPress sites from one dashboard
  2. Story Publishing - Post templated content (Stories) to sites (Top Priority)
  3. Offline Management - Full content management without touching live sites
  4. Scalable Architecture - Ready for future feature expansion

Decisions Made

Decision Choice
UI Framework Vue.js + Inertia.js
Database MariaDB
Custom Templates Backlog (use predefined for now)
Scheduled Publishing Phase 2

🏗 Architecture Overview

Design Principles

  • Domain-Driven Design (DDD) - Separate domains for Sites, Content, Stories, Media
  • Repository Pattern - Abstract data access layer for easy testing
  • Service Layer - Business logic encapsulated in dedicated services
  • Event-Driven - Use Laravel Events for decoupled processing

Request Flow

Request → Controller → FormRequest → Service → Repository → Model → Database
                                         ↓
                                      Events → Listeners → Jobs (async)
flowchart LR
    A[HTTP Request] --> B[Controller]
    B --> C[FormRequest]
    C -->|Valid| D[Service]
    C -->|Invalid| E[422 Response]
    D --> F[Repository]
    F --> G[Model]
    G --> H[(Database)]
    D --> I[Event]
    I --> J[Listener]
    J --> K[Job Queue]
Loading

📁 Directory Structure (Laravel Standard)

app/
├── Console/
│   └── Commands/
│       ├── SyncSitesCommand.php
│       └── HealthCheckCommand.php
│
├── Enums/
│   ├── SiteStatus.php
│   ├── StoryStatus.php
│   └── PublicationStatus.php
│
├── Events/
│   ├── SiteConnected.php
│   ├── StoryCreated.php
│   └── StoryPublished.php
│
├── Http/
│   ├── Controllers/
│   │   ├── SiteController.php
│   │   ├── StoryController.php
│   │   ├── PostController.php
│   │   └── MediaController.php
│   ├── Requests/
│   │   ├── StoreSiteRequest.php
│   │   ├── StoreStoryRequest.php
│   │   └── PublishStoryRequest.php
│   └── Resources/
│       ├── SiteResource.php
│       ├── StoryResource.php
│       └── PostResource.php
│
├── Jobs/
│   ├── SyncSiteContentJob.php
│   ├── PublishStoryJob.php
│   └── UploadMediaJob.php
│
├── Listeners/
│   ├── HandleSiteConnected.php
│   └── HandleStoryPublished.php
│
├── Models/
│   ├── Site.php
│   ├── Story.php
│   ├── StoryPublication.php
│   ├── Post.php
│   ├── Category.php
│   ├── Tag.php
│   └── Media.php
│
├── Policies/
│   ├── SitePolicy.php
│   └── StoryPolicy.php
│
├── Providers/
│   ├── AppServiceProvider.php
│   ├── WordPressServiceProvider.php
│   └── RepositoryServiceProvider.php
│
├── Repositories/
│   ├── Contracts/
│   │   ├── SiteRepositoryInterface.php
│   │   ├── StoryRepositoryInterface.php
│   │   └── PostRepositoryInterface.php
│   └── Eloquent/
│       ├── SiteRepository.php
│       ├── StoryRepository.php
│       └── PostRepository.php
│
├── Services/
│   ├── SiteConnectionService.php
│   ├── StoryBuilderService.php
│   ├── StoryPublisherService.php
│   ├── ContentSyncService.php
│   └── MediaUploadService.php
│
└── Support/
    └── WordPress/
        ├── ClientFactory.php
        └── ConnectionManager.php

resources/
├── js/
│   ├── Pages/
│   │   ├── Sites/
│   │   │   ├── Index.vue
│   │   │   └── Create.vue
│   │   ├── Stories/
│   │   │   ├── Index.vue
│   │   │   ├── Create.vue
│   │   │   └── Publish.vue
│   │   └── Dashboard.vue
│   ├── Components/
│   │   ├── StoryEditor.vue
│   │   └── SiteSelector.vue
│   └── app.js
└── views/
    └── app.blade.php

🔄 Story Publishing Flows

1. Local Story Creation & Publishing

flowchart TB
    subgraph Local["Local Story Management"]
        Create[Create Story]
        Edit[Edit Story]
        Preview[Preview Rendered HTML]
        Save[(Save to DB as Draft)]
    end
    
    subgraph Publish["Publishing"]
        Select[Select Target Sites]
        Queue[Queue PublishJob per Site]
        Transform[Transform to SDK Template]
    end
    
    subgraph Remote["WordPress Site"]
        API[POST to WP REST API]
        WPPost[Created as WP Post]
    end
    
    Create --> Edit --> Preview --> Save
    Save --> Select --> Queue --> Transform --> API --> WPPost
    WPPost -.->|Store wp_post_id| Save
Loading

2. Sync FROM WordPress (Pull)

sequenceDiagram
    participant Job as SyncJob
    participant SDK as WordPress SDK
    participant WP as WordPress
    participant Repo as PostRepository
    participant DB as Database
    
    Job->>SDK: posts()->list(params)
    SDK->>WP: GET /wp-json/wp/v2/posts
    WP-->>SDK: Post[] response
    SDK-->>Job: Collection of Post DTOs
    
    loop Each WP Post
        Job->>Repo: findByWpPostId(wp_id, site_id)
        alt Exists locally
            Repo->>DB: Update local record
        else New post
            Repo->>DB: Create local record
        end
    end
Loading

3. Update & Republish Story

sequenceDiagram
    participant User
    participant Service as StoryPublisherService
    participant Repo as StoryRepo
    participant SDK as WordPress SDK
    participant WP as WordPress
    
    User->>Service: updateAndRepublish(storyId, data)
    Service->>Repo: update(storyId, data)
    Service->>Repo: getPublications(storyId)
    
    loop Each publication (site)
        Service->>SDK: posts()->update(wp_post_id, newContent)
        SDK->>WP: PUT /wp-json/wp/v2/posts/{id}
        WP-->>SDK: Updated post
        Service->>Repo: updatePublicationStatus
    end
Loading

4. Media Files Handling

flowchart LR
    subgraph Local["Local Storage"]
        Upload[Upload Media]
        Store[Store in storage/app/media]
        Meta[(Save metadata to DB)]
    end
    
    subgraph Sync["On Story Publish"]
        Check{Media exists on WP?}
        UploadWP[Upload to WordPress]
        MapID[Map local_id → wp_media_id]
    end
    
    subgraph WP["WordPress"]
        WPMedia[WP Media Library]
    end
    
    Upload --> Store --> Meta
    Meta --> Check
    Check -->|No| UploadWP --> WPMedia
    Check -->|Yes| MapID
    UploadWP --> MapID
Loading
Scenario Action
New media in story Upload to WP first, get wp_media_id, then use in post
Media already synced Use cached wp_media_id
Sync from WP Download URL, store reference (not file) locally

🔐 Multi-Site Authentication

Strategy: Encrypted Database Storage

flowchart LR
    subgraph Storage["Database Storage"]
        Cred[Encrypted Credentials]
        AppKey[Laravel APP_KEY]
    end
    
    subgraph Runtime["Runtime"]
        Decrypt[Decrypt on use]
        SDK[WordPress SDK Client]
    end
    
    Cred --> AppKey --> Decrypt --> SDK
Loading

Sites Table Schema

Schema::create('sites', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('url');
    $table->string('username');
    $table->text('password');             // Encrypted via cast
    $table->string('auth_type')->default('basic');
    $table->enum('status', ['active', 'inactive', 'error']);
    $table->timestamp('last_connected_at')->nullable();
    $table->timestamps();
});

Model with Encryption

class Site extends Model
{
    protected $casts = [
        'password' => 'encrypted',
        'status' => SiteStatus::class,
    ];
    
    protected $hidden = ['password'];
}
Method Pros Cons
Encrypted DB Simple, Laravel native Key in .env
Vault Enterprise-grade Complex setup
OAuth No password storage WP plugin required

🔌 Extension Mechanisms

Extension Points

flowchart TB
    subgraph Core["Core System"]
        Events[Laravel Events]
        Contracts[Interfaces/Contracts]
        Config[Config Files]
    end
    
    subgraph Extensions["Extension Methods"]
        Listeners[Custom Listeners]
        Impl[Custom Implementations]
        Providers[Service Providers]
    end
    
    Events --> Listeners
    Contracts --> Impl
    Config --> Providers
Loading

How to Extend

Extension Type How to Extend
New Template Implement StoryTemplateInterface, register in config
New Sync Source Implement SyncableInterface, add to SyncService
Custom Actions Listen to Events (StoryPublished, SiteConnected)
New Repositories Implement interface, rebind in ServiceProvider

Template Extension Config

// config/wordpress.php
return [
    'templates' => [
        'story' => \App\Templates\StoryTemplate::class,
        'product_review' => \App\Templates\ProductReviewTemplate::class,
        // Add new templates here
    ],
];

📊 Class Diagrams

Domain Models

classDiagram
    class Site {
        +id
        +name
        +url
        +username
        +encrypted password
        +auth_type
        +status
        +last_connected_at
    }
    
    class Story {
        +id
        +title
        +template_type
        +json template_data
        +status
    }
    
    class StoryPublication {
        +id
        +story_id
        +site_id
        +wp_post_id
        +status
        +published_at
    }
    
    class Post {
        +id
        +site_id
        +wp_post_id
        +title
        +content
        +status
    }
    
    class Media {
        +id
        +site_id
        +wp_media_id
        +title
        +url
        +mime_type
    }
    
    Site "1" --> "*" Post
    Site "1" --> "*" Media
    Story "1" --> "*" StoryPublication
    StoryPublication --> Site
Loading

🛠 Technology Stack

Component Technology
Framework Laravel 12
Frontend Vue.js + Inertia.js
Database MariaDB
Queue Redis + Laravel Horizon
Cache Redis
SDK jooservices/wordpress-sdk
Testing Pest

📋 Feature Prioritization

Phase 1: Core MVP (Story Publishing)

  • Site management (add/edit/delete)
  • Site connection testing
  • Story creation & editing
  • Story template system
  • Publish story to selected sites
  • Publication status tracking

Phase 2: Content Management

  • Post listing & sync from WordPress
  • Category/Tag management
  • Media library sync
  • Scheduled publishing

Phase 3: Advanced (Backlog)

  • Bulk publishing
  • Custom template builder
  • Content analytics
  • API for external integrations