# SEO OS — Social Media Module
## Complete Technical Implementation Spec for Claude Code

---

## 0. Module Identity

**Module Name:** Social Media Engine  
**Parent System:** SEO OS (Laravel 11 + Next.js 14)  
**Purpose:** Generate, manage, approve, schedule, and auto-publish social media posts across multiple platforms — powered by content intelligence from the SEO OS data layer (blog posts, feeds, Google Alerts).  
**Publishing Backend:** Postiz (self-hosted or cloud) via Public API v1  

### Core Flow

```
Content Sources → AI Generation → Post Queue → Approval → Scheduling → Postiz API → Published
     ↓                                                                        ↓
[Blog Posts]                                                           [Analytics Back]
[RSS Feeds]
[Google Alerts]
```

### Design Principle

Same as the rest of SEO OS: **nothing publishes without human approval.** AI generates drafts, humans approve. Auto-posting mode exists but is opt-in per channel and still respects the approval gate — it auto-publishes only posts that have been pre-approved or generated from approved templates.

---

## 1. Content Source Layer

These are the data inputs that feed the AI post generator. Each source type has its own sync mechanism and normalized storage.

### 1.1 Blog Posts (Internal Content)

Already exists in SEO OS as `pages` table. This module reads from it.

**Trigger events:**
- New blog post published (status changes to `published`)
- Existing blog post updated (content hash changes)
- Manual selection by user from page inventory

**Data extracted per blog post:**
- Title, URL, meta description, excerpt (first 300 chars)
- Main topic / primary keyword (from SEO OS analysis)
- Publication date, last modified date
- Content type tag (tutorial, news, comparison, guide, etc.)

### 1.2 RSS / Social Feeds

External content streams — Reddit threads, Twitter/X trending, industry news, competitor blogs.

#### Migration: `social_feeds`

```
id                  BIGINT PRIMARY KEY
project_id          BIGINT FK → projects
name                VARCHAR(255)              -- "Reddit /r/webhosting", "HN Front Page"
source_type         ENUM('rss', 'reddit', 'twitter_search', 'custom_api')
source_url          TEXT                       -- RSS URL or API endpoint
source_config       JSONB                      -- auth tokens, subreddit name, search query, etc.
fetch_interval_min  INTEGER DEFAULT 60         -- how often to poll (minutes)
is_active           BOOLEAN DEFAULT true
last_fetched_at     TIMESTAMP NULL
created_at          TIMESTAMP
updated_at          TIMESTAMP
```

#### Migration: `social_feed_items`

```
id                  BIGINT PRIMARY KEY
feed_id             BIGINT FK → social_feeds
external_id         VARCHAR(500)               -- dedup key (RSS guid, reddit post id, tweet id)
title               VARCHAR(500)
body                TEXT NULL
url                 TEXT
author              VARCHAR(255) NULL
published_at        TIMESTAMP
metadata            JSONB                      -- upvotes, comments count, subreddit, hashtags
is_used             BOOLEAN DEFAULT false      -- marked true when used to generate a post
created_at          TIMESTAMP

UNIQUE(feed_id, external_id)
INDEX(feed_id, published_at DESC)
INDEX(is_used)
```

### 1.3 Google Alerts

Already designed in the Brand Radar system. This module reads from the existing `google_alert_items` table (or equivalent). If Brand Radar module is not yet built, use a lightweight version:

#### Migration: `google_alert_streams`

```
id                  BIGINT PRIMARY KEY
project_id          BIGINT FK → projects
name                VARCHAR(255)              -- "MonoVM brand mentions", "VPS hosting news"
rss_url             TEXT                       -- Google Alerts RSS feed URL
fetch_interval_min  INTEGER DEFAULT 30
is_active           BOOLEAN DEFAULT true
last_fetched_at     TIMESTAMP NULL
created_at          TIMESTAMP
updated_at          TIMESTAMP
```

#### Migration: `google_alert_items`

```
id                  BIGINT PRIMARY KEY
stream_id           BIGINT FK → google_alert_streams
external_id         VARCHAR(500)
title               VARCHAR(500)
snippet             TEXT
source_url          TEXT
source_name         VARCHAR(255) NULL
published_at        TIMESTAMP
is_used             BOOLEAN DEFAULT false
created_at          TIMESTAMP

UNIQUE(stream_id, external_id)
INDEX(stream_id, published_at DESC)
```

### 1.4 Feed Sync Jobs

```php
// Jobs
SyncSocialFeedJob        — fetches one feed, upserts items, deduplicates
SyncGoogleAlertJob       — fetches one alert stream, upserts items
SyncAllFeedsJob          — dispatches individual sync jobs for all active feeds

// Scheduler (Kernel.php)
$schedule->job(new SyncAllFeedsJob)->everyThirtyMinutes();
```

---

## 2. Channel & Style System

This is the core configuration layer. Each social media platform connected via Postiz is a "channel." Each channel has style rules that control how AI generates content for it.

### 2.1 Channels

#### Migration: `social_channels`

```
id                      BIGINT PRIMARY KEY
project_id              BIGINT FK → projects
name                    VARCHAR(255)              -- "MonoVM Twitter", "MonoVM LinkedIn"
platform                ENUM('twitter', 'linkedin', 'facebook', 'instagram', 'reddit', 'threads', 'tiktok', 'youtube', 'mastodon', 'bluesky', 'pinterest', 'telegram', 'other')
postiz_integration_id   VARCHAR(255)              -- Postiz integration/channel ID
postiz_settings_schema  JSONB NULL                -- cached platform-specific settings from Postiz
style_profile_id        BIGINT FK → social_style_profiles NULL  -- channel-specific style (overrides global)
is_active               BOOLEAN DEFAULT true
auto_post_enabled       BOOLEAN DEFAULT false     -- if true, approved posts auto-publish
auto_post_requires_approval BOOLEAN DEFAULT true  -- even in auto mode, require approval first
posting_window_start    TIME NULL                 -- e.g., 09:00 (local time)
posting_window_end      TIME NULL                 -- e.g., 18:00
posting_timezone        VARCHAR(50) DEFAULT 'UTC'
max_posts_per_day       INTEGER DEFAULT 3
posting_days            JSONB DEFAULT '["mon","tue","wed","thu","fri"]'
created_at              TIMESTAMP
updated_at              TIMESTAMP

INDEX(project_id, platform)
INDEX(postiz_integration_id)
```

