Files
prautoblogger/ARCHITECTURE.md

28 KiB
Executable File

PRAutoBlogger — Architecture

PRAutoBlogger is a WordPress plugin that monitors social media (starting with Reddit) for recurring questions, complaints, and comparisons in a configured niche, uses LLM agents (via OpenRouter) to generate high-quality blog articles from those insights, runs them through a chief editor agent for quality review, and auto-publishes them on a configurable daily schedule. All collected data and generation metrics are stored for a self-improvement feedback loop.


Data Flow

┌─────────────────────────────────────────────────────────────────────────┐
│                         WP-Cron / Action Scheduler                      │
│                         (Daily trigger, configurable)                    │
└────────────┬────────────────────────────────────────────────────────────┘
             │
             ▼
┌─────────────────────────┐
│   1. Source Collector    │  Pulls raw posts/comments from social platforms
│   (Reddit API, etc.)    │  Stores raw data in `ab_source_data` table
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│   2. Content Analyzer   │  LLM call (cheap model) to detect:
│   (Analysis Agent)      │  - Recurring questions
│                         │  - Complaints / pain points
│                         │  - Product comparisons
│                         │  Stores analysis in `ab_analysis_results` table
│                         │  Outputs ranked article ideas
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│   3. Idea Scorer &      │  Deduplicates against existing posts
│      Deduplicator       │  Scores ideas by relevance, frequency, freshness
│                         │  Picks top N ideas (N = daily article target)
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│   4. Writer Agent       │  Configurable pipeline:
│   (Content Generator)   │  Single-pass: one LLM call → complete article
│                         │  Multi-step: outline → draft → polish
│                         │  Uses quality model via OpenRouter
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│   5. Chief Editor Agent │  LLM-powered editorial review:
│                         │  - Checks quality, accuracy, tone
│                         │  - Checks SEO (title, headings, keyword density)
│                         │  - Can request rewrites or approve
│                         │  - Approved → publish; Rejected → flag for human
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│   6. Publisher           │  Creates/updates WordPress post
│                         │  Sets categories, tags, featured image
│                         │  Stores generation metadata in post_meta
│                         │  Logs cost/tokens in `prab_generation_log` table
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│   7. Metrics Collector  │  Tracks post-publish performance:
│   (Separate cron job)   │  - WordPress native (views, comments)
│                         │  - GA4 (pageviews, bounce rate, time on page)
│                         │  - Composite "content score"
│                         │  Feeds back into Analysis step for self-improvement
└─────────────────────────┘

File Tree

prautoblogger/
├── prautoblogger.php                       # Plugin bootstrap — minimal, only hook registration
├── uninstall.php                         # Clean removal of ALL plugin data
├── readme.txt                            # WordPress.org standard readme
├── composer.json                         # Autoloading (PSR-4) and dependencies
├── ARCHITECTURE.md                       # This file
├── CONVENTIONS.md                        # Naming patterns, extension guides
├── CHANGELOG.md                          # Semantic versioning changelog
│
├── assets/
│   ├── css/
│   │   ├── admin.css                     # Admin page styles (wp-admin conventions)
│   │   └── posts-widget.css              # Frontend posts widget styles (uses theme CSS vars)
│   └── js/
│       ├── admin.js                      # Admin page interactivity (vanilla JS / Alpine.js)
│       └── posts-widget.js               # React component for frontend post cards (wp.element)
│
├── includes/
│   ├── class-prautoblogger.php             # Main orchestrator — registers all hooks via loader
│   ├── class-activator.php               # Activation: create DB tables, set defaults, schedule cron
│   ├── class-deactivator.php             # Deactivation: clear cron, cleanup transients
│   ├── class-autoloader.php              # PSR-4-style autoloader for plugin classes
│   │
│   ├── admin/
│   │   ├── class-admin-page.php          # Main settings page (tabbed SaaS-style UI)
│   │   ├── class-settings-fields.php     # Declarative settings definitions (sections + fields)
│   │   ├── class-admin-notices.php       # Onboarding notices, error alerts, budget warnings
│   │   ├── class-dashboard-widget.php    # WP Dashboard widget showing generation status
│   │   ├── class-post-metabox.php        # Metabox on posts showing generation metadata
│   │   ├── class-metrics-page.php        # Admin page for cost dashboard and content scores
│   │   ├── class-review-queue.php        # Approve/edit/reject queue for generated drafts
│   │   └── class-log-viewer.php          # Activity log viewer with level filtering
│   │
│   ├── core/
│   │   ├── class-scheduler.php           # WP-Cron / Action Scheduler job management
│   │   ├── class-source-collector.php    # Orchestrates data collection from all sources
│   │   ├── class-content-analyzer.php    # LLM-powered analysis of collected social data
│   │   ├── class-idea-scorer.php         # Ranks and deduplicates article ideas
│   │   ├── class-content-generator.php   # Writer agent — manages the generation pipeline
│   │   ├── class-chief-editor.php        # Editor agent — LLM-powered editorial review
│   │   ├── class-publisher.php           # Creates WordPress posts from approved content
│   │   ├── class-cost-tracker.php        # Logs all API costs, enforces budget limits
│   │   ├── class-logger.php             # Structured logging singleton (error/warning/info/debug)
│   │   ├── class-pipeline-runner.php    # Orchestrates the full generation pipeline
│   │   ├── class-ga4-client.php         # Google Analytics 4 API client (OAuth + Data API)
│   │   └── class-metrics-collector.php   # Collects post performance data (WP + GA4)
│   │
│   ├── providers/
│   │   ├── interface-llm-provider.php    # Contract for any LLM provider
│   │   ├── class-openrouter-provider.php # OpenRouter API implementation
│   │   ├── class-openrouter-pricing.php  # Model pricing lookup and cost estimation
│   │   ├── interface-source-provider.php # Contract for any social media source
│   │   ├── class-reddit-provider.php     # Reddit API (OAuth) implementation
│   │   ├── class-reddit-api-client.php   # Reddit HTTP client (OAuth + rate limiting)
│   │   ├── class-tiktok-provider.php     # TikTok — stub/future implementation
│   │   ├── class-instagram-provider.php  # Instagram — stub/future implementation
│   │   └── class-youtube-provider.php    # YouTube — stub/future implementation
│   │
│   ├── frontend/
│   │   └── class-posts-widget.php        # [prautoblogger_posts] shortcode + REST endpoint
│   │
│   └── models/
│       ├── class-source-data.php         # Value object: raw social media post/comment
│       ├── class-analysis-result.php     # Value object: analyzed topic with scores
│       ├── class-article-idea.php        # Value object: scored article idea
│       ├── class-content-request.php     # Value object: generation request with params
│       ├── class-editorial-review.php    # Value object: editor verdict, scores, revised content
│       ├── class-generation-log.php      # Value object: log entry for a generation run
│       └── class-content-score.php       # Value object: composite performance score
│
├── templates/
│   └── admin/
│       ├── settings-page.php             # Settings page template (tabbed sidebar layout)
│       ├── metrics-page.php              # Metrics/cost dashboard template
│       ├── review-queue.php              # Review queue table template
│       ├── log-viewer.php                # Activity log viewer template
│       └── metabox-generation-info.php   # Post metabox template
│
├── languages/
│   └── prautoblogger.pot                   # i18n translation template
│
└── tests/
    ├── unit/                             # PHPUnit tests (mocked dependencies)
    └── integration/                      # WordPress integration tests

