Files
prautoblogger/ARCHITECTURE.md
peptiderepo b5cbbef804 feat: v0.2.1 — fix encryption, diverse multi-topic generation, UX fixes
- Rewrite encryption with "enc:" prefix to permanently eliminate
  the double-encryption bug on settings save
- Add one-time migration to clear legacy non-prefixed encrypted values
- Rewrite content analyzer prompts to request N distinct diverse topics
  across 5 categories (question, complaint, comparison, news, guide)
- Pass target_count*2 from pipeline to analyzer for headroom
- Add skip_dedup flag so manual Generate Now always produces content
- Generate Now button now sends force=1 to clear stale locks
- Rename OpenRouter provider files to PSR-4 convention (hyphenated)
- Switch default models to google/gemini-2.5-flash-lite
- Clean up reddit_debug log channel to standard 'reddit'
- Bump version to 0.2.1

Cost: ~$0.003/article with Gemini 2.0 Flash, ~34s pipeline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 13:36:36 +07:00

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
│  (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:
│  (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-open-router-provider.php # OpenRouter API implementation
│   │   ├── class-open-router-pricing.php  # Model pricing lookup and cost estimation
│   │   ├── interface-source-provider.php # Contract for any social media source
│   │   ├── class-pull-push-client.php    # PullPush.io HTTP client (primary, no auth)
│   │   ├── 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-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_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

Service Purpose Auth Rate Limit Code
OpenRouter All LLM calls (analysis, writing, editing) API key (encrypted in wp_options) Per-model providers/class-open-router-provider.php, providers/class-open-router-pricing.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
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

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

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

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