### 2.2 Style Profiles

Style profiles define HOW AI writes posts. There are two scopes:
- **Global style** — project-level default, applied to any channel that doesn't have its own
- **Channel style** — overrides global for a specific channel

#### Migration: `social_style_profiles`

```
id                  BIGINT PRIMARY KEY
project_id          BIGINT FK → projects
name                VARCHAR(255)                -- "MonoVM Professional", "MonoVM Casual Twitter"
scope               ENUM('global', 'channel')
is_default          BOOLEAN DEFAULT false       -- one global default per project

-- Voice & Tone
tone                VARCHAR(100)                -- "professional", "casual", "witty", "technical", "friendly"
voice_description   TEXT                        -- free-text: "We speak as a knowledgeable hosting provider..."
personality_traits  JSONB                       -- ["authoritative", "helpful", "slightly humorous"]

-- Content Rules
max_length          INTEGER NULL                -- char limit (NULL = use platform default)
min_length          INTEGER NULL
use_emojis          BOOLEAN DEFAULT false
emoji_style         VARCHAR(50) NULL            -- "minimal", "moderate", "heavy"
use_hashtags        BOOLEAN DEFAULT true
max_hashtags        INTEGER DEFAULT 5
hashtag_strategy    ENUM('trending', 'branded', 'mixed', 'none') DEFAULT 'mixed'
default_hashtags    JSONB DEFAULT '[]'          -- always-include hashtags: ["#VPS", "#MonoVM"]
use_mentions        BOOLEAN DEFAULT false
cta_style           ENUM('none', 'subtle', 'direct', 'aggressive') DEFAULT 'subtle'
default_cta         TEXT NULL                   -- "Learn more at monovm.com"
link_placement      ENUM('end', 'inline', 'first_comment', 'none') DEFAULT 'end'

-- Language & Format
language            VARCHAR(10) DEFAULT 'en'
writing_guidelines  TEXT NULL                   -- detailed instructions for AI
forbidden_words     JSONB DEFAULT '[]'          -- words/phrases to never use
preferred_phrases   JSONB DEFAULT '[]'          -- preferred terminology
post_templates      JSONB DEFAULT '[]'          -- reusable template structures (see below)

-- Platform-Specific Overrides
platform_overrides  JSONB DEFAULT '{}'          -- per-platform tweaks within this profile

created_at          TIMESTAMP
updated_at          TIMESTAMP

INDEX(project_id, scope)
UNIQUE(project_id, is_default) WHERE is_default = true AND scope = 'global'
```

#### Post Templates (stored in `post_templates` JSONB)

```json
[
  {
    "name": "blog_promotion",
    "structure": "hook → value_prop → link → hashtags",
    "example": "Did you know {{hook}}?\n\nOur latest guide covers {{topic}} in depth.\n\n{{url}}\n\n{{hashtags}}"
  },
  {
    "name": "tip_post",
    "structure": "emoji → tip → explanation → cta",
    "example": "💡 {{tip_title}}\n\n{{tip_body}}\n\n{{cta}}"
  },
  {
    "name": "thread_opener",
    "structure": "bold_claim → thread_promise",
    "example": "{{bold_statement}}\n\nHere's what most people get wrong 🧵👇"
  },
  {
    "name": "industry_news",
    "structure": "news_hook → our_take → link",
    "example": "{{news_summary}}\n\nOur take: {{opinion}}\n\n{{source_url}}"
  }
]
```

### 2.3 Style Resolution Logic

```
When generating a post for Channel X:
  1. Load Channel X's style_profile_id
  2. If NULL → load project's default global style (is_default = true, scope = 'global')
  3. If global also NULL → use hardcoded system defaults
  4. Merge: channel style overrides global style field-by-field
  5. Apply platform_overrides[channel.platform] on top
  6. Pass merged style config to AI prompt
```

---

## 3. AI Post Generation Engine

### 3.1 Generation Triggers

Posts can be generated from:

| Trigger | Source Data | Typical Output |
|---|---|---|
| New blog post published | Blog post title, URL, excerpt, keywords | 1 post per active channel (or selected channels) |
| Blog post updated | Updated content diff, URL | 1 post per selected channel |
| Feed item selected | Feed item title, snippet, URL | 1 post per selected channel |
| Google Alert item selected | Alert title, snippet, source URL | 1 post per selected channel |
| Manual prompt | User-written topic/brief | 1+ posts per selected channel |
| Scheduled batch | AI picks from unused feed items + recent blog posts | N posts per channel per schedule |

### 3.2 Generation Request

#### Migration: `social_post_generations`

```
id                  BIGINT PRIMARY KEY
project_id          BIGINT FK → projects
triggered_by        ENUM('blog_publish', 'blog_update', 'feed_item', 'alert_item', 'manual', 'scheduled_batch')
source_type         VARCHAR(50) NULL            -- 'page', 'social_feed_item', 'google_alert_item'
source_id           BIGINT NULL                 -- FK to source record
manual_brief        TEXT NULL                   -- user-provided topic for manual triggers
target_channels     JSONB                       -- array of channel IDs to generate for
post_count          INTEGER DEFAULT 1           -- how many variants per channel
status              ENUM('pending', 'processing', 'completed', 'failed') DEFAULT 'pending'
error_message       TEXT NULL
created_by          BIGINT FK → users NULL
created_at          TIMESTAMP
updated_at          TIMESTAMP
```

