547 lines
14 KiB
Markdown
547 lines
14 KiB
Markdown
# 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 <repository>
|
|
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 <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)
|
|
|
|
```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
|