Frontend Widget Data Flow

┌──────────────────────────────┐
│  WordPress Page/Post         │  Contains [prautoblogger_posts] shortcode
│  (e.g. Home page)            │
└────────────┬─────────────────┘
             │  Shortcode renders:
             │  1. <div id="prab-posts-root"></div>
             │  2. Enqueues posts-widget.js + posts-widget.css
             │  3. Passes config via wp_localize_script
             ▼
┌──────────────────────────────┐
│  React Component (wp.element)│  Mounts into #prab-posts-root
│  posts-widget.js             │  Shows loading skeleton on mount
└────────────┬─────────────────┘
             │  Fetches asynchronously
             ▼
┌──────────────────────────────┐
│  REST API Endpoint           │  GET /wp-json/prautoblogger/v1/posts
│  class-posts-widget.php      │  Queries published posts with
│                              │  _prautoblogger_generated = '1' meta
│                              │  Returns slim JSON (id, title, excerpt,
│                              │  url, date, category, image, word_count)
│                              │  5-minute Cache-Control header
└──────────────────────────────┘

Shortcode Attributes

Attribute Default Description
count 6 Posts to display (max 12)
category (all) Filter by category slug
title "Latest Research & Insights" Widget heading
subtitle "Evidence-based articles..." Widget subheading

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:

  • 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.

Database Schema

Custom Tables

All tables use $wpdb->prefix + prautoblogger_ prefix.

ab_source_data — Raw social media data

Column Type Description
id BIGINT UNSIGNED Auto-increment PK
source_type VARCHAR(50) 'reddit', 'tiktok', 'instagram', 'youtube'
source_id VARCHAR(255) Platform-specific unique ID (post/comment ID)
subreddit VARCHAR(255) Subreddit or channel name (nullable)
title TEXT Post/video title
content LONGTEXT Post body / comment text / transcript
author VARCHAR(255) Original author username
score INT Upvotes/likes/engagement metric
comment_count INT Number of comments/replies
permalink VARCHAR(500) URL to original content
collected_at DATETIME When we collected this data
metadata_json LONGTEXT Platform-specific extra data (JSON)
INDEX source_type, collected_at
UNIQUE INDEX source_type, source_id

ab_analysis_results — Analyzed topics and patterns