### 3.3 AI Prompt Architecture

The generation job builds a prompt per channel using:

```
SYSTEM PROMPT:
  - Role: "You are a social media content writer for {{project.name}}"
  - Style profile (merged): tone, voice, personality, guidelines
  - Platform rules: char limits, hashtag rules, link rules
  - Forbidden words, preferred phrases
  - Post template to follow (if applicable)
  - Output format: JSON with fields {content, hashtags[], cta, suggested_media_query}

USER PROMPT:
  - Source content (blog excerpt, feed item, alert snippet, or manual brief)
  - URL to include (if applicable)
  - Any user notes
  - "Generate {{post_count}} post variant(s) for {{platform}}"
```

**Response parsing:**

```json
{
  "variants": [
    {
      "content": "The post text here...",
      "hashtags": ["#VPS", "#CloudHosting"],
      "cta": "Read the full guide →",
      "suggested_media_query": "server rack data center",
      "thread_parts": null
    }
  ]
}
```

### 3.4 Generation Job

```php
// Job: GenerateSocialPostsJob
// Queue: social-generation
// Input: social_post_generation_id

class GenerateSocialPostsJob implements ShouldQueue
{
    // 1. Load generation request + source content
    // 2. For each target channel:
    //    a. Resolve style profile (channel → global → defaults)
    //    b. Build prompt
    //    c. Call Claude API (claude-sonnet-4-20250514)
    //    d. Parse response
    //    e. Create social_posts records (status = 'draft')
    // 3. Mark generation as 'completed'
    // 4. Notify user via frontend (broadcast event)
}
```

---

## 4. Post Management

### 4.1 Posts Table

#### Migration: `social_posts`

```
id                      BIGINT PRIMARY KEY
project_id              BIGINT FK → projects
channel_id              BIGINT FK → social_channels
generation_id           BIGINT FK → social_post_generations NULL  -- NULL if manually created

-- Content
content                 TEXT                       -- the post text
hashtags                JSONB DEFAULT '[]'
cta                     TEXT NULL
thread_parts            JSONB NULL                 -- for threaded posts: [{content, media_ids}]
link_url                TEXT NULL                   -- URL to include in post
short_link_url          TEXT NULL                   -- shortened version (via Postiz or custom)

-- Source Reference
source_type             VARCHAR(50) NULL
source_id               BIGINT NULL
source_title            VARCHAR(500) NULL           -- denormalized for quick display
source_url              TEXT NULL

-- Media
media_ids               JSONB DEFAULT '[]'          -- array of social_media.id references

-- Status & Workflow
status                  ENUM('draft', 'pending_review', 'approved', 'scheduled', 'publishing', 'published', 'failed', 'rejected', 'archived') DEFAULT 'draft'
rejection_reason        TEXT NULL
reviewed_by             BIGINT FK → users NULL
reviewed_at             TIMESTAMP NULL

-- Scheduling
scheduled_at            TIMESTAMP NULL              -- when to publish
published_at            TIMESTAMP NULL              -- when actually published
postiz_post_id          VARCHAR(255) NULL           -- ID returned by Postiz after creation
postiz_release_url      TEXT NULL                   -- live URL of published post

-- Platform Settings
platform_settings       JSONB DEFAULT '{}'          -- Postiz platform-specific settings (__type, etc.)

-- Metadata
notes                   TEXT NULL                   -- internal notes
tags                    JSONB DEFAULT '[]'          -- internal tags for organization
created_by              BIGINT FK → users NULL
created_at              TIMESTAMP
updated_at              TIMESTAMP

INDEX(project_id, status)
INDEX(channel_id, status)
INDEX(scheduled_at) WHERE status = 'scheduled'
INDEX(status, scheduled_at)
```

### 4.2 Media Management

#### Migration: `social_media`

```
id                  BIGINT PRIMARY KEY
project_id          BIGINT FK → projects
original_filename   VARCHAR(255)
mime_type           VARCHAR(100)
file_size           INTEGER                     -- bytes
disk                VARCHAR(50) DEFAULT 'local' -- 'local', 's3'
path                TEXT                        -- storage path
thumbnail_path      TEXT NULL
width               INTEGER NULL
height              INTEGER NULL
duration_seconds    INTEGER NULL                -- for video/audio
alt_text            TEXT NULL
postiz_media_id     VARCHAR(255) NULL           -- after uploading to Postiz
postiz_media_url    TEXT NULL                   -- Postiz-hosted URL
uploaded_by         BIGINT FK → users NULL
created_at          TIMESTAMP
updated_at          TIMESTAMP

INDEX(project_id, created_at DESC)
```

**Media upload flow:**

```
1. User uploads file via Next.js UI → stored locally (or S3)
2. social_media record created
3. User attaches media to post (adds ID to social_posts.media_ids)
4. At publish time, media is uploaded to Postiz first:
   POST /public/v1/upload-from-url  (if URL accessible)
   or direct upload
5. Postiz returns media ID/URL → stored in social_media.postiz_media_id
6. Post creation payload references Postiz media paths
```

### 4.3 Post Workflow States

```
                    ┌──────────┐
                    │  draft   │
                    └────┬─────┘
                         │ submit_for_review
                         ▼
                  ┌──────────────┐
                  │pending_review│
                  └──────┬───────┘
                    ┌────┴────┐
                    │         │
               approve    reject
                    │         │
                    ▼         ▼
              ┌──────────┐ ┌──────────┐
              │ approved │ │ rejected │
              └────┬─────┘ └──────────┘
                   │
            schedule (set scheduled_at)
                   │
                   ▼
             ┌───────────┐
             │ scheduled  │
             └─────┬──────┘
                   │ publish_time reached
                   ▼
            ┌────────────┐
            │ publishing  │
            └──────┬──────┘
              ┌────┴────┐
              │         │
           success    fail
              │         │
              ▼         ▼
        ┌───────────┐ ┌────────┐
        │ published  │ │ failed │
        └───────────┘ └────────┘
```

