You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A Laravel 12 application for centralized WordPress site management, leveraging the jooservices/wordpress-sdk package.
🎯 Project Overview
Core Objectives
Centralized Management - Manage multiple WordPress sites from one dashboard
Story Publishing - Post templated content (Stories) to sites (Top Priority)
Offline Management - Full content management without touching live sites
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]
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'];
}
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.phpreturn [
'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