Column Type Description
id BIGINT UNSIGNED Auto-increment PK
analysis_type VARCHAR(50) 'question', 'complaint', 'comparison'
topic VARCHAR(500) Detected topic/theme
summary TEXT LLM-generated summary of the pattern
frequency INT How many source posts mention this
relevance_score FLOAT LLM-assigned relevance score (0-1)
source_ids_json LONGTEXT JSON array of ab_source_data.id references
analyzed_at DATETIME When analysis was performed
metadata_json LONGTEXT Extra analysis data (JSON)
INDEX analysis_type, relevance_score
INDEX analyzed_at

prab_generation_log — API cost and generation tracking

Column Type Description
id BIGINT UNSIGNED Auto-increment PK
post_id BIGINT UNSIGNED WordPress post ID (nullable, set after publish)
run_id VARCHAR(36) UUID linking all entries from one pipeline run
stage VARCHAR(50) 'analysis', 'outline', 'draft', 'edit', 'review'
provider VARCHAR(50) 'openrouter'
model VARCHAR(100) Model identifier used
prompt_tokens INT Input tokens consumed
completion_tokens INT Output tokens generated
estimated_cost DECIMAL(10,6) Estimated USD cost
request_json LONGTEXT Full request payload (for debugging)
response_status VARCHAR(20) 'success', 'error', 'timeout'
error_message TEXT Error details if failed (nullable)
created_at DATETIME When the API call was made
INDEX post_id
INDEX run_id
INDEX created_at
INDEX stage

prab_event_log — Structured application logging

Column Type Description
id BIGINT UNSIGNED Auto-increment PK
level VARCHAR(10) 'error', 'warning', 'info', 'debug'
context VARCHAR(100) Origin: 'pipeline', 'reddit', 'ga4', etc.
message TEXT Human-readable log message
meta_json LONGTEXT Optional structured data (nullable)
created_at DATETIME When the event occurred
INDEX level, created_at
INDEX created_at

ab_content_scores — Post performance metrics

Column Type Description
id BIGINT UNSIGNED Auto-increment PK
post_id BIGINT UNSIGNED WordPress post ID
pageviews INT Total pageviews (WP native or GA4)
avg_time_on_page FLOAT Average seconds on page (GA4)
bounce_rate FLOAT Bounce rate percentage (GA4)
comment_count INT WordPress comment count
composite_score FLOAT LLM-computed content quality score (0-100)
score_factors_json LONGTEXT JSON breakdown of what contributed to score
measured_at DATETIME When metrics were collected
INDEX post_id
INDEX measured_at
INDEX composite_score

WordPress Options (wp_options)

All prefixed with prautoblogger_:

Option Key Description
prautoblogger_openrouter_api_key Encrypted OpenRouter API key
prautoblogger_reddit_client_id Reddit OAuth client ID
prautoblogger_reddit_client_secret Encrypted Reddit client secret
prautoblogger_reddit_access_token Reddit OAuth access token (transient-backed)
prautoblogger_ga4_property_id Google Analytics 4 property ID
prautoblogger_ga4_credentials_json Encrypted GA4 service account credentials
prautoblogger_analysis_model OpenRouter model for analysis (default: cheap)
prautoblogger_writing_model OpenRouter model for writing (default: quality)
prautoblogger_editor_model OpenRouter model for chief editor (default: quality)
prautoblogger_daily_article_target Number of articles per day (1-10, default: 1)
prautoblogger_writing_pipeline 'single_pass' or 'multi_step' (default: multi_step)
prautoblogger_niche_description Text description of the site's niche
prautoblogger_target_subreddits JSON array of subreddits to monitor
prautoblogger_monthly_budget_usd Monthly API spend limit in USD
prautoblogger_tone Content tone (informational, conversational, etc.)
prautoblogger_min_word_count Minimum article word count (default: 800)
prautoblogger_max_word_count Maximum article word count (default: 2000)
prautoblogger_topic_exclusions JSON array of topics to never write about
prautoblogger_enabled_sources JSON array of active source types
prautoblogger_auto_publish Toggle: auto-publish approved posts (default: '0')
prautoblogger_default_author WP user ID for generated post authorship
prautoblogger_default_category WP category ID fallback for generated posts
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

Stored on every PRAutoBlogger-generated post:

Meta Key Description
_prautoblogger_generated Boolean flag — '1' if generated by plugin
_prautoblogger_analysis_id FK to ab_analysis_results.id
_prautoblogger_source_ids JSON array of source data IDs used
_prautoblogger_model_used Model that generated the content
_prautoblogger_pipeline_mode 'single_pass' or 'multi_step'
_prautoblogger_total_cost Total USD cost for this post
_prautoblogger_total_tokens Total tokens consumed for this post
_prautoblogger_editor_verdict 'approved', 'revised', 'rejected'
_prautoblogger_editor_notes Chief editor's review notes
_prautoblogger_generated_at ISO 8601 timestamp of generation
_prautoblogger_research_sources JSON array of source URLs used

External API Integrations

API Purpose Code Location
OpenRouter All LLM calls (analysis, writing, editing) 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
Google Analytics 4 Post performance metrics core/class-ga4-client.php, core/class-metrics-collector.php

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.

  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.

  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.

  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.

  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.

  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.

  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.

  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.

  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.