**Shortcut:** If `auto_post_enabled` on channel AND `auto_post_requires_approval = false`:
`draft → approved → scheduled` happens automatically.

If `auto_post_enabled` AND `auto_post_requires_approval = true`:
`draft → pending_review` (still needs human), then `approved → scheduled` is automatic.

---

## 5. Scheduling & Auto-Posting Engine

### 5.1 Scheduler Configuration (per Channel)

Already defined in `social_channels`:
- `posting_window_start` / `posting_window_end` — allowed time range
- `posting_timezone` — timezone for the window
- `max_posts_per_day` — daily cap
- `posting_days` — which days of the week

### 5.2 Slot Finder

When a post is approved and needs scheduling, the system finds the next available slot:

```php
class SlotFinder
{
    /**
     * Find next available posting slot for a channel.
     *
     * Algorithm:
     * 1. Get channel's posting_days, window_start, window_end, max_posts_per_day
     * 2. Starting from now (or specified start date):
     *    a. Check if today is a posting day
     *    b. Count already-scheduled posts for today
     *    c. If under max_posts_per_day, find a time within the window
     *       that's at least 60min from any other scheduled post
     *    d. If no slot today, move to next posting day
     * 3. Also check Postiz slot availability:
     *    GET /public/v1/find-slot/:integration_id
     * 4. Return DateTime of next available slot
     */
    public function findNextSlot(SocialChannel $channel, ?Carbon $after = null): Carbon
}
```

### 5.3 Auto-Posting Job

```php
// Job: ProcessScheduledPostsJob
// Runs: every minute via scheduler
// Queue: social-publishing

class ProcessScheduledPostsJob implements ShouldQueue
{
    // 1. Query social_posts WHERE status = 'scheduled' AND scheduled_at <= now()
    // 2. For each post:
    //    a. Set status = 'publishing'
    //    b. Upload any un-uploaded media to Postiz
    //    c. Build Postiz API payload
    //    d. POST to Postiz /public/v1/posts
    //    e. On success: status = 'published', store postiz_post_id, release_url
    //    f. On failure: status = 'failed', store error, retry up to 3x
}
```

### 5.4 Batch Generation Job

For auto-posting channels that want a continuous stream:

```php
// Job: GenerateBatchPostsJob
// Runs: daily at configured time (per project setting)
// Queue: social-generation

class GenerateBatchPostsJob implements ShouldQueue
{
    // 1. For each project with auto-post channels:
    //    a. Check how many posts are needed for next 3 days
    //    b. Pick unused content sources:
    //       - Recent blog posts not yet promoted on this channel
    //       - Top feed items (by engagement score) not yet used
    //       - Recent Google Alert items not yet used
    //    c. Dispatch GenerateSocialPostsJob for each
    //    d. Generated posts enter workflow based on channel settings
}
```

---

## 6. Postiz Integration Layer

### 6.1 Configuration

```
# .env
POSTIZ_API_URL=https://api.postiz.com/public/v1    # or self-hosted URL
POSTIZ_API_KEY=your_api_key_here
```

### 6.2 Postiz API Service

```php
class PostizService
{
    private string $baseUrl;
    private string $apiKey;
    private int $rateLimit = 30; // per hour

    // --- Integrations ---
    public function listIntegrations(): array
    // GET /integrations
    // Returns connected social accounts

    public function getIntegrationSettings(string $integrationId): array
    // GET /integrations/settings/:id
    // Returns platform-specific settings schema

    // --- Media ---
    public function uploadFromUrl(string $url): array
    // POST /upload-from-url { "url": "..." }
    // Returns { id, name, path }

    public function uploadFile(string $filePath): array
    // Multipart upload if needed

    // --- Posts ---
    public function createPost(array $payload): array
    // POST /posts
    // Main publishing endpoint
    // Payload structure:
    // {
    //   "type": "schedule",           // or "now" for immediate
    //   "date": "ISO8601",
    //   "shortLink": false,
    //   "tags": [],
    //   "posts": [
    //     {
    //       "integration": { "id": "xxx" },
    //       "value": [
    //         { "content": "post text", "image": ["postiz_media_path"] }
    //       ],
    //       "settings": { "__type": "x", ... }
    //     }
    //   ]
    // }

    public function listPosts(string $startDate, string $endDate, ?string $customerId = null): array
    // GET /posts?startDate=...&endDate=...

    public function deletePost(string $postId): void
    // DELETE /posts/:id

    // --- Slots ---
    public function findSlot(string $integrationId): array
    // GET /find-slot/:id

    // --- Rate Limiting ---
    // Use Redis to track API calls per hour
    // If approaching 30/hr limit, queue and retry
}
```

### 6.3 Payload Builder

