# SEO Optimizer for WordPress Production-ready Go application that automatically optimizes WordPress blog posts for SEO and generates new post ideas from Reddit using Claude or DeepSeek. ## Features - **Autonomous Optimization**: Automatically selects and optimizes old blog posts - **Budget-Conscious**: Single comprehensive LLM API call per post - **Multi-Language Support**: Alternates between English and Spanish (Polylang) - **Comprehensive SEO**: Meta tags, content improvements, internal linking, FAQ blocks - **Draft Revisions**: Creates draft revisions for human approval (never auto-publishes) - **Smart Selection**: Respects cooldown periods and content quality criteria - **REST API**: Manual triggers and status monitoring - **Idea Generation**: Daily SEO-focused post ideas from r/kubernetes - **Production-Ready**: Docker, Kubernetes, graceful shutdown, structured logging ## Architecture ``` ┌─────────────────┐ │ Scheduler │ Language rotation (EN → ES) │ (2.5h cycle) │ └────────┬────────┘ │ v ┌─────────────────┐ │ Post Selector │ Select oldest posts (120+ days) └────────┬────────┘ Content >= 500 words │ Cooldown: 90 days v ┌─────────────────┐ │ LLM Provider │ Single comprehensive prompt │ (Claude/DeepSeek) │ - SEO meta optimization └────────┬────────┘ - Content improvements │ - Internal links (3-8) │ - FAQ blocks │ - Fact checking v ┌─────────────────┐ │Content Builder │ Apply all optimizations └────────┬────────┘ Generate Gutenberg blocks │ v ┌─────────────────┐ │ WordPress API │ Create draft revision └─────────────────┘ (pending approval) ``` ## Quick Start ### Prerequisites - Go 1.21+ - Docker (optional) - WordPress with: - Application Password enabled - Polylang plugin - RankMath SEO plugin - REST API accessible ### Setup 1. **Clone and configure**: ```bash git clone cd WPSideKick make dev-setup ``` 2. **Edit credentials**: ```bash # Edit .env with your credentials nano .env ``` Required: - `WP_APP_PASSWORD`: Generate in WordPress → Users → Profile → Application Passwords - `CLAUDE_API_KEY` (when using Claude) or `DEEPSEEK_API_KEY` (when using DeepSeek) 3. **Install dependencies**: ```bash make deps ``` 4. **Run locally**: ```bash make run ``` ## Configuration ### Environment Variables | Variable | Description | Default | |----------|-------------|---------| | `WP_APP_PASSWORD` | WordPress Application Password | **Required** | | `LLM_PROVIDER` | LLM provider (`claude` or `deepseek`) | claude | | `CLAUDE_API_KEY` | Claude API key (when `LLM_PROVIDER=claude`) | - | | `DEEPSEEK_API_KEY` | DeepSeek API key (when `LLM_PROVIDER=deepseek`) | - | | `DEEPSEEK_BASE_URL` | DeepSeek API base URL | https://api.deepseek.com | | `DRY_RUN` | Skip WordPress draft creation/publishing | false | | `IDEAS_ENABLED` | Enable idea generation | true | | `IDEAS_INTERVAL_HOURS` | Hours between idea runs | 24 | | `IDEAS_SUBREDDIT` | Subreddit to analyze | kubernetes | | `IDEAS_LISTING` | Listing (`top`, `hot`, `new`) | top | | `IDEAS_TIME_RANGE` | Time range for top | day | | `IDEAS_MAX_REDDIT_POSTS` | Max Reddit posts per run | 20 | | `IDEAS_MAX_IDEAS` | Max ideas per run | 5 | | `IDEAS_STYLE_SAMPLES` | Recent posts used for style | 3 | | `IDEAS_MIN_SCORE` | Min Reddit score to include | 25 | | `IDEAS_MIN_COMMENTS` | Min Reddit comments to include | 5 | | `IDEAS_STORAGE_PATH` | Idea storage path | ./data/ideas | | `OUTREACH_ENABLED` | Enable outreach suggestions | true | | `OUTREACH_SUBREDDIT` | Subreddit for outreach | kubernetes | | `OUTREACH_LISTING` | Listing (`top`, `hot`, `new`) | top | | `OUTREACH_TIME_RANGE` | Time range for top | day | | `OUTREACH_MAX_REDDIT_POSTS` | Max Reddit posts per run | 20 | | `OUTREACH_MAX_SUGGESTIONS` | Max suggestions per run | 5 | | `OUTREACH_MIN_SCORE` | Min Reddit score to include | 25 | | `OUTREACH_MIN_COMMENTS` | Min Reddit comments to include | 5 | | `OUTREACH_MAX_ARTICLES` | Max internal articles to consider | 50 | | `OUTREACH_STORAGE_PATH` | Outreach storage path | ./data/outreach | | `NEWSLETTER_ENABLED` | Enable weekly newsletters | true | | `NEWSLETTER_INTERVAL_HOURS` | Hours between runs | 168 | | `NEWSLETTER_FEED_URL` | RSS feed URL | https://alexandre-vazquez.com/feed/ | | `NEWSLETTER_SUBREDDIT` | Subreddit for context | kubernetes | | `NEWSLETTER_LISTING` | Listing (`top`, `hot`, `new`) | top | | `NEWSLETTER_TIME_RANGE` | Time range for top | week | | `NEWSLETTER_MAX_FEED_ITEMS` | Max feed items | 5 | | `NEWSLETTER_MAX_REDDIT_POSTS` | Max Reddit posts | 10 | | `NEWSLETTER_MIN_SCORE` | Min Reddit score | 10 | | `NEWSLETTER_MIN_COMMENTS` | Min Reddit comments | 2 | | `NEWSLETTER_STORAGE_PATH` | Newsletter storage path | ./data/newsletters | | `OPTIMIZATION_INTERVAL_HOURS` | Hours between optimizations | 2.5 | | `LOG_LEVEL` | Logging level (INFO/DEBUG) | INFO | | `API_PORT` | REST API port | 8080 | | `WEBHOOK_URL` | Optional webhook for notifications | - | ### Configuration File Single-site setup: copy `configs/config.example.yaml` to `configs/config.yaml` and customize. Multi-site setup: copy `configs/multi.example.yaml` to `configs/config.yaml` and customize the `global` block plus each entry in `sites` (including `data_base_path` to keep storage isolated). For outreach, set `outreach.min_score` and `outreach.min_comments` per site so thresholds match subreddit traffic (for example, `kubernetes` can use higher values than `paternidad`). ```yaml wordpress: base_url: "https://your-blog.com" username: "admin" runtime: dry_run: false claude: model: "claude-sonnet-4-20250514" max_tokens: 4096 temperature: 0.3 llm: provider: "claude" deepseek: base_url: "https://api.deepseek.com" model: "deepseek-chat" max_tokens: 4096 temperature: 0.3 ideas: enabled: true interval_hours: 24 subreddit: "kubernetes" listing: "top" time_range: "day" max_reddit_posts: 20 max_ideas: 5 style_sample_posts: 3 min_score: 25 min_comments: 5 base_path: "./data/ideas" outreach: enabled: true subreddit: "kubernetes" listing: "top" time_range: "day" max_reddit_posts: 20 max_suggestions: 5 min_score: 25 min_comments: 5 max_articles: 50 base_path: "./data/outreach" newsletter: enabled: true interval_hours: 168 feed_url: "https://alexandre-vazquez.com/feed/" subreddit: "kubernetes" listing: "top" time_range: "week" max_feed_items: 5 max_reddit_posts: 10 min_score: 10 min_comments: 2 base_path: "./data/newsletters" scheduler: enabled: true interval_hours: 2.5 languages: ["en", "es"] optimization: min_content_length: 500 min_age_days: 30 max_age_days: 365 reoptimization_cooldown_days: 180 storage: base_path: "./data/optimizations" audit: base_path: "./data/audit" ``` ## CLI ```bash # List new ideas wp-sk-cli ideas # Generate ideas now wp-sk-cli ideas generate # Turn an idea into EN+ES drafts wp-sk-cli ideas draft # Publish both drafts wp-sk-cli ideas publish # Delete an idea wp-sk-cli ideas delete # List outreach suggestions wp-sk-cli outreach # Generate outreach suggestions now wp-sk-cli outreach generate # Delete an outreach suggestion wp-sk-cli outreach delete # List newsletter drafts wp-sk-cli newsletter # Generate newsletter now wp-sk-cli newsletter generate # Delete a newsletter draft wp-sk-cli newsletter delete # Render a draft in readable text wp-sk-cli pending text # Show recent audit records wp-sk-cli audit 100 # Interactive shell wp-sk-cli shell # Show CLI + server version wp-sk-cli version wp-sk-cli --version ``` ## Web UI The server now hosts a minimal Web UI at: ``` http://localhost:8080/ui/ ``` ## Docker Deployment ### Docker Compose (Recommended) ```bash # Build and start make docker-build make docker-run # View logs make logs # Stop make docker-stop ``` ### Docker Run ```bash docker build -t seo-optimizer . docker run -d \ --name seo-optimizer \ -p 8080:8080 \ -e WP_APP_PASSWORD="your-password" \ -e CLAUDE_API_KEY="your-key" \ -e OPTIMIZATION_INTERVAL_HOURS=2.5 \ seo-optimizer ``` ## Kubernetes Deployment ```bash # Edit secrets in deployment.yaml first kubectl apply -f deployments/kubernetes/deployment.yaml # Check status kubectl get pods -l app=seo-optimizer kubectl logs -f deployment/seo-optimizer # Port-forward for local access kubectl port-forward svc/seo-optimizer 8080:8080 ``` ## REST API ### Endpoints #### Health Check ```bash curl http://localhost:8080/api/v1/health # Response: {"status":"ok","version":"1.0.0"} ``` #### Manual Optimization ```bash curl -X POST http://localhost:8080/api/v1/optimize \ -H "Content-Type: application/json" \ -d '{"post_id": 123, "language": "es"}' # Response: {"job_id":"uuid","status":"accepted"} ``` #### Check Status ```bash curl http://localhost:8080/api/v1/status/uuid # Response: {"status":"completed","post_id":123,...} ``` #### Pending Reviews ```bash curl http://localhost:8080/api/v1/pending # Response: {"count":2,"records":[...]} ``` #### Optimization Details (by Post ID) ```bash curl http://localhost:8080/api/v1/optimization/123 # Response: { ... optimization record ... } ``` #### Apply Draft ```bash curl -X POST http://localhost:8080/api/v1/apply-draft \ -H "Content-Type: application/json" \ -d '{"draft_post_id": 456}' # Response: {"status":"applied","message":"Draft applied to original post and draft deleted"} ``` ### Optimization Records (File Storage) Optimizations are stored on disk for audit and review: - Base path: `storage.base_path` (default `./data/optimizations`) - Subfolders: `pending/`, `applied/`, `rejected/`, `skipped/` - Files: `{post_id}_{unix_timestamp}.json` containing the full LLM response ## How It Works ### Post Selection The optimizer selects posts based on: - **Age**: Last modified between 30 and 365 days ago - **Content**: >= 500 words - **Cooldown**: Not optimized in last 12 months - **Status**: Published only - **Order**: Oldest first ### Optimization Process 1. **SEO Meta**: Optimized title (50-60 chars) and description (150-160 chars) 2. **Content Changes**: Keyword injection, heading improvements, technical updates 3. **Internal Links**: 3-8 relevant internal links with descriptive anchors 4. **FAQ Block**: 4-6 questions in RankMath format (if appropriate) 5. **Validation**: Fact-checking and broken link fixes 6. **Draft Creation**: WordPress draft revision (requires manual approval) ### Cost Tracking - Example (Claude): Sonnet 4.5 at $3/1M input tokens and $15/1M output tokens - Est. per post: 8K input + 2K output ≈ $0.054 (€0.05) - Budget: €10/month ≈ 200 posts - Interval: 2.5 hours ≈ 10 posts/day ≈ 300 posts/month ## Monitoring ### Logs ```bash # Docker Compose docker-compose logs -f # Kubernetes kubectl logs -f deployment/seo-optimizer # JSON format (default) { "time":"2026-01-24T10:30:00Z", "level":"INFO", "msg":"Optimization completed", "post_id":123, "cost_usd":0.054, "changes":8, "links":5, "duration_seconds":45.2 } ``` ### Health Check ```bash # HTTP curl http://localhost:8080/api/v1/health # Docker docker inspect --format='{{.State.Health.Status}}' seo-optimizer ``` ## Development ### Build ```bash # Local build make build # Production build (optimized) make build-prod # Run with debug logging make run-debug ``` ### Testing ```bash # Run all tests make test # With coverage make test-coverage # Quick tests make test-quick ``` ### Code Quality ```bash # Format code make fmt # Lint (requires golangci-lint) make lint # All checks make all ``` ## Troubleshooting ### WordPress Connection Issues **Problem**: "WordPress API error: status 401" **Solution**: 1. Verify WordPress Application Password is correct 2. Check WordPress REST API is accessible: `curl https://your-blog.com/wp-json/wp/v2/posts` 3. Ensure username and password are properly set in .env ### Claude API Errors (Claude Only) **Problem**: "Claude API error: status 401" **Solution**: 1. Verify Claude API key is correct 2. Check API key has sufficient credits 3. Test API key: `curl https://api.anthropic.com/v1/messages -H "x-api-key: YOUR_KEY"` ### No Posts Selected **Problem**: "No eligible posts found for optimization" **Possible causes**: - All posts are too recent (< 120 days old) - All posts were recently optimized (< 90 days ago) - Posts don't meet minimum word count (500 words) **Solution**: - Adjust `max_age_days` or `reoptimization_cooldown_days` in config - Check post modification dates in WordPress ### Budget Exceeded **Problem**: Costs exceeding €10/month **Solution**: - Increase `interval_hours` (e.g., 3.0 or 4.0) - Reduce optimization frequency - Monitor costs in logs: `grep "cost_usd" logs/*.log` ## Security - **No Auto-Publish**: Creates draft revisions only - **Local API**: No authentication (assumes local/trusted network) - **Credentials**: Use environment variables, never commit secrets - **Non-Root**: Docker runs as non-root user (UID 1000) ## License [Add your license here] ## Support - Issues: [GitHub Issues](https://github.com/your-repo/issues) - Blog: https://alexandre-vazquez.com - Contact: [Your contact info] ## Roadmap - [ ] Prometheus metrics export - [ ] Advanced retry strategies - [ ] Image optimization suggestions - [ ] Schema markup generation - [ ] Multi-site support - [ ] Category-specific optimization strategies