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
-
Clone and configure:
git clone <repository> cd WPSideKick make dev-setup -
Edit credentials:
# Edit .env with your credentials nano .envRequired:
WP_APP_PASSWORD: Generate in WordPress → Users → Profile → Application PasswordsCLAUDE_API_KEY(when using Claude) orDEEPSEEK_API_KEY(when using DeepSeek)
-
Install dependencies:
make deps -
Run locally:
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).
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
# 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 <idea_id>
# Publish both drafts
wp-sk-cli ideas publish <idea_id>
# Delete an idea
wp-sk-cli ideas delete <idea_id>
# 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 <id>
# List newsletter drafts
wp-sk-cli newsletter
# Generate newsletter now
wp-sk-cli newsletter generate
# Delete a newsletter draft
wp-sk-cli newsletter delete <id>
# Render a draft in readable text
wp-sk-cli pending text <draft_post_id>
# 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)
# Build and start
make docker-build
make docker-run
# View logs
make logs
# Stop
make docker-stop
Docker Run
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
# 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
curl http://localhost:8080/api/v1/health
# Response: {"status":"ok","version":"1.0.0"}
Manual Optimization
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
curl http://localhost:8080/api/v1/status/uuid
# Response: {"status":"completed","post_id":123,...}
Pending Reviews
curl http://localhost:8080/api/v1/pending
# Response: {"count":2,"records":[...]}
Optimization Details (by Post ID)
curl http://localhost:8080/api/v1/optimization/123
# Response: { ... optimization record ... }
Apply Draft
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}.jsoncontaining 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
- SEO Meta: Optimized title (50-60 chars) and description (150-160 chars)
- Content Changes: Keyword injection, heading improvements, technical updates
- Internal Links: 3-8 relevant internal links with descriptive anchors
- FAQ Block: 4-6 questions in RankMath format (if appropriate)
- Validation: Fact-checking and broken link fixes
- 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
# 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
# HTTP
curl http://localhost:8080/api/v1/health
# Docker
docker inspect --format='{{.State.Health.Status}}' seo-optimizer
Development
Build
# Local build
make build
# Production build (optimized)
make build-prod
# Run with debug logging
make run-debug
Testing
# Run all tests
make test
# With coverage
make test-coverage
# Quick tests
make test-quick
Code Quality
# 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:
- Verify WordPress Application Password is correct
- Check WordPress REST API is accessible:
curl https://your-blog.com/wp-json/wp/v2/posts - Ensure username and password are properly set in .env
Claude API Errors (Claude Only)
Problem: "Claude API error: status 401"
Solution:
- Verify Claude API key is correct
- Check API key has sufficient credits
- 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_daysorreoptimization_cooldown_daysin 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
- 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