```php
class PostizPayloadBuilder
{
    /**
     * Build Postiz API payload from a social_post record.
     *
     * Handles:
     * - Platform-specific settings (__type field)
     * - Media attachment (Postiz media paths)
     * - Thread/comment structure for multi-part posts
     * - Short link toggle
     * - Schedule vs immediate posting
     */
    public function build(SocialPost $post): array
    {
        $channel = $post->channel;

        return [
            'type' => $post->scheduled_at ? 'schedule' : 'now',
            'date' => $post->scheduled_at?->toISOString(),
            'shortLink' => (bool) $post->short_link_url,
            'tags' => $post->tags ?? [],
            'posts' => [
                [
                    'integration' => ['id' => $channel->postiz_integration_id],
                    'value' => $this->buildValueBlocks($post),
                    'settings' => array_merge(
                        ['__type' => $this->mapPlatformToType($channel->platform)],
                        $post->platform_settings ?? []
                    ),
                ],
            ],
        ];
    }

    private function buildValueBlocks(SocialPost $post): array
    {
        // Main post
        $blocks = [
            [
                'content' => $this->assembleContent($post),
                'image' => $this->getPostizMediaPaths($post),
            ],
        ];

        // Thread parts (comments)
        if ($post->thread_parts) {
            foreach ($post->thread_parts as $part) {
                $blocks[] = [
                    'content' => $part['content'],
                    'image' => $part['media_paths'] ?? [],
                ];
            }
        }

        return $blocks;
    }

    private function assembleContent(SocialPost $post): string
    {
        $content = $post->content;

        // Append hashtags based on style
        if (!empty($post->hashtags)) {
            $content .= "\n\n" . implode(' ', array_map(fn($h) => "#$h", $post->hashtags));
        }

        // Append CTA
        if ($post->cta) {
            $content .= "\n\n" . $post->cta;
        }

        // Append link (if link_placement is 'end')
        if ($post->link_url && $this->getStyleLinkPlacement($post) === 'end') {
            $content .= "\n\n" . ($post->short_link_url ?? $post->link_url);
        }

        return $content;
    }

    private function mapPlatformToType(string $platform): string
    {
        return match($platform) {
            'twitter' => 'x',
            'linkedin' => 'linkedin-page',  // or 'linkedin' for personal
            'facebook' => 'facebook-page',
            'instagram' => 'instagram',
            'reddit' => 'reddit',
            'threads' => 'threads',
            'tiktok' => 'tiktok',
            'youtube' => 'youtube',
            'pinterest' => 'pinterest',
            'telegram' => 'telegram',
            'bluesky' => 'bluesky',
            'mastodon' => 'mastodon',
            default => $platform,
        };
    }
}
```

---

## 7. API Routes (Laravel)

All routes prefixed with `/api/v1/social/` and scoped to project via middleware.

### Channels

```
GET    /channels                         — list project channels
POST   /channels                         — create channel (+ sync from Postiz)
GET    /channels/:id                     — get channel details
PUT    /channels/:id                     — update channel settings
DELETE /channels/:id                     — soft delete channel
POST   /channels/sync-from-postiz        — import all Postiz integrations as channels
GET    /channels/:id/stats               — posting stats for channel
```

### Style Profiles

```
GET    /styles                           — list project style profiles
POST   /styles                           — create style profile
GET    /styles/:id                       — get style profile
PUT    /styles/:id                       — update style profile
DELETE /styles/:id                       — delete style profile
POST   /styles/:id/duplicate             — clone a style profile
PUT    /styles/:id/set-default           — set as project's global default
POST   /styles/preview                   — generate a sample post using this style (AI call)
```

### Feeds

```
GET    /feeds                            — list feeds + alert streams
POST   /feeds                            — create feed source
PUT    /feeds/:id                        — update feed
DELETE /feeds/:id                        — delete feed
GET    /feeds/:id/items                  — list feed items (paginated, filterable)
POST   /feeds/:id/sync                   — manual sync trigger
POST   /feeds/items/:id/mark-used        — mark item as used
```

### Post Generation

```
POST   /generate                         — trigger AI generation
       Body: {
         source_type, source_id,         // or manual_brief
         target_channel_ids: [],
         post_count: 1,
         template_name: null             // optional template to use
       }
GET    /generations                      — list generation requests
GET    /generations/:id                  — get generation + its posts
POST   /generate/batch                   — trigger batch generation for auto-post channels
```

### Posts

```
GET    /posts                            — list posts (filterable by status, channel, date range, tags)
POST   /posts                            — create post manually
GET    /posts/:id                        — get post detail
PUT    /posts/:id                        — edit post content
DELETE /posts/:id                        — delete post (if draft/rejected)

-- Workflow Actions
POST   /posts/:id/submit                 — submit for review (draft → pending_review)
POST   /posts/:id/approve                — approve post (pending_review → approved)
POST   /posts/:id/reject                 — reject post (pending_review → rejected) + reason
POST   /posts/:id/schedule               — schedule post (approved → scheduled) + scheduled_at
POST   /posts/:id/unschedule             — unschedule (scheduled → approved)
POST   /posts/:id/publish-now            — publish immediately (approved → publishing)
POST   /posts/:id/retry                  — retry failed post
POST   /posts/:id/archive                — archive post

-- Bulk Actions
POST   /posts/bulk/approve               — approve multiple posts
POST   /posts/bulk/schedule              — schedule multiple posts (auto-slot)
POST   /posts/bulk/delete                — delete multiple drafts

-- Media
POST   /posts/:id/media                  — attach media (upload file)
DELETE /posts/:id/media/:mediaId         — detach media
POST   /posts/:id/media/reorder          — reorder attached media
```

### Media Library

```
GET    /media                            — list all project media (paginated)
POST   /media/upload                     — upload media file
DELETE /media/:id                        — delete media
GET    /media/:id                        — get media details
```

### Analytics & Logs

```
GET    /analytics/overview               — posts published/scheduled/failed counts
GET    /analytics/by-channel             — per-channel stats
GET    /analytics/by-date                — daily/weekly/monthly breakdown
GET    /analytics/content-sources        — which sources generate most posts
GET    /activity-log                     — audit trail of all social actions
```

---

## 8. Frontend Pages (Next.js)

### Page Structure

