docs: update ARCHITECTURE.md for PullPush.io migration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
441
ARCHITECTURE.md
441
ARCHITECTURE.md
@@ -8,20 +8,21 @@ PRAutoBlogger is a WordPress plugin that monitors social media (starting with Re
|
|||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
│ WP-Cron / Action Scheduler │
|
│ WP-Cron / Action Scheduler │
|
||||||
│ (Daily trigger, configurable) │
|
│ (Daily trigger, configurable) │
|
||||||
└────────────┬────────────────────────────────────────────────────────────┘
|
└────────────┬────────────────────────────────────────────────────────────┘
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌─────────────────────────┐
|
┌─────────────────────────┐
|
||||||
│ 1. Source Collector │ Pulls raw posts/comments from social platforms
|
│ 1. Source Collector │ Pulls raw posts/comments from social platforms
|
||||||
│ (Reddit API, etc.) │ Stores raw data in `ab_source_data` table
|
│ (PullPush.io primary │ PullPush.io API (primary) / Reddit .json (fallback)
|
||||||
|
│ / Reddit .json fallback) Stores raw data in `ab_source_data` table
|
||||||
└────────────┬────────────┘
|
└────────────┬────────────┘
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌─────────────────────────┐
|
┌─────────────────────────┐
|
||||||
│ 2. Content Analyzer │ LLM call (cheap model) to detect:
|
│ 2. Content Analyzer │ LLM call (cheap model) to detect:
|
||||||
│ (Analysis Agent) │ - Recurring questions
|
│ (Analysis Agent) │ - Recurring questions
|
||||||
│ │ - Complaints / pain points
|
│ │ - Complaints / pain points
|
||||||
│ │ - Product comparisons
|
│ │ - Product comparisons
|
||||||
│ │ Stores analysis in `ab_analysis_results` table
|
│ │ Stores analysis in `ab_analysis_results` table
|
||||||
@@ -30,22 +31,22 @@ PRAutoBlogger is a WordPress plugin that monitors social media (starting with Re
|
|||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌─────────────────────────┐
|
┌─────────────────────────┐
|
||||||
│ 3. Idea Scorer & │ Deduplicates against existing posts
|
│ 3. Idea Scorer & │ Deduplicates against existing posts
|
||||||
│ Deduplicator │ Scores ideas by relevance, frequency, freshness
|
│ Deduplicator │ Scores ideas by relevance, frequency, freshness
|
||||||
│ │ Picks top N ideas (N = daily article target)
|
│ │ Picks top N ideas (N = daily article target)
|
||||||
└────────────┬────────────┘
|
└────────────┬────────────┘
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌─────────────────────────┐
|
┌─────────────────────────┐
|
||||||
│ 4. Writer Agent │ Configurable pipeline:
|
│ 4. Writer Agent │ Configurable pipeline:
|
||||||
│ (Content Generator) │ Single-pass: one LLM call → complete article
|
│ (Content Generator) │ Single-pass: one LLM call → complete article
|
||||||
│ │ Multi-step: outline → draft → polish
|
│ │ Multi-step: outline → draft → polish
|
||||||
│ │ Uses quality model via OpenRouter
|
│ │ Uses quality model via OpenRouter
|
||||||
└────────────┬────────────┘
|
└────────────┬────────────┘
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌─────────────────────────┐
|
┌─────────────────────────┐
|
||||||
│ 5. Chief Editor Agent │ LLM-powered editorial review:
|
│ 5. Chief Editor Agent │ LLM-powered editorial review:
|
||||||
│ │ - Checks quality, accuracy, tone
|
│ │ - Checks quality, accuracy, tone
|
||||||
│ │ - Checks SEO (title, headings, keyword density)
|
│ │ - Checks SEO (title, headings, keyword density)
|
||||||
│ │ - Can request rewrites or approve
|
│ │ - Can request rewrites or approve
|
||||||
@@ -54,7 +55,7 @@ PRAutoBlogger is a WordPress plugin that monitors social media (starting with Re
|
|||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌─────────────────────────┐
|
┌─────────────────────────┐
|
||||||
│ 6. Publisher │ Creates/updates WordPress post
|
│ 6. Publisher │ Creates/updates WordPress post
|
||||||
│ │ Sets categories, tags, featured image
|
│ │ Sets categories, tags, featured image
|
||||||
│ │ Stores generation metadata in post_meta
|
│ │ Stores generation metadata in post_meta
|
||||||
│ │ Logs cost/tokens in `prab_generation_log` table
|
│ │ Logs cost/tokens in `prab_generation_log` table
|
||||||
@@ -62,8 +63,8 @@ PRAutoBlogger is a WordPress plugin that monitors social media (starting with Re
|
|||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌─────────────────────────┐
|
┌─────────────────────────┐
|
||||||
│ 7. Metrics Collector │ Tracks post-publish performance:
|
│ 7. Metrics Collector │ Tracks post-publish performance:
|
||||||
│ (Separate cron job) │ - WordPress native (views, comments)
|
│ (Separate cron job) │ - WordPress native (views, comments)
|
||||||
│ │ - GA4 (pageviews, bounce rate, time on page)
|
│ │ - GA4 (pageviews, bounce rate, time on page)
|
||||||
│ │ - Composite "content score"
|
│ │ - Composite "content score"
|
||||||
│ │ Feeds back into Analysis step for self-improvement
|
│ │ Feeds back into Analysis step for self-improvement
|
||||||
@@ -76,89 +77,90 @@ PRAutoBlogger is a WordPress plugin that monitors social media (starting with Re
|
|||||||
|
|
||||||
```
|
```
|
||||||
prautoblogger/
|
prautoblogger/
|
||||||
├── prautoblogger.php # Plugin bootstrap — minimal, only hook registration
|
├── prautoblogger.php # Plugin bootstrap — minimal, only hook registration
|
||||||
├── uninstall.php # Clean removal of ALL plugin data
|
├── uninstall.php # Clean removal of ALL plugin data
|
||||||
├── readme.txt # WordPress.org standard readme
|
├── readme.txt # WordPress.org standard readme
|
||||||
├── composer.json # Autoloading (PSR-4) and dependencies
|
├── composer.json # Autoloading (PSR-4) and dependencies
|
||||||
├── ARCHITECTURE.md # This file
|
├── ARCHITECTURE.md # This file
|
||||||
├── CONVENTIONS.md # Naming patterns, extension guides
|
├── CONVENTIONS.md # Naming patterns, extension guides
|
||||||
├── CHANGELOG.md # Semantic versioning changelog
|
├── CHANGELOG.md # Semantic versioning changelog
|
||||||
│
|
│
|
||||||
├── assets/
|
├── assets/
|
||||||
│ ├── css/
|
│ ├── css/
|
||||||
│ │ ├── admin.css # Admin page styles (wp-admin conventions)
|
│ │ ├── admin.css # Admin page styles (wp-admin conventions)
|
||||||
│ │ └── posts-widget.css # Frontend posts widget styles (uses theme CSS vars)
|
│ │ └── posts-widget.css # Frontend posts widget styles (uses theme CSS vars)
|
||||||
│ └── js/
|
│ └── js/
|
||||||
│ ├── admin.js # Admin page interactivity (vanilla JS / Alpine.js)
|
│ ├── admin.js # Admin page interactivity (vanilla JS / Alpine.js)
|
||||||
│ └── posts-widget.js # React component for frontend post cards (wp.element)
|
│ └── posts-widget.js # React component for frontend post cards (wp.element)
|
||||||
│
|
│
|
||||||
├── includes/
|
├── includes/
|
||||||
│ ├── class-prautoblogger.php # Main orchestrator — registers all hooks via loader
|
│ ├── class-prautoblogger.php # Main orchestrator — registers all hooks via loader
|
||||||
│ ├── class-activator.php # Activation: create DB tables, set defaults, schedule cron
|
│ ├── class-activator.php # Activation: create DB tables, set defaults, schedule cron
|
||||||
│ ├── class-deactivator.php # Deactivation: clear cron, cleanup transients
|
│ ├── class-deactivator.php # Deactivation: clear cron, cleanup transients
|
||||||
│ ├── class-autoloader.php # PSR-4-style autoloader for plugin classes
|
│ ├── class-autoloader.php # PSR-4-style autoloader for plugin classes
|
||||||
│ │
|
│ │
|
||||||
│ ├── admin/
|
│ ├── admin/
|
||||||
│ │ ├── class-admin-page.php # Main settings page (tabbed SaaS-style UI)
|
│ │ ├── class-admin-page.php # Main settings page (tabbed SaaS-style UI)
|
||||||
│ │ ├── class-settings-fields.php # Declarative settings definitions (sections + fields)
|
│ │ ├── class-settings-fields.php # Declarative settings definitions (sections + fields)
|
||||||
│ │ ├── class-admin-notices.php # Onboarding notices, error alerts, budget warnings
|
│ │ ├── class-admin-notices.php # Onboarding notices, error alerts, budget warnings
|
||||||
│ │ ├── class-dashboard-widget.php # WP Dashboard widget showing generation status
|
│ │ ├── class-dashboard-widget.php # WP Dashboard widget showing generation status
|
||||||
│ │ ├── class-post-metabox.php # Metabox on posts showing generation metadata
|
│ │ ├── class-post-metabox.php # Metabox on posts showing generation metadata
|
||||||
│ │ ├── class-metrics-page.php # Admin page for cost dashboard and content scores
|
│ │ ├── class-metrics-page.php # Admin page for cost dashboard and content scores
|
||||||
│ │ ├── class-review-queue.php # Approve/edit/reject queue for generated drafts
|
│ │ ├── class-review-queue.php # Approve/edit/reject queue for generated drafts
|
||||||
│ │ └── class-log-viewer.php # Activity log viewer with level filtering
|
│ │ └── class-log-viewer.php # Activity log viewer with level filtering
|
||||||
│ │
|
│ │
|
||||||
│ ├── core/
|
│ ├── core/
|
||||||
│ │ ├── class-scheduler.php # WP-Cron / Action Scheduler job management
|
│ │ ├── class-scheduler.php # WP-Cron / Action Scheduler job management
|
||||||
│ │ ├── class-source-collector.php # Orchestrates data collection from all sources
|
│ │ ├── class-source-collector.php # Orchestrates data collection from all sources
|
||||||
│ │ ├── class-content-analyzer.php # LLM-powered analysis of collected social data
|
│ │ ├── class-content-analyzer.php # LLM-powered analysis of collected social data
|
||||||
│ │ ├── class-idea-scorer.php # Ranks and deduplicates article ideas
|
│ │ ├── class-idea-scorer.php # Ranks and deduplicates article ideas
|
||||||
│ │ ├── class-content-generator.php # Writer agent — manages the generation pipeline
|
│ │ ├── class-content-generator.php# Writer agent — manages the generation pipeline
|
||||||
│ │ ├── class-chief-editor.php # Editor agent — LLM-powered editorial review
|
│ │ ├── class-chief-editor.php # Editor agent — LLM-powered editorial review
|
||||||
│ │ ├── class-publisher.php # Creates WordPress posts from approved content
|
│ │ ├── class-publisher.php # Creates WordPress posts from approved content
|
||||||
│ │ ├── class-cost-tracker.php # Logs all API costs, enforces budget limits
|
│ │ ├── class-cost-tracker.php # Logs all API costs, enforces budget limits
|
||||||
│ │ ├── class-logger.php # Structured logging singleton (error/warning/info/debug)
|
│ │ ├── class-logger.php # Structured logging singleton (error/warning/info/debug)
|
||||||
│ │ ├── class-pipeline-runner.php # Orchestrates the full generation pipeline
|
│ │ ├── class-pipeline-runner.php # Orchestrates the full generation pipeline
|
||||||
│ │ ├── class-ga4-client.php # Google Analytics 4 API client (OAuth + Data API)
|
│ │ ├── class-ga4-client.php # Google Analytics 4 API client (OAuth + Data API)
|
||||||
│ │ └── class-metrics-collector.php # Collects post performance data (WP + GA4)
|
│ │ └── class-metrics-collector.php# Collects post performance data (WP + GA4)
|
||||||
│ │
|
│ │
|
||||||
│ ├── providers/
|
│ ├── providers/
|
||||||
│ │ ├── interface-llm-provider.php # Contract for any LLM provider
|
│ │ ├── interface-llm-provider.php # Contract for any LLM provider
|
||||||
│ │ ├── class-openrouter-provider.php # OpenRouter API implementation
|
│ │ ├── class-openrouter-provider.php # OpenRouter API implementation
|
||||||
│ │ ├── class-openrouter-pricing.php # Model pricing lookup and cost estimation
|
│ │ ├── class-openrouter-pricing.php # Model pricing lookup and cost estimation
|
||||||
│ │ ├── interface-source-provider.php # Contract for any social media source
|
│ │ ├── interface-source-provider.php # Contract for any social media source
|
||||||
│ │ ├── class-reddit-provider.php # Reddit API (OAuth) implementation
|
│ │ ├── class-pull-push-client.php # PullPush.io HTTP client (primary, no auth)
|
||||||
│ │ ├── class-reddit-api-client.php # Reddit HTTP client (OAuth + rate limiting)
|
│ │ ├── class-reddit-json-client.php # Reddit .json endpoint client (fallback, no auth)
|
||||||
|
│ │ ├── class-reddit-provider.php # Reddit data collection (PullPush primary + .json fallback)
|
||||||
│ │ ├── class-tiktok-provider.php # TikTok — stub/future implementation
|
│ │ ├── class-tiktok-provider.php # TikTok — stub/future implementation
|
||||||
│ │ ├── class-instagram-provider.php # Instagram — stub/future implementation
|
│ │ ├── class-instagram-provider.php # Instagram — stub/future implementation
|
||||||
│ │ └── class-youtube-provider.php # YouTube — stub/future implementation
|
│ │ └── class-youtube-provider.php # YouTube — stub/future implementation
|
||||||
│ │
|
│ │
|
||||||
│ ├── frontend/
|
│ ├── frontend/
|
||||||
│ │ └── class-posts-widget.php # [prautoblogger_posts] shortcode + REST endpoint
|
│ │ └── class-posts-widget.php # [prautoblogger_posts] shortcode + REST endpoint
|
||||||
│ │
|
│ │
|
||||||
│ └── models/
|
│ └── models/
|
||||||
│ ├── class-source-data.php # Value object: raw social media post/comment
|
│ ├── class-source-data.php # Value object: raw social media post/comment
|
||||||
│ ├── class-analysis-result.php # Value object: analyzed topic with scores
|
│ ├── class-analysis-result.php # Value object: analyzed topic with scores
|
||||||
│ ├── class-article-idea.php # Value object: scored article idea
|
│ ├── class-article-idea.php # Value object: scored article idea
|
||||||
│ ├── class-content-request.php # Value object: generation request with params
|
│ ├── class-content-request.php # Value object: generation request with params
|
||||||
│ ├── class-editorial-review.php # Value object: editor verdict, scores, revised content
|
│ ├── class-editorial-review.php # Value object: editor verdict, scores, revised content
|
||||||
│ ├── class-generation-log.php # Value object: log entry for a generation run
|
│ ├── class-generation-log.php # Value object: log entry for a generation run
|
||||||
│ └── class-content-score.php # Value object: composite performance score
|
│ └── class-content-score.php # Value object: composite performance score
|
||||||
│
|
│
|
||||||
├── templates/
|
├── templates/
|
||||||
│ └── admin/
|
│ └── admin/
|
||||||
│ ├── settings-page.php # Settings page template (tabbed sidebar layout)
|
│ ├── settings-page.php # Settings page template (tabbed sidebar layout)
|
||||||
│ ├── metrics-page.php # Metrics/cost dashboard template
|
│ ├── metrics-page.php # Metrics/cost dashboard template
|
||||||
│ ├── review-queue.php # Review queue table template
|
│ ├── review-queue.php # Review queue table template
|
||||||
│ ├── log-viewer.php # Activity log viewer template
|
│ ├── log-viewer.php # Activity log viewer template
|
||||||
│ └── metabox-generation-info.php # Post metabox template
|
│ └── metabox-generation-info.php# Post metabox template
|
||||||
│
|
│
|
||||||
├── languages/
|
├── languages/
|
||||||
│ └── prautoblogger.pot # i18n translation template
|
│ └── prautoblogger.pot # i18n translation template
|
||||||
│
|
│
|
||||||
└── tests/
|
└── tests/
|
||||||
├── unit/ # PHPUnit tests (mocked dependencies)
|
├── unit/ # PHPUnit tests (mocked dependencies)
|
||||||
└── integration/ # WordPress integration tests
|
└── integration/ # WordPress integration tests
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -191,20 +193,21 @@ prautoblogger/
|
|||||||
└──────────────────────────────┘
|
└──────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### Shortcode Attributes
|
**Shortcode Attributes**
|
||||||
|
|
||||||
| Attribute | Default | Description |
|
| Attribute | Default | Description |
|
||||||
|------------|---------|-------------|
|
|------------|----------------------------------|-------------------------|
|
||||||
| `count` | 6 | Posts to display (max 12) |
|
| `count` | `6` | Posts to display (max 12) |
|
||||||
| `category` | (all) | Filter by category slug |
|
| `category` | (all) | Filter by category slug |
|
||||||
| `title` | "Latest Research & Insights" | Widget heading |
|
| `title` | `"Latest Research & Insights"` | Widget heading |
|
||||||
| `subtitle` | "Evidence-based articles..." | Widget subheading |
|
| `subtitle` | `"Evidence-based articles..."` | Widget subheading |
|
||||||
|
|
||||||
### CSS Integration
|
**CSS Integration**
|
||||||
|
|
||||||
The widget CSS (`posts-widget.css`) uses the Peptide Starter theme's CSS custom properties (`--color-*`, `--spacing-*`, `--text-*`, `--radius-*`, `--transition-*`) with hardcoded fallbacks. This means:
|
The widget CSS (`posts-widget.css`) uses the Peptide Starter theme's CSS custom properties (`--color-*`, `--spacing-*`, `--text-*`, `--radius-*`, `--transition-*`) with hardcoded fallbacks. This means:
|
||||||
- On sites using Peptide Starter: widget automatically adapts to light/dark mode via `data-theme`.
|
|
||||||
- On other themes: fallback values render a sensible dark-themed design.
|
- **On sites using Peptide Starter:** widget automatically adapts to light/dark mode via `data-theme`.
|
||||||
|
- **On other themes:** fallback values render a sensible dark-themed design.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -215,173 +218,197 @@ The widget CSS (`posts-widget.css`) uses the Peptide Starter theme's CSS custom
|
|||||||
All tables use `$wpdb->prefix` + `prautoblogger_` prefix.
|
All tables use `$wpdb->prefix` + `prautoblogger_` prefix.
|
||||||
|
|
||||||
#### `ab_source_data` — Raw social media data
|
#### `ab_source_data` — Raw social media data
|
||||||
| Column | Type | Description |
|
|
||||||
|-----------------|-------------------|------------------------------------------------|
|
| Column | Type | Description |
|
||||||
| id | BIGINT UNSIGNED | Auto-increment PK |
|
|---------------|-------------------|--------------------------------------------------|
|
||||||
| source_type | VARCHAR(50) | 'reddit', 'tiktok', 'instagram', 'youtube' |
|
| id | BIGINT UNSIGNED | Auto-increment PK |
|
||||||
| source_id | VARCHAR(255) | Platform-specific unique ID (post/comment ID) |
|
| source_type | VARCHAR(50) | 'reddit', 'tiktok', 'instagram', 'youtube' |
|
||||||
| subreddit | VARCHAR(255) | Subreddit or channel name (nullable) |
|
| source_id | VARCHAR(255) | Platform-specific unique ID (post/comment ID) |
|
||||||
| title | TEXT | Post/video title |
|
| subreddit | VARCHAR(255) | Subreddit or channel name (nullable) |
|
||||||
| content | LONGTEXT | Post body / comment text / transcript |
|
| title | TEXT | Post/video title |
|
||||||
| author | VARCHAR(255) | Original author username |
|
| content | LONGTEXT | Post body / comment text / transcript |
|
||||||
| score | INT | Upvotes/likes/engagement metric |
|
| author | VARCHAR(255) | Original author username |
|
||||||
| comment_count | INT | Number of comments/replies |
|
| score | INT | Upvotes/likes/engagement metric |
|
||||||
| permalink | VARCHAR(500) | URL to original content |
|
| comment_count | INT | Number of comments/replies |
|
||||||
| collected_at | DATETIME | When we collected this data |
|
| permalink | VARCHAR(500) | URL to original content |
|
||||||
| metadata_json | LONGTEXT | Platform-specific extra data (JSON) |
|
| collected_at | DATETIME | When we collected this data |
|
||||||
| INDEX | source_type, collected_at |
|
| metadata_json | LONGTEXT | Platform-specific extra data (JSON) |
|
||||||
| UNIQUE INDEX | source_type, source_id |
|
|
||||||
|
- INDEX: `source_type`, `collected_at`
|
||||||
|
- UNIQUE INDEX: `source_type`, `source_id`
|
||||||
|
|
||||||
#### `ab_analysis_results` — Analyzed topics and patterns
|
#### `ab_analysis_results` — Analyzed topics and patterns
|
||||||
| Column | Type | Description |
|
|
||||||
|-----------------|-------------------|------------------------------------------------|
|
| Column | Type | Description |
|
||||||
| id | BIGINT UNSIGNED | Auto-increment PK |
|
|------------------|-------------------|------------------------------------------------|
|
||||||
| analysis_type | VARCHAR(50) | 'question', 'complaint', 'comparison' |
|
| id | BIGINT UNSIGNED | Auto-increment PK |
|
||||||
| topic | VARCHAR(500) | Detected topic/theme |
|
| analysis_type | VARCHAR(50) | 'question', 'complaint', 'comparison' |
|
||||||
| summary | TEXT | LLM-generated summary of the pattern |
|
| topic | VARCHAR(500) | Detected topic/theme |
|
||||||
| frequency | INT | How many source posts mention this |
|
| summary | TEXT | LLM-generated summary of the pattern |
|
||||||
| relevance_score | FLOAT | LLM-assigned relevance score (0-1) |
|
| frequency | INT | How many source posts mention this |
|
||||||
| source_ids_json | LONGTEXT | JSON array of ab_source_data.id references |
|
| relevance_score | FLOAT | LLM-assigned relevance score (0-1) |
|
||||||
| analyzed_at | DATETIME | When analysis was performed |
|
| source_ids_json | LONGTEXT | JSON array of ab_source_data.id references |
|
||||||
| metadata_json | LONGTEXT | Extra analysis data (JSON) |
|
| analyzed_at | DATETIME | When analysis was performed |
|
||||||
| INDEX | analysis_type, relevance_score |
|
| metadata_json | LONGTEXT | Extra analysis data (JSON) |
|
||||||
| INDEX | analyzed_at |
|
|
||||||
|
- INDEX: `analysis_type`, `relevance_score`
|
||||||
|
- INDEX: `analyzed_at`
|
||||||
|
|
||||||
#### `prab_generation_log` — API cost and generation tracking
|
#### `prab_generation_log` — API cost and generation tracking
|
||||||
| Column | Type | Description |
|
|
||||||
|------------------|-------------------|-----------------------------------------------|
|
| Column | Type | Description |
|
||||||
| id | BIGINT UNSIGNED | Auto-increment PK |
|
|-------------------|-------------------|-----------------------------------------------|
|
||||||
| post_id | BIGINT UNSIGNED | WordPress post ID (nullable, set after publish)|
|
| id | BIGINT UNSIGNED | Auto-increment PK |
|
||||||
| run_id | VARCHAR(36) | UUID linking all entries from one pipeline run |
|
| post_id | BIGINT UNSIGNED | WordPress post ID (nullable, set after publish)|
|
||||||
| stage | VARCHAR(50) | 'analysis', 'outline', 'draft', 'edit', 'review' |
|
| run_id | VARCHAR(36) | UUID linking all entries from one pipeline run |
|
||||||
| provider | VARCHAR(50) | 'openrouter' |
|
| stage | VARCHAR(50) | 'analysis', 'outline', 'draft', 'edit', 'review' |
|
||||||
| model | VARCHAR(100) | Model identifier used |
|
| provider | VARCHAR(50) | 'openrouter' |
|
||||||
| prompt_tokens | INT | Input tokens consumed |
|
| model | VARCHAR(100) | Model identifier used |
|
||||||
| completion_tokens| INT | Output tokens generated |
|
| prompt_tokens | INT | Input tokens consumed |
|
||||||
| estimated_cost | DECIMAL(10,6) | Estimated USD cost |
|
| completion_tokens | INT | Output tokens generated |
|
||||||
| request_json | LONGTEXT | Full request payload (for debugging) |
|
| estimated_cost | DECIMAL(10,6) | Estimated USD cost |
|
||||||
| response_status | VARCHAR(20) | 'success', 'error', 'timeout' |
|
| request_json | LONGTEXT | Full request payload (for debugging) |
|
||||||
| error_message | TEXT | Error details if failed (nullable) |
|
| response_status | VARCHAR(20) | 'success', 'error', 'timeout' |
|
||||||
| created_at | DATETIME | When the API call was made |
|
| error_message | TEXT | Error details if failed (nullable) |
|
||||||
| INDEX | post_id |
|
| created_at | DATETIME | When the API call was made |
|
||||||
| INDEX | run_id |
|
|
||||||
| INDEX | created_at |
|
- INDEX: `post_id`
|
||||||
| INDEX | stage |
|
- INDEX: `run_id`
|
||||||
|
- INDEX: `created_at`
|
||||||
|
- INDEX: `stage`
|
||||||
|
|
||||||
#### `prab_event_log` — Structured application logging
|
#### `prab_event_log` — Structured application logging
|
||||||
| Column | Type | Description |
|
|
||||||
|------------------|-------------------|-----------------------------------------------|
|
| Column | Type | Description |
|
||||||
| id | BIGINT UNSIGNED | Auto-increment PK |
|
|------------|-------------------|-------------------------------------------------|
|
||||||
| level | VARCHAR(10) | 'error', 'warning', 'info', 'debug' |
|
| id | BIGINT UNSIGNED | Auto-increment PK |
|
||||||
| context | VARCHAR(100) | Origin: 'pipeline', 'reddit', 'ga4', etc. |
|
| level | VARCHAR(10) | 'error', 'warning', 'info', 'debug' |
|
||||||
| message | TEXT | Human-readable log message |
|
| context | VARCHAR(100) | Origin: 'pipeline', 'reddit', 'ga4', etc. |
|
||||||
| meta_json | LONGTEXT | Optional structured data (nullable) |
|
| message | TEXT | Human-readable log message |
|
||||||
| created_at | DATETIME | When the event occurred |
|
| meta_json | LONGTEXT | Optional structured data (nullable) |
|
||||||
| INDEX | level, created_at |
|
| created_at | DATETIME | When the event occurred |
|
||||||
| INDEX | created_at |
|
|
||||||
|
- INDEX: `level`, `created_at`
|
||||||
|
- INDEX: `created_at`
|
||||||
|
|
||||||
#### `ab_content_scores` — Post performance metrics
|
#### `ab_content_scores` — Post performance metrics
|
||||||
| Column | Type | Description |
|
|
||||||
|------------------|-------------------|-----------------------------------------------|
|
| Column | Type | Description |
|
||||||
| id | BIGINT UNSIGNED | Auto-increment PK |
|
|--------------------|-------------------|----------------------------------------------|
|
||||||
| post_id | BIGINT UNSIGNED | WordPress post ID |
|
| id | BIGINT UNSIGNED | Auto-increment PK |
|
||||||
| pageviews | INT | Total pageviews (WP native or GA4) |
|
| post_id | BIGINT UNSIGNED | WordPress post ID |
|
||||||
| avg_time_on_page | FLOAT | Average seconds on page (GA4) |
|
| pageviews | INT | Total pageviews (WP native or GA4) |
|
||||||
| bounce_rate | FLOAT | Bounce rate percentage (GA4) |
|
| avg_time_on_page | FLOAT | Average seconds on page (GA4) |
|
||||||
| comment_count | INT | WordPress comment count |
|
| bounce_rate | FLOAT | Bounce rate percentage (GA4) |
|
||||||
| composite_score | FLOAT | LLM-computed content quality score (0-100) |
|
| comment_count | INT | WordPress comment count |
|
||||||
| score_factors_json| LONGTEXT | JSON breakdown of what contributed to score |
|
| composite_score | FLOAT | LLM-computed content quality score (0-100) |
|
||||||
| measured_at | DATETIME | When metrics were collected |
|
| score_factors_json | LONGTEXT | JSON breakdown of what contributed to score |
|
||||||
| INDEX | post_id |
|
| measured_at | DATETIME | When metrics were collected |
|
||||||
| INDEX | measured_at |
|
|
||||||
| INDEX | composite_score |
|
- INDEX: `post_id`
|
||||||
|
- INDEX: `measured_at`
|
||||||
|
- INDEX: `composite_score`
|
||||||
|
|
||||||
### WordPress Options (`wp_options`)
|
### WordPress Options (`wp_options`)
|
||||||
|
|
||||||
All prefixed with `prautoblogger_`:
|
All prefixed with `prautoblogger_`:
|
||||||
|
|
||||||
| Option Key | Description |
|
| Option Key | Description |
|
||||||
|-------------------------------------|------------------------------------------------------|
|
|----------------------------------------|-------------------------------------------------------|
|
||||||
| `prautoblogger_openrouter_api_key` | Encrypted OpenRouter API key |
|
| `prautoblogger_openrouter_api_key` | Encrypted OpenRouter API key |
|
||||||
| `prautoblogger_reddit_client_id` | Reddit OAuth client ID |
|
| `prautoblogger_ga4_property_id` | Google Analytics 4 property ID |
|
||||||
| `prautoblogger_reddit_client_secret` | Encrypted Reddit client secret |
|
| `prautoblogger_ga4_credentials_json` | Encrypted GA4 service account credentials |
|
||||||
| `prautoblogger_reddit_access_token` | Reddit OAuth access token (transient-backed) |
|
| `prautoblogger_analysis_model` | OpenRouter model for analysis (default: cheap) |
|
||||||
| `prautoblogger_ga4_property_id` | Google Analytics 4 property ID |
|
| `prautoblogger_writing_model` | OpenRouter model for writing (default: quality) |
|
||||||
| `prautoblogger_ga4_credentials_json` | Encrypted GA4 service account credentials |
|
| `prautoblogger_editor_model` | OpenRouter model for chief editor (default: quality) |
|
||||||
| `prautoblogger_analysis_model` | OpenRouter model for analysis (default: cheap) |
|
| `prautoblogger_daily_article_target` | Number of articles per day (1-10, default: 1) |
|
||||||
| `prautoblogger_writing_model` | OpenRouter model for writing (default: quality) |
|
| `prautoblogger_writing_pipeline` | 'single_pass' or 'multi_step' (default: multi_step) |
|
||||||
| `prautoblogger_editor_model` | OpenRouter model for chief editor (default: quality) |
|
| `prautoblogger_niche_description` | Text description of the site's niche |
|
||||||
| `prautoblogger_daily_article_target` | Number of articles per day (1-10, default: 1) |
|
| `prautoblogger_target_subreddits` | JSON array of subreddits to monitor |
|
||||||
| `prautoblogger_writing_pipeline` | 'single_pass' or 'multi_step' (default: multi_step) |
|
| `prautoblogger_monthly_budget_usd` | Monthly API spend limit in USD |
|
||||||
| `prautoblogger_niche_description` | Text description of the site's niche |
|
| `prautoblogger_tone` | Content tone (informational, conversational, etc.) |
|
||||||
| `prautoblogger_target_subreddits` | JSON array of subreddits to monitor |
|
| `prautoblogger_min_word_count` | Minimum article word count (default: 800) |
|
||||||
| `prautoblogger_monthly_budget_usd` | Monthly API spend limit in USD |
|
| `prautoblogger_max_word_count` | Maximum article word count (default: 2000) |
|
||||||
| `prautoblogger_tone` | Content tone (informational, conversational, etc.) |
|
| `prautoblogger_topic_exclusions` | JSON array of topics to never write about |
|
||||||
| `prautoblogger_min_word_count` | Minimum article word count (default: 800) |
|
| `prautoblogger_enabled_sources` | JSON array of active source types |
|
||||||
| `prautoblogger_max_word_count` | Maximum article word count (default: 2000) |
|
| `prautoblogger_auto_publish` | Toggle: auto-publish approved posts (default: '0') |
|
||||||
| `prautoblogger_topic_exclusions` | JSON array of topics to never write about |
|
| `prautoblogger_default_author` | WP user ID for generated post authorship |
|
||||||
| `prautoblogger_enabled_sources` | JSON array of active source types |
|
| `prautoblogger_default_category` | WP category ID fallback for generated posts |
|
||||||
| `prautoblogger_auto_publish` | Toggle: auto-publish approved posts (default: '0') |
|
| `prautoblogger_log_level` | Logging threshold: error/warning/info/debug |
|
||||||
| `prautoblogger_default_author` | WP user ID for generated post authorship |
|
| `prautoblogger_db_version` | Schema version for migrations |
|
||||||
| `prautoblogger_default_category` | WP category ID fallback for generated posts |
|
| `prautoblogger_schedule_time` | Daily generation time (HH:MM, default: '03:00') |
|
||||||
| `prautoblogger_log_level` | Logging threshold: error/warning/info/debug |
|
|
||||||
| `prautoblogger_db_version` | Schema version for migrations |
|
|
||||||
| `prautoblogger_schedule_time` | Daily generation time (HH:MM, default: '03:00') |
|
|
||||||
|
|
||||||
### Post Meta
|
### Post Meta
|
||||||
|
|
||||||
Stored on every PRAutoBlogger-generated post:
|
Stored on every PRAutoBlogger-generated post:
|
||||||
|
|
||||||
| Meta Key | Description |
|
| Meta Key | Description |
|
||||||
|---------------------------------------|----------------------------------------------|
|
|---------------------------------------|-----------------------------------------------|
|
||||||
| `_prautoblogger_generated` | Boolean flag — '1' if generated by plugin |
|
| `_prautoblogger_generated` | Boolean flag — '1' if generated by plugin |
|
||||||
| `_prautoblogger_analysis_id` | FK to ab_analysis_results.id |
|
| `_prautoblogger_analysis_id` | FK to ab_analysis_results.id |
|
||||||
| `_prautoblogger_source_ids` | JSON array of source data IDs used |
|
| `_prautoblogger_source_ids` | JSON array of source data IDs used |
|
||||||
| `_prautoblogger_model_used` | Model that generated the content |
|
| `_prautoblogger_model_used` | Model that generated the content |
|
||||||
| `_prautoblogger_pipeline_mode` | 'single_pass' or 'multi_step' |
|
| `_prautoblogger_pipeline_mode` | 'single_pass' or 'multi_step' |
|
||||||
| `_prautoblogger_total_cost` | Total USD cost for this post |
|
| `_prautoblogger_total_cost` | Total USD cost for this post |
|
||||||
| `_prautoblogger_total_tokens` | Total tokens consumed for this post |
|
| `_prautoblogger_total_tokens` | Total tokens consumed for this post |
|
||||||
| `_prautoblogger_editor_verdict` | 'approved', 'revised', 'rejected' |
|
| `_prautoblogger_editor_verdict` | 'approved', 'revised', 'rejected' |
|
||||||
| `_prautoblogger_editor_notes` | Chief editor's review notes |
|
| `_prautoblogger_editor_notes` | Chief editor's review notes |
|
||||||
| `_prautoblogger_generated_at` | ISO 8601 timestamp of generation |
|
| `_prautoblogger_generated_at` | ISO 8601 timestamp of generation |
|
||||||
| `_prautoblogger_research_sources` | JSON array of source URLs used |
|
| `_prautoblogger_research_sources` | JSON array of source URLs used |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## External API Integrations
|
## External API Integrations
|
||||||
|
|
||||||
| API | Purpose | Code Location |
|
| Service | Purpose | Auth | Rate Limit | Code |
|
||||||
|----------------|-----------------------------------|----------------------------------------|
|
|---------|---------|------|------------|------|
|
||||||
| OpenRouter | All LLM calls (analysis, writing, editing) | `providers/class-openrouter-provider.php`, `providers/class-openrouter-pricing.php` |
|
| OpenRouter | All LLM calls (analysis, writing, editing) | API key (encrypted in wp_options) | Per-model | `providers/class-openrouter-provider.php`, `providers/class-openrouter-pricing.php` |
|
||||||
| Reddit API | Pull posts/comments from subreddits | `providers/class-reddit-provider.php`, `providers/class-reddit-api-client.php` |
|
| PullPush.io | Primary Reddit data source — submissions and comments by subreddit | None (free public API) | 30 req/min, 1000/hour | `providers/class-pull-push-client.php` |
|
||||||
| Google Analytics 4 | Post performance metrics | `core/class-ga4-client.php`, `core/class-metrics-collector.php` |
|
| Reddit .json | Fallback Reddit data — appends .json to Reddit URLs | None (unauthenticated) | ~10 req/min (datacenter IPs may be blocked) | `providers/class-reddit-json-client.php` |
|
||||||
|
| Google Analytics 4 | Post performance metrics | OAuth2 service account | Standard GA4 limits | `core/class-ga4-client.php`, `core/class-metrics-collector.php` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Key Decisions
|
## Key Decisions
|
||||||
|
|
||||||
1. **OpenRouter over direct provider APIs.** Gives access to many models through one API, simplifies provider management, and lets the user switch models without code changes. Trade-off: slight latency overhead and dependency on OpenRouter's availability.
|
### #1: OpenRouter over direct provider APIs
|
||||||
|
Gives access to many models through one API, simplifies provider management, and lets the user switch models without code changes. Trade-off: slight latency overhead and dependency on OpenRouter's availability.
|
||||||
|
|
||||||
2. **Reddit OAuth API over JSON scraping.** Official API is reliable, rate-limited in a predictable way, and won't break unexpectedly. Requires user to create a Reddit app (we document this). Trade-off: more setup friction for the user.
|
### #2: Reddit as first social source (via PullPush.io + .json fallback)
|
||||||
|
Reddit has rich discussion data ideal for identifying recurring questions and pain points. PullPush.io is the primary data source (free, no auth required). Reddit .json endpoints serve as a fallback. See Key Decision #14 for the migration from Reddit OAuth. Trade-off: Reddit data is text-heavy (no video transcription needed), making it the simplest starting point.
|
||||||
|
|
||||||
3. **Chief editor agent instead of human review queue.** The user wants full automation — a second LLM pass reviews quality, SEO, and accuracy before publishing. Posts that fail editorial review are flagged for human intervention rather than published. Trade-off: doubles the LLM cost of the "review" step, but catches quality issues.
|
### #3: Chief editor agent instead of human review queue
|
||||||
|
The user wants full automation — a second LLM pass reviews quality, SEO, and accuracy before publishing. Posts that fail editorial review are flagged for human intervention rather than published. Trade-off: doubles the LLM cost of the "review" step, but catches quality issues.
|
||||||
|
|
||||||
4. **Configurable pipeline (single-pass vs. multi-step).** Some users want cheap/fast, others want high quality. Making this configurable avoids forcing one approach. Trade-off: more code paths to maintain.
|
### #4: Configurable pipeline (single-pass vs. multi-step)
|
||||||
|
Some users want cheap/fast, others want high quality. Making this configurable avoids forcing one approach. Trade-off: more code paths to maintain.
|
||||||
|
|
||||||
5. **Composite content score using LLM.** Rather than just tracking raw metrics, we periodically have the LLM evaluate what made high-performing posts succeed and low-performing posts fail. This feeds back into the analysis and generation prompts. Trade-off: additional API cost for scoring, but enables the self-improvement loop.
|
### #5: Composite content score using LLM
|
||||||
|
Rather than just tracking raw metrics, we periodically have the LLM evaluate what made high-performing posts succeed and low-performing posts fail. This feeds back into the analysis and generation prompts. Trade-off: additional API cost for scoring, but enables the self-improvement loop.
|
||||||
|
|
||||||
6. **Source provider interface for future platforms.** Reddit is first, but TikTok/Instagram/YouTube are planned. The interface pattern means adding a new source is one class implementation. Trade-off: slight over-engineering for day one, but pays off immediately at source #2.
|
### #6: Source provider interface for future platforms
|
||||||
|
Reddit is first, but TikTok/Instagram/YouTube are planned. The interface pattern means adding a new source is one class implementation. Trade-off: slight over-engineering for day one, but pays off immediately at source #2.
|
||||||
|
|
||||||
7. **Custom tables for high-volume data.** Source data, analysis results, and generation logs are high-write, time-series data. WordPress post_meta and options are wrong for this. Custom tables with proper indexes. Trade-off: more complex activation/uninstall, but correct.
|
### #7: Custom tables for high-volume data
|
||||||
|
Source data, analysis results, and generation logs are high-write, time-series data. WordPress post_meta and options are wrong for this. Custom tables with proper indexes. Trade-off: more complex activation/uninstall, but correct.
|
||||||
|
|
||||||
8. **All API keys encrypted at rest.** Using `wp_salt()` as encryption key with OpenSSL. Not bulletproof (salt is in wp-config.php) but significantly better than plaintext in wp_options. Trade-off: adds complexity to option get/set, but necessary for security.
|
### #8: All API keys encrypted at rest
|
||||||
|
Using `wp_salt()` as encryption key with OpenSSL. Not bulletproof (salt is in wp-config.php) but significantly better than plaintext in wp_options. Trade-off: adds complexity to option get/set, but necessary for security.
|
||||||
|
|
||||||
9. **Structured logger instead of raw error_log().** All application logging flows through `PRAutoBlogger_Logger` singleton, which writes to the `prab_event_log` table and forwards errors/warnings to PHP's `error_log()`. This gives users an in-admin Activity Log page with filtering by level, search, and pagination — much more accessible than server logs. Trade-off: one extra DB write per log entry, but the table is prunable and indexed.
|
### #9: Structured logger instead of raw error_log()
|
||||||
|
All application logging flows through `PRAutoBlogger_Logger` singleton, which writes to the `prab_event_log` table and forwards errors/warnings to PHP's `error_log()`. This gives users an in-admin Activity Log page with filtering by level, search, and pagination — much more accessible than server logs. Trade-off: one extra DB write per log entry, but the table is prunable and indexed.
|
||||||
|
|
||||||
10. **Database-level atomic mutex for generation lock.** Uses `INSERT IGNORE` on `wp_options` (which has a UNIQUE index on `option_name`) instead of transient-based locking. This eliminates the TOCTOU race condition where two concurrent cron runs could both read the transient as "not locked" before either sets it. Expired locks older than 1 hour are cleaned up first to prevent permanent deadlock.
|
### #10: Database-level atomic mutex for generation lock
|
||||||
|
Uses `INSERT IGNORE` on wp_options (which has a UNIQUE index on `option_name`) instead of transient-based locking. This eliminates the TOCTOU race condition where two concurrent cron runs could both read the transient as "not locked" before either sets it. Expired locks older than 1 hour are cleaned up first to prevent permanent deadlock.
|
||||||
|
|
||||||
12. **wp.element for frontend React (no build step).** The posts widget uses WordPress-bundled React (`wp.element`) with raw `createElement` calls instead of JSX. This avoids requiring a Node.js build pipeline for what is a simple card grid. Trade-off: more verbose component code, but zero tooling dependencies for the plugin consumer.
|
### #11: wp.element for frontend React (no build step)
|
||||||
|
The posts widget uses WordPress-bundled React (`wp.element`) with raw `createElement` calls instead of JSX. This avoids requiring a Node.js build pipeline for what is a simple card grid. Trade-off: more verbose component code, but zero tooling dependencies for the plugin consumer.
|
||||||
|
|
||||||
13. **REST API for frontend data fetching.** The widget fetches posts asynchronously from a dedicated REST endpoint rather than rendering server-side. This keeps initial page load fast and allows the endpoint to be cached independently (5-minute `Cache-Control`). The permission callback uses a filter (`prautoblogger_rest_posts_public`) so sites can restrict access if needed.
|
### #12: REST API for frontend data fetching
|
||||||
|
The widget fetches posts asynchronously from a dedicated REST endpoint rather than rendering server-side. This keeps initial page load fast and allows the endpoint to be cached independently (5-minute Cache-Control). The permission callback uses a filter (`prautoblogger_rest_posts_public`) so sites can restrict access if needed.
|
||||||
|
|
||||||
11. **Run-ID based log linking.** Each pipeline execution generates a UUID (`run_id`) that tags every `prab_generation_log` entry. When a post is published, `link_generation_logs()` uses `UPDATE WHERE run_id = X` to associate entries with the post_id. This is more reliable than the previous timestamp-window approach, especially in batch runs where multiple posts are generated in quick succession.
|
### #13: Run-ID based log linking
|
||||||
|
Each pipeline execution generates a UUID (`run_id`) that tags every `prab_generation_log` entry. When a post is published, `link_generation_logs()` uses `UPDATE WHERE run_id = X` to associate entries with the `post_id`. This is more reliable than the previous timestamp-window approach, especially in batch runs where multiple posts are generated in quick succession.
|
||||||
|
|
||||||
|
### #14: PullPush.io replaces Reddit OAuth
|
||||||
|
Reddit rejected our OAuth API application (April 2026). Rather than reapply or use a third-party proxy, we switched to PullPush.io as the primary data source. PullPush mirrors Reddit's submission and comment data via a free, no-auth API with 30 req/min limits. Reddit's .json endpoints (appending .json to any Reddit URL) serve as a fallback when PullPush is unavailable. The provider auto-detects PullPush availability on each collection run (cached for 5 minutes) and transparently falls back. Each collected item's metadata includes a `data_source` field (`pullpush` or `reddit_json`) for auditability. This eliminates the need for Reddit client ID/secret storage and the OAuth token lifecycle.
|
||||||
|
|||||||
Reference in New Issue
Block a user