```
app/
├── (dashboard)/
│   ├── [projectId]/
│   │   ├── social/
│   │   │   ├── page.tsx                  -- Social Dashboard (overview stats)
│   │   │   ├── channels/
│   │   │   │   ├── page.tsx              -- Channel list + Postiz sync
│   │   │   │   └── [channelId]/
│   │   │   │       ├── page.tsx          -- Channel detail + settings
│   │   │   │       └── schedule/
│   │   │   │           └── page.tsx      -- Channel calendar view
│   │   │   ├── styles/
│   │   │   │   ├── page.tsx              -- Style profiles list
│   │   │   │   └── [styleId]/
│   │   │   │       └── page.tsx          -- Style editor
│   │   │   ├── feeds/
│   │   │   │   ├── page.tsx              -- Feed sources list
│   │   │   │   └── [feedId]/
│   │   │   │       └── page.tsx          -- Feed items browser
│   │   │   ├── generate/
│   │   │   │   └── page.tsx              -- Post generation wizard
│   │   │   ├── posts/
│   │   │   │   ├── page.tsx              -- Post list (filterable kanban or table)
│   │   │   │   └── [postId]/
│   │   │   │       └── page.tsx          -- Post editor + preview
│   │   │   ├── calendar/
│   │   │   │   └── page.tsx              -- Calendar view (all channels)
│   │   │   ├── media/
│   │   │   │   └── page.tsx              -- Media library
│   │   │   └── analytics/
│   │   │       └── page.tsx              -- Social analytics dashboard
```

### Key UI Components

```
components/social/
├── ChannelCard.tsx                 -- Channel with platform icon, status, post count
├── ChannelSyncButton.tsx           -- "Import from Postiz" button
├── StyleProfileEditor.tsx          -- Full style profile form
├── StylePreview.tsx                -- Live preview of AI-generated sample post
├── FeedItemBrowser.tsx             -- Scrollable feed items with "Generate Post" action
├── PostCard.tsx                    -- Post preview card with status badge
├── PostEditor.tsx                  -- Rich text editor for post content
├── PostPreview.tsx                 -- Platform-specific preview (Twitter card, LinkedIn card, etc.)
├── PostKanban.tsx                  -- Drag-and-drop kanban: Draft → Review → Approved → Scheduled → Published
├── PostTable.tsx                   -- Table view alternative
├── MediaUploader.tsx               -- Drag-and-drop file upload
├── MediaLibraryPicker.tsx          -- Grid picker for existing media
├── MediaAttachments.tsx            -- Attached media strip with reorder
├── CalendarView.tsx                -- Monthly/weekly calendar with post dots
├── GenerationWizard.tsx            -- Step-by-step: Source → Channels → Style → Generate
├── BulkActionBar.tsx               -- Floating bar for multi-select actions
├── PlatformIcon.tsx                -- Renders platform logo/icon
├── PostStatusBadge.tsx             -- Color-coded status badge
└── AnalyticsCharts.tsx             -- Recharts-based posting analytics
```

---

## 9. Database Relationships (Eloquent)

```php
// Project
hasMany SocialChannel
hasMany SocialStyleProfile
hasMany SocialFeed
hasMany GoogleAlertStream
hasMany SocialPost
hasMany SocialMedia
hasMany SocialPostGeneration

// SocialChannel
belongsTo Project
belongsTo SocialStyleProfile (nullable)
hasMany SocialPost

// SocialStyleProfile
belongsTo Project
hasMany SocialChannel (channels using this style)

// SocialFeed
belongsTo Project
hasMany SocialFeedItem

// SocialFeedItem
belongsTo SocialFeed

// GoogleAlertStream
belongsTo Project
hasMany GoogleAlertItem

// SocialPostGeneration
belongsTo Project
belongsTo User (created_by)
hasMany SocialPost

// SocialPost
belongsTo Project
belongsTo SocialChannel
belongsTo SocialPostGeneration (nullable)
belongsTo User (created_by)
belongsTo User (reviewed_by)
belongsToMany SocialMedia (via media_ids JSONB — or use pivot table)

// SocialMedia
belongsTo Project
belongsTo User (uploaded_by)
```

---

## 10. Queue Architecture

```
Queue Name              | Jobs                                    | Workers
------------------------|----------------------------------------|--------
social-feeds            | SyncSocialFeedJob, SyncGoogleAlertJob  | 2
social-generation       | GenerateSocialPostsJob,                | 2
                        | GenerateBatchPostsJob                  |
social-publishing       | ProcessScheduledPostsJob,              | 1
                        | UploadMediaToPostizJob                 |
social-default          | Everything else                        | 1
```

### Scheduler (Kernel.php additions)

```php
// Feed syncing
$schedule->job(new SyncAllFeedsJob)->everyThirtyMinutes();

// Post publishing
$schedule->job(new ProcessScheduledPostsJob)->everyMinute();

// Batch generation (for auto-post channels)
$schedule->job(new GenerateBatchPostsJob)->dailyAt('06:00');

// Analytics sync (pull stats from Postiz)
$schedule->job(new SyncPostizAnalyticsJob)->everyFourHours();
```

---

## 11. Events & Notifications

```php
// Events
SocialPostCreated          — triggers notification to reviewers
SocialPostSubmitted        — triggers notification to approvers  
SocialPostApproved         — triggers auto-schedule if channel allows
SocialPostPublished        — triggers analytics tracking start
SocialPostFailed           — triggers alert to admin
FeedItemsDiscovered        — triggers batch generation if auto-post enabled
BlogPostPublished          — triggers social post generation for linked channels

// Listeners
AutoScheduleOnApproval     — listens to SocialPostApproved
NotifyReviewers            — listens to SocialPostSubmitted
AlertOnFailure             — listens to SocialPostFailed
TriggerSocialOnBlogPublish — listens to BlogPostPublished
```

---

## 12. Implementation Phases

### Phase 1: Foundation (Week 1-2)

**Goal:** Database, models, channels, styles — no AI yet.

- [ ] Run all migrations
- [ ] Create Eloquent models with relationships
- [ ] Implement `PostizService` with integration listing + settings fetch
- [ ] Build `SocialChannel` CRUD API + "Sync from Postiz" endpoint
- [ ] Build `SocialStyleProfile` CRUD API
- [ ] Frontend: Channel management page, Style profile editor
- [ ] Tests: PostizService mock, Channel CRUD, Style CRUD

### Phase 2: Content Sources (Week 2-3)

**Goal:** Feeds and alerts flowing in.

- [ ] Implement `SocialFeed` and `SocialFeedItem` CRUD
- [ ] Build RSS fetcher (use `SimplePie` or `laminas/laminas-feed`)
- [ ] Build Reddit feed parser (public JSON API, no auth needed)
- [ ] Build Google Alerts RSS parser
- [ ] Implement `SyncSocialFeedJob`, `SyncGoogleAlertJob`, `SyncAllFeedsJob`
- [ ] Register scheduler entries
- [ ] Frontend: Feed management page, Feed items browser
- [ ] Tests: Feed sync jobs, deduplication

### Phase 3: AI Generation (Week 3-4)

**Goal:** AI can produce posts from any source.

- [ ] Build prompt assembler (merges style + source + platform rules)
- [ ] Implement `GenerateSocialPostsJob`
- [ ] Build `SocialPostGeneration` tracking
- [ ] Create `SocialPost` records from AI output
- [ ] Frontend: Generation wizard (select source → channels → generate)
- [ ] Frontend: Post editor with content editing
- [ ] Style preview endpoint (generates sample post from style)
- [ ] Tests: Prompt assembly, post creation from AI response

### Phase 4: Post Management & Workflow (Week 4-5)

**Goal:** Full post lifecycle with approval.

- [ ] Post CRUD API with all workflow action endpoints
- [ ] Implement status transitions with validation
- [ ] Build `PostKanban` view (drag to change status)
- [ ] Build `PostTable` view with filters
- [ ] Implement bulk actions (approve, schedule, delete)
- [ ] Add rejection with reason
- [ ] Frontend: Post detail page with preview + editor
- [ ] Platform-specific previews (Twitter card, LinkedIn card)
- [ ] Tests: Workflow state machine, permission checks

### Phase 5: Media Management (Week 5-6)

**Goal:** Upload, attach, and sync media.

- [ ] `SocialMedia` CRUD + file upload endpoint
- [ ] Thumbnail generation for images
- [ ] Media library UI with grid view
- [ ] Attach/detach media on post editor
- [ ] Implement `UploadMediaToPostizJob` (pre-publish upload)
- [ ] Media reordering on posts
- [ ] Tests: Upload, Postiz media sync

### Phase 6: Scheduling & Publishing (Week 6-7)

**Goal:** Posts actually go live via Postiz.

- [ ] Implement `SlotFinder` algorithm
- [ ] Implement `PostizPayloadBuilder`
- [ ] Implement `ProcessScheduledPostsJob` (the publisher)
- [ ] Handle Postiz rate limiting (30/hr) with Redis tracking
- [ ] Retry logic for failed posts (max 3 retries with backoff)
- [ ] Calendar view showing scheduled posts across channels
- [ ] Frontend: Schedule dialog with slot suggestions
- [ ] Store `postiz_post_id` and `release_url` on success
- [ ] Tests: Payload building, scheduling, rate limiting

### Phase 7: Auto-Posting & Batch Generation (Week 7-8)

**Goal:** Hands-off mode for configured channels.

- [ ] Implement `GenerateBatchPostsJob` (picks sources, generates posts)
- [ ] Auto-schedule logic on approval (via event listener)
- [ ] Content source scoring (prioritize unused blog posts, high-engagement feeds)
- [ ] "Source usage" tracking (mark items as used)
- [ ] Blog publish → auto-generate social posts listener
- [ ] Frontend: Auto-post settings per channel
- [ ] Tests: Batch generation, auto-schedule, source selection

### Phase 8: Analytics & Logging (Week 8-9)

**Goal:** Visibility into what's working.

- [ ] Activity log for all social actions (who, what, when)
- [ ] Analytics overview: posts by status, by channel, by date
- [ ] Pull published post stats from Postiz analytics endpoint
- [ ] Content source effectiveness (which feeds/blogs produce most engagement)
- [ ] Frontend: Analytics dashboard with charts
- [ ] Frontend: Activity log page
- [ ] Tests: Analytics aggregation queries

---

## 13. Environment Variables (additions to .env)

```bash
# Postiz
POSTIZ_API_URL=https://api.postiz.com/public/v1
POSTIZ_API_KEY=

# Social Media Module
SOCIAL_DEFAULT_TIMEZONE=UTC
SOCIAL_BATCH_GENERATION_TIME=06:00
SOCIAL_MAX_RETRIES=3
SOCIAL_RETRY_BACKOFF_MINUTES=5
SOCIAL_MEDIA_DISK=local                    # or 's3'
SOCIAL_MEDIA_MAX_SIZE_MB=50
SOCIAL_RATE_LIMIT_BUFFER=5                 # stop at 25/30 calls per hour

# AI (already in SEO OS)
ANTHROPIC_API_KEY=
ANTHROPIC_MODEL=claude-sonnet-4-20250514
```

---

## 14. Critical Implementation Notes

1. **Postiz rate limit is 30 requests/hour.** Use Redis to track calls. Batch multiple posts into single Postiz API calls when possible (the API accepts multiple posts in one request). If rate limited, queue and retry next window.

2. **Content must be sanitized before Postiz.** Remove `\n`, `\r`, `\t` characters and replace with proper spacing. Postiz API returns JSON parse errors on unescaped content. Build a `PostContentSanitizer` utility.

3. **Media must be uploaded to Postiz BEFORE creating the post.** You cannot use external URLs directly in post media — files must go through Postiz's upload endpoint first. Cache the Postiz media ID/URL on the `social_media` record to avoid re-uploading.

4. **Platform character limits are enforced at generation time, not publish time.** Load limits from `postiz_settings_schema` (cached on channel) and pass to AI prompt. Twitter/X = 280, LinkedIn = 3000, etc.

5. **Never auto-publish without the approval flag.** Even if `auto_post_enabled = true`, the `auto_post_requires_approval` flag must be explicitly set to `false` by the user. Default is `true`.

6. **Style profiles should be versioned at generation time.** When a post is generated, snapshot the style config into `social_post_generations.metadata` so you can audit what style was used, even if the profile is later edited.

7. **Feed deduplication uses `external_id`.** RSS items use `<guid>`, Reddit uses post ID, Google Alerts use the URL hash. Always upsert on `(feed_id, external_id)`.

8. **Thread support:** For Twitter threads, the AI generates `thread_parts` as an array. The Postiz payload builder maps these to the `value` array (first element = main post, subsequent = comments/replies).

9. **Cross-post with variations:** When generating for multiple channels from the same source, each channel gets its own AI call with its own style profile. Don't duplicate the same text across platforms — the whole point is platform-native content.

10. **The `platform_settings` JSONB on `social_posts` stores Postiz-specific config** like Reddit flair, YouTube playlist, GMB topic type, etc. The UI should dynamically render settings fields based on the channel's `postiz_settings_schema`.

---

## 15. File Structure (additions to SEO OS backend)

```
app/
├── Http/Controllers/Social/
│   ├── SocialChannelController.php
│   ├── SocialStyleProfileController.php
│   ├── SocialFeedController.php
│   ├── SocialPostController.php
│   ├── SocialPostGenerationController.php
│   ├── SocialMediaController.php
│   └── SocialAnalyticsController.php
│
├── Models/Social/
│   ├── SocialChannel.php
│   ├── SocialStyleProfile.php
│   ├── SocialFeed.php
│   ├── SocialFeedItem.php
│   ├── GoogleAlertStream.php
│   ├── GoogleAlertItem.php
│   ├── SocialPost.php
│   ├── SocialPostGeneration.php
│   └── SocialMedia.php
│
├── Services/Social/
│   ├── PostizService.php
│   ├── PostizPayloadBuilder.php
│   ├── PostContentSanitizer.php
│   ├── SlotFinder.php
│   ├── StyleResolver.php
│   ├── PromptAssembler.php
│   ├── FeedFetcher.php
│   └── SourceScorer.php
│
├── Jobs/Social/
│   ├── SyncSocialFeedJob.php
│   ├── SyncGoogleAlertJob.php
│   ├── SyncAllFeedsJob.php
│   ├── GenerateSocialPostsJob.php
│   ├── GenerateBatchPostsJob.php
│   ├── ProcessScheduledPostsJob.php
│   ├── UploadMediaToPostizJob.php
│   └── SyncPostizAnalyticsJob.php
│
├── Events/Social/
│   ├── SocialPostCreated.php
│   ├── SocialPostSubmitted.php
│   ├── SocialPostApproved.php
│   ├── SocialPostPublished.php
│   ├── SocialPostFailed.php
│   └── FeedItemsDiscovered.php
│
├── Listeners/Social/
│   ├── AutoScheduleOnApproval.php
│   ├── NotifyReviewers.php
│   ├── AlertOnFailure.php
│   └── TriggerSocialOnBlogPublish.php
│
├── Enums/Social/
│   ├── Platform.php
│   ├── PostStatus.php
│   ├── StyleScope.php
│   ├── FeedSourceType.php
│   ├── GenerationTrigger.php
│   ├── CtaStyle.php
│   ├── HashtagStrategy.php
│   └── LinkPlacement.php
│
└── Policies/Social/
    ├── SocialPostPolicy.php
    └── SocialChannelPolicy.php

database/migrations/
├── xxxx_create_social_style_profiles_table.php
├── xxxx_create_social_channels_table.php
├── xxxx_create_social_feeds_table.php
├── xxxx_create_social_feed_items_table.php
├── xxxx_create_google_alert_streams_table.php
├── xxxx_create_google_alert_items_table.php
├── xxxx_create_social_post_generations_table.php
├── xxxx_create_social_posts_table.php
└── xxxx_create_social_media_table.php
```

---

## 16. First Commands for Claude Code

```bash
# From the SEO OS backend root:

# 1. Create migrations (in this order — FK dependencies)
php artisan make:migration create_social_style_profiles_table
php artisan make:migration create_social_channels_table
php artisan make:migration create_social_feeds_table
php artisan make:migration create_social_feed_items_table
php artisan make:migration create_google_alert_streams_table
php artisan make:migration create_google_alert_items_table
php artisan make:migration create_social_post_generations_table
php artisan make:migration create_social_posts_table
php artisan make:migration create_social_media_table

# 2. Create models
php artisan make:model Social/SocialChannel
php artisan make:model Social/SocialStyleProfile
php artisan make:model Social/SocialFeed
php artisan make:model Social/SocialFeedItem
php artisan make:model Social/GoogleAlertStream
php artisan make:model Social/GoogleAlertItem
php artisan make:model Social/SocialPost
php artisan make:model Social/SocialPostGeneration
php artisan make:model Social/SocialMedia

# 3. Create controllers
php artisan make:controller Social/SocialChannelController --api
php artisan make:controller Social/SocialStyleProfileController --api
php artisan make:controller Social/SocialFeedController --api
php artisan make:controller Social/SocialPostController --api
php artisan make:controller Social/SocialPostGenerationController --api
php artisan make:controller Social/SocialMediaController --api
php artisan make:controller Social/SocialAnalyticsController

# 4. Run migrations
php artisan migrate

# Start with Phase 1 tasks from Section 12.
```
