Add category management - Create and update WordPress categories from AI

New Features:
- Create WordPress categories based on AI proposals
- Bulk assign posts to categories
- Confidence-based filtering (High/Medium/Low)
- Manual category creation
- Dry run mode for safe preview

New Commands:
- seo category_apply - Apply AI proposals to WordPress
- seo category_create - Create new category manually

New Modules:
- src/seo/category_manager.py - WordPress category management
  - WordPressCategoryManager: Create/get categories
  - CategoryAssignmentProcessor: Process AI proposals

Features:
- Automatic category creation if doesn't exist
- Bulk category assignment
- Confidence threshold filtering
- Append mode (doesn't replace existing categories)
- Comprehensive error handling
- Detailed statistics and logging

Usage:
./seo category_propose                    # Get AI proposals
./seo category_apply -s mistergeek.net    # Apply to site
./seo category_apply -s site -c High      # High confidence only
./seo category_create -s site "New Cat"   # Create category
./seo category_apply --dry-run            # Preview changes

Documentation:
- CATEGORY_MANAGEMENT_GUIDE.md - Complete guide

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Kevin Bataille
2026-02-16 15:25:33 +01:00
parent c8fb141cdd
commit 3c4b72c0ea
5 changed files with 824 additions and 3 deletions

View File

@@ -0,0 +1,258 @@
# Category Management Guide
## Overview
The SEO automation tool can now **create and update WordPress categories** based on AI propositions. This includes:
1. **AI-powered category proposals** - Analyze posts and suggest optimal categories
2. **Automatic category creation** - Create new categories in WordPress if they don't exist
3. **Bulk category assignment** - Assign multiple posts to categories at once
4. **Confidence-based filtering** - Only apply high-confidence recommendations
## Workflow
```
1. Export posts
2. Get AI category proposals
3. Review proposals (optional)
4. Apply to WordPress (with confidence filter)
```
## Commands
### 1. Propose Categories (`seo category_propose`)
Analyze posts and generate category recommendations.
```bash
# Propose categories for latest export
./seo category_propose
# Propose for specific CSV
./seo category_propose output/all_posts_2026-02-16.csv
# Save to custom file
./seo category_propose -o output/my_proposals.csv
```
**Output CSV columns:**
- `post_id` - Post identifier
- `title` - Post title
- `current_categories` - Current categories
- `proposed_category` - AI-suggested category
- `alternative_categories` - Alternative suggestions
- `category_reason` - Explanation
- `category_confidence` - Confidence level (High/Medium/Low)
### 2. Apply Categories (`seo category_apply`)
Apply AI category proposals to WordPress.
```bash
# Apply with default settings (Medium confidence)
./seo category_apply -s mistergeek.net
# Apply only high-confidence recommendations
./seo category_apply -s mistergeek.net -c High
# Apply specific proposals file
./seo category_apply output/category_proposals_*.csv -s webscroll.fr
# Dry run (preview changes)
./seo category_apply -s mistergeek.net --dry-run
```
**Options:**
- `-s, --site` - WordPress site (required): `mistergeek.net`, `webscroll.fr`, `hellogeek.net`
- `-c, --confidence` - Minimum confidence: `High`, `Medium`, `Low` (default: Medium)
- `--dry-run` - Preview changes without applying
### 3. Create Category (`seo category_create`)
Manually create a new category.
```bash
# Create category
./seo category_create -s mistergeek.net "VPN Reviews"
# Create with description
./seo category_create -s webscroll.fr "Torrent Clients" -d "Guides about torrent clients"
# Dry run
./seo category_create -s hellogeek.net "Test Category" --dry-run
```
## Usage Examples
### Example 1: Complete Category Update Workflow
```bash
# Step 1: Export posts
./seo export
# Step 2: Get AI category proposals
./seo category_propose
# Step 3: Review proposals (open CSV in spreadsheet)
open output/category_proposals_*.csv
# Step 4: Apply high-confidence recommendations
./seo category_apply -s mistergeek.net -c High
# Step 5: Apply remaining Medium confidence (optional)
./seo category_apply -s mistergeek.net -c Medium
```
### Example 2: Site-Specific Category Management
```bash
# For mistergeek.net (tech content)
./seo category_propose
./seo category_apply -s mistergeek.net -c Medium
# For webscroll.fr (torrent content)
./seo category_apply -s webscroll.fr -c Medium
# For hellogeek.net (misc content)
./seo category_apply -s hellogeek.net -c Low
```
### Example 3: Manual Category Creation
```bash
# Create new categories before applying
./seo category_create -s mistergeek.net "AI Tools"
./seo category_create -s mistergeek.net "VPN Reviews"
./seo category_create -s webscroll.fr "Seedbox Guides"
# Then apply AI proposals
./seo category_apply -s mistergeek.net
```
### Example 4: Safe Dry Run
```bash
# Preview what would happen
./seo category_propose
./seo category_apply -s mistergeek.net --dry-run
# Output shows:
# Would assign post 123 to "VPN"
# Would assign post 456 to "Software"
# etc.
# If satisfied, run without --dry-run
./seo category_apply -s mistergeek.net
```
## How It Works
### Category Creation Logic
1. **Check if category exists** (by slug)
2. **If exists**: Use existing category ID
3. **If not**: Create new category with AI-suggested name
4. **Assign post** to the category
### Confidence Filtering
- **High**: Only apply very confident recommendations (>90% accuracy)
- **Medium**: Apply most recommendations (default, ~80% accuracy)
- **Low**: Apply all recommendations including uncertain ones
### Safety Features
- **Dry run mode**: Preview changes before applying
- **Confidence threshold**: Filter out low-confidence suggestions
- **Append mode**: Adds to existing categories (doesn't replace)
- **Error handling**: Continues on errors, reports statistics
## API Endpoints Used
The category manager uses WordPress REST API:
```
GET /wp-json/wp/v2/categories - List categories
POST /wp-json/wp/v2/categories - Create category
GET /wp-json/wp/v2/posts/{id} - Get post details
POST /wp-json/wp/v2/posts/{id} - Update post categories
```
## Troubleshooting
### "Site not found" error
```bash
# Use exact site name
./seo category_apply -s mistergeek.net # ✓ Correct
./seo category_apply -s mistergeek # ✗ Wrong
```
### "No proposals found" error
```bash
# Run category_propose first
./seo category_propose
./seo category_apply -s mistergeek.net
```
### Authentication errors
```bash
# Check .env file has correct credentials
# WORDPRESS_MISTERGEEK_USERNAME=...
# WORDPRESS_MISTERGEEK_PASSWORD=...
```
### Categories not being created
```bash
# Check WordPress user has permission to create categories
# Requires 'manage_categories' capability
```
## Best Practices
1. **Start with High confidence**: Test with `-c High` first
2. **Review proposals**: Open CSV and review before applying
3. **Use dry run**: Always test with `--dry-run` first
4. **Backup first**: Export posts before bulk changes
5. **Monitor results**: Check WordPress admin after applying
## Programmatic Usage
```python
from seo import SEOApp, CategoryAssignmentProcessor
app = SEOApp()
# Get proposals
proposals_file = app.category_propose()
# Apply with high confidence
stats = app.category_apply(
proposals_csv=proposals_file,
site_name='mistergeek.net',
confidence='High',
dry_run=False
)
print(f"Updated {stats['posts_updated']} posts")
```
## Output Statistics
After applying categories, you'll see:
```
PROCESSING SUMMARY
Total proposals processed: 150
Categories created/found: 25
Posts updated: 142
Errors: 8
```
---
**Version**: 1.0.0
**Last Updated**: 2026-02-16
**Related**: See ENHANCED_ANALYSIS_GUIDE.md for AI analysis

View File

@@ -5,10 +5,19 @@ Single entry point for all SEO automation functionality.
__version__ = '1.0.0'
__author__ = 'SEO Automation Team'
__all__ = ['SEOApp', 'PostExporter', 'PostAnalyzer', 'CategoryProposer']
__all__ = [
'SEOApp',
'PostExporter',
'PostAnalyzer',
'EnhancedPostAnalyzer',
'CategoryProposer',
'WordPressCategoryManager',
'CategoryAssignmentProcessor'
]
# Import main classes for easy access
from .app import SEOApp
from .exporter import PostExporter
from .analyzer import PostAnalyzer, EnhancedPostAnalyzer
from .category_proposer import CategoryProposer
from .category_manager import WordPressCategoryManager, CategoryAssignmentProcessor

View File

@@ -10,6 +10,7 @@ from typing import Optional, List
from .exporter import PostExporter
from .analyzer import EnhancedPostAnalyzer
from .category_proposer import CategoryProposer
from .category_manager import WordPressCategoryManager, CategoryAssignmentProcessor
logger = logging.getLogger(__name__)
@@ -77,6 +78,55 @@ class SEOApp:
proposer = CategoryProposer(csv_file)
return proposer.run(output_file=output)
def category_apply(self, proposals_csv: str, site_name: str,
confidence: str = 'Medium', dry_run: bool = False) -> dict:
"""
Apply AI category proposals to WordPress.
Args:
proposals_csv: Path to proposals CSV
site_name: Site to apply changes to (mistergeek.net, webscroll.fr, hellogeek.net)
confidence: Minimum confidence level (High, Medium, Low)
dry_run: If True, preview changes without applying
Returns:
Statistics dict
"""
logger.info(f"📋 Applying category proposals to {site_name}...")
processor = CategoryAssignmentProcessor()
stats = processor.run(
proposals_csv=proposals_csv,
site_name=site_name,
confidence_threshold=confidence,
dry_run=dry_run
)
return stats
def category_create(self, site_name: str, category_name: str,
description: str = '') -> Optional[int]:
"""
Create a new category on a WordPress site.
Args:
site_name: Site to create category on
category_name: Name of the category
description: Category description
Returns:
Category ID if successful
"""
logger.info(f"📁 Creating category '{category_name}' on {site_name}...")
manager = WordPressCategoryManager()
category_id = manager.get_or_create_category(site_name, category_name, description)
if category_id:
logger.info(f"✓ Category created/found (ID: {category_id})")
return category_id
def status(self) -> dict:
"""Get status of output files."""
files = list(self.output_dir.glob('*.csv'))

408
src/seo/category_manager.py Normal file
View File

@@ -0,0 +1,408 @@
"""
Category Manager - Create, update, and assign categories in WordPress
"""
import csv
import logging
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional, Tuple
import requests
from requests.auth import HTTPBasicAuth
from .config import Config
logger = logging.getLogger(__name__)
class WordPressCategoryManager:
"""Manage WordPress categories: create, update, and assign to posts."""
def __init__(self):
"""Initialize category manager."""
self.sites = Config.WORDPRESS_SITES
self.category_cache = {} # Cache categories by site
def get_site_auth(self, site_name: str) -> Tuple[str, HTTPBasicAuth]:
"""Get site URL and auth for a given site name."""
site_config = self.sites.get(site_name)
if not site_config:
raise ValueError(f"Site not found: {site_name}")
base_url = site_config['url'].rstrip('/')
auth = HTTPBasicAuth(site_config['username'], site_config['password'])
return base_url, auth
def fetch_categories(self, site_name: str) -> Dict[str, int]:
"""
Fetch all categories from a WordPress site.
Returns:
Dict mapping category name (slug) to category ID
"""
if site_name in self.category_cache:
return self.category_cache[site_name]
logger.info(f"Fetching categories from {site_name}...")
try:
base_url, auth = self.get_site_auth(site_name)
categories = {}
page = 1
while True:
response = requests.get(
f"{base_url}/wp-json/wp/v2/categories",
params={'per_page': 100, 'page': page},
auth=auth,
timeout=10
)
response.raise_for_status()
page_categories = response.json()
if not page_categories:
break
for cat in page_categories:
categories[cat['slug'].lower()] = {
'id': cat['id'],
'name': cat['name'],
'slug': cat['slug'],
'count': cat.get('count', 0)
}
# Check for more pages
if len(page_categories) < 100:
break
page += 1
self.category_cache[site_name] = categories
logger.info(f"✓ Fetched {len(categories)} categories from {site_name}")
return categories
except Exception as e:
logger.error(f"Error fetching categories from {site_name}: {e}")
return {}
def create_category(self, site_name: str, category_name: str,
description: str = '', parent_id: int = 0) -> Optional[int]:
"""
Create a new category in WordPress.
Args:
site_name: Site to create category on
category_name: Name of the category
description: Category description
parent_id: Parent category ID (0 for top-level)
Returns:
Category ID if successful, None otherwise
"""
try:
base_url, auth = self.get_site_auth(site_name)
# Create slug from name
slug = category_name.lower().replace(' ', '-').replace('/', '-')
logger.info(f"Creating category '{category_name}' on {site_name}...")
response = requests.post(
f"{base_url}/wp-json/wp/v2/categories",
json={
'name': category_name,
'slug': slug,
'description': description,
'parent': parent_id
},
auth=auth,
timeout=10
)
if response.status_code == 201:
category_data = response.json()
logger.info(f"✓ Created category '{category_name}' (ID: {category_data['id']})")
# Update cache
if site_name in self.category_cache:
self.category_cache[site_name][slug] = {
'id': category_data['id'],
'name': category_data['name'],
'slug': slug,
'count': 0
}
return category_data['id']
elif response.status_code == 409:
# Category already exists
logger.info(f" Category '{category_name}' already exists")
existing = response.json()
if isinstance(existing, list) and len(existing) > 0:
return existing[0]['id']
return None
else:
logger.error(f"Error creating category: {response.status_code} - {response.text}")
return None
except Exception as e:
logger.error(f"Error creating category: {e}")
return None
def get_or_create_category(self, site_name: str, category_name: str,
description: str = '') -> Optional[int]:
"""
Get existing category or create it if it doesn't exist.
Args:
site_name: Site to work with
category_name: Name of the category
description: Category description (used if creating)
Returns:
Category ID
"""
# Fetch categories if not cached
if site_name not in self.category_cache:
self.fetch_categories(site_name)
# Check if category exists
slug = category_name.lower().replace(' ', '-').replace('/', '-')
categories = self.category_cache.get(site_name, {})
if slug in categories:
logger.info(f"✓ Found existing category '{category_name}' (ID: {categories[slug]['id']})")
return categories[slug]['id']
# Try alternative slug formats
alt_slug = category_name.lower().replace(' ', '-')
if alt_slug in categories:
logger.info(f"✓ Found existing category '{category_name}' (ID: {categories[alt_slug]['id']})")
return categories[alt_slug]['id']
# Create new category
return self.create_category(site_name, category_name, description)
def assign_post_to_category(self, site_name: str, post_id: int,
category_id: int, append: bool = True) -> bool:
"""
Assign a post to a category.
Args:
site_name: Site where post exists
post_id: Post ID
category_id: Category ID to assign
append: If True, add to existing categories; if False, replace all
Returns:
True if successful, False otherwise
"""
try:
base_url, auth = self.get_site_auth(site_name)
if append:
# Get current categories
response = requests.get(
f"{base_url}/wp-json/wp/v2/posts/{post_id}",
auth=auth,
timeout=10
)
if response.status_code == 200:
post_data = response.json()
current_categories = post_data.get('categories', [])
if category_id not in current_categories:
current_categories.append(category_id)
else:
logger.error(f"Could not fetch post {post_id}")
return False
else:
current_categories = [category_id]
# Update post with new categories
response = requests.post(
f"{base_url}/wp-json/wp/v2/posts/{post_id}",
json={'categories': current_categories},
auth=auth,
timeout=10
)
if response.status_code == 200:
logger.info(f"✓ Assigned post {post_id} to category {category_id}")
return True
else:
logger.error(f"Error assigning category: {response.status_code}")
return False
except Exception as e:
logger.error(f"Error assigning category: {e}")
return False
def bulk_assign_categories(self, site_name: str,
post_category_map: Dict[int, List[int]]) -> Dict[str, int]:
"""
Bulk assign posts to categories.
Args:
site_name: Site to work with
post_category_map: Dict mapping post_id to list of category_ids
Returns:
Statistics dict with success/failure counts
"""
stats = {'success': 0, 'failed': 0}
logger.info(f"Bulk assigning categories on {site_name}...")
for post_id, category_ids in post_category_map.items():
for category_id in category_ids:
if self.assign_post_to_category(site_name, post_id, category_id):
stats['success'] += 1
else:
stats['failed'] += 1
logger.info(f"✓ Bulk assignment complete: {stats['success']} successful, {stats['failed']} failed")
return stats
class CategoryAssignmentProcessor:
"""Process AI category proposals and apply them to WordPress."""
def __init__(self):
"""Initialize processor."""
self.category_manager = WordPressCategoryManager()
self.processing_stats = {
'total_posts': 0,
'categories_created': 0,
'posts_updated': 0,
'errors': 0
}
def load_proposals(self, proposals_csv: str) -> List[Dict]:
"""Load category proposals from CSV."""
logger.info(f"Loading proposals from: {proposals_csv}")
try:
with open(proposals_csv, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
proposals = list(reader)
logger.info(f"✓ Loaded {len(proposals)} proposals")
return proposals
except Exception as e:
logger.error(f"Error loading proposals: {e}")
return []
def process_proposals(self, proposals: List[Dict], site_name: str,
confidence_threshold: str = 'Medium',
dry_run: bool = False) -> Dict[str, int]:
"""
Process AI category proposals and apply to WordPress.
Args:
proposals: List of proposal dicts from CSV
site_name: Site to apply changes to
confidence_threshold: Minimum confidence to apply (High, Medium, Low)
dry_run: If True, don't actually make changes
Returns:
Statistics dict
"""
logger.info("\n" + "="*70)
logger.info("PROCESSING CATEGORY PROPOSALS")
logger.info("="*70)
if dry_run:
logger.info("DRY RUN - No changes will be made")
# Filter by confidence
confidence_order = {'High': 3, 'Medium': 2, 'Low': 1}
min_confidence = confidence_order.get(confidence_threshold, 2)
filtered_proposals = [
p for p in proposals
if confidence_order.get(p.get('category_confidence', 'Medium'), 2) >= min_confidence
]
logger.info(f"Filtered to {len(filtered_proposals)} proposals (confidence >= {confidence_threshold})")
# Fetch existing categories
self.category_manager.fetch_categories(site_name)
# Process each proposal
for i, proposal in enumerate(filtered_proposals, 1):
logger.info(f"\n[{i}/{len(filtered_proposals)}] Processing post {proposal.get('post_id')}...")
post_id = int(proposal.get('post_id', 0))
proposed_category = proposal.get('proposed_category', '')
current_categories = proposal.get('current_categories', '')
confidence = proposal.get('category_confidence', 'Medium')
if not post_id or not proposed_category:
logger.warning(" Skipping: Missing post_id or proposed_category")
self.processing_stats['errors'] += 1
continue
if dry_run:
logger.info(f" Would assign to: {proposed_category}")
continue
# Get or create the category
category_id = self.category_manager.get_or_create_category(
site_name,
proposed_category,
description=f"AI-proposed category (confidence: {confidence})"
)
if category_id:
self.processing_stats['categories_created'] += 1
# Assign post to category
if self.category_manager.assign_post_to_category(
site_name, post_id, category_id, append=True
):
self.processing_stats['posts_updated'] += 1
logger.info(f" ✓ Assigned to '{proposed_category}'")
else:
self.processing_stats['errors'] += 1
else:
self.processing_stats['errors'] += 1
logger.error(f" Failed to get/create category '{proposed_category}'")
self.processing_stats['total_posts'] = len(filtered_proposals)
# Print summary
logger.info("\n" + "="*70)
logger.info("PROCESSING SUMMARY")
logger.info("="*70)
logger.info(f"Total proposals processed: {self.processing_stats['total_posts']}")
logger.info(f"Categories created/found: {self.processing_stats['categories_created']}")
logger.info(f"Posts updated: {self.processing_stats['posts_updated']}")
logger.info(f"Errors: {self.processing_stats['errors']}")
return self.processing_stats
def run(self, proposals_csv: str, site_name: str,
confidence_threshold: str = 'Medium',
dry_run: bool = False) -> Dict[str, int]:
"""
Run complete category assignment process.
Args:
proposals_csv: Path to proposals CSV
site_name: Site to apply changes to
confidence_threshold: Minimum confidence to apply
dry_run: If True, preview changes without applying
Returns:
Statistics dict
"""
proposals = self.load_proposals(proposals_csv)
if not proposals:
logger.error("No proposals to process")
return self.processing_stats
return self.process_proposals(
proposals,
site_name,
confidence_threshold,
dry_run
)

View File

@@ -42,6 +42,11 @@ Examples:
help='Fields to analyze')
parser.add_argument('--update', '-u', action='store_true', help='Update input file')
parser.add_argument('--output', '-o', help='Output file path')
parser.add_argument('--confidence', '-c', choices=['High', 'Medium', 'Low'],
default='Medium', help='Confidence threshold for category apply')
parser.add_argument('--site', '-s', choices=['mistergeek.net', 'webscroll.fr', 'hellogeek.net'],
help='WordPress site for category operations')
parser.add_argument('--description', '-d', help='Category description')
args = parser.parse_args()
@@ -64,6 +69,8 @@ Examples:
'export': cmd_export,
'analyze': cmd_analyze,
'category_propose': cmd_category_propose,
'category_apply': cmd_category_apply,
'category_create': cmd_category_create,
'status': cmd_status,
'help': cmd_help,
}
@@ -139,6 +146,82 @@ def cmd_category_propose(app, args):
return 0
def cmd_category_apply(app, args):
"""Apply category proposals to WordPress."""
if args.dry_run:
print("Would apply category proposals to WordPress")
print(f" Site: {args.site}")
print(f" Confidence: {args.confidence}")
return 0
if not args.site:
print("❌ Site required. Use --site mistergeek.net|webscroll.fr|hellogeek.net")
return 1
proposals_csv = args.args[0] if args.args else None
if not proposals_csv:
# Find latest proposals
proposals_files = list(app.output_dir.glob('category_proposals_*.csv'))
if proposals_files:
proposals_csv = str(max(proposals_files, key=lambda f: f.stat().st_ctime))
else:
print("❌ No proposals CSV found. Run 'seo category_propose' first or provide file.")
return 1
print(f"Applying categories from: {proposals_csv}")
print(f"Site: {args.site}")
print(f"Confidence threshold: {args.confidence}")
stats = app.category_apply(
proposals_csv=proposals_csv,
site_name=args.site,
confidence=args.confidence,
dry_run=False
)
if stats:
print(f"\n✅ Category application complete!")
print(f" Posts processed: {stats.get('total_posts', 0)}")
print(f" Categories created/found: {stats.get('categories_created', 0)}")
print(f" Posts updated: {stats.get('posts_updated', 0)}")
print(f" Errors: {stats.get('errors', 0)}")
return 0
def cmd_category_create(app, args):
"""Create a new category on WordPress site."""
if args.dry_run:
print(f"Would create category on {args.site}")
if args.args:
print(f" Category name: {args.args[0]}")
return 0
if not args.site:
print("❌ Site required. Use --site mistergeek.net|webscroll.fr|hellogeek.net")
return 1
if not args.args:
print("❌ Category name required")
print(" Usage: seo category_create --site <site> <category_name>")
return 1
category_name = args.args[0]
description = args.description or f"Category: {category_name}"
category_id = app.category_create(
site_name=args.site,
category_name=category_name,
description=description
)
if category_id:
print(f"✅ Category '{category_name}' created/found (ID: {category_id})")
else:
print(f"❌ Failed to create category '{category_name}'")
return 0
def cmd_status(app, args):
"""Show status."""
if args.dry_run:
@@ -163,12 +246,20 @@ def cmd_help(app, args):
print("""
SEO Automation CLI - Available Commands
Basic Commands:
Export & Analysis:
export Export all posts from WordPress sites
analyze [csv_file] Analyze posts with AI
analyze -f title Analyze specific fields (title, meta_description, categories, site)
analyze -u Update input CSV with new columns (creates backup)
category_propose [csv] Propose categories based on content
Category Management:
category_apply [csv] Apply AI category proposals to WordPress
category_apply -s site -c High Apply with high confidence threshold
category_create --site site <name> Create a new category
category_create -s mistergeek.net "VPN Reviews"
Utility:
status Show output files status
help Show this help message
@@ -178,14 +269,19 @@ Options:
--fields, -f Fields to analyze: title, meta_description, categories, site
--update, -u Update input CSV file (creates backup)
--output, -o Output file path
--confidence, -c Confidence threshold: High, Medium, Low
--site, -s WordPress site: mistergeek.net, webscroll.fr, hellogeek.net
--description, -d Category description
Examples:
seo export
seo analyze
seo analyze output/all_posts_2026-02-16.csv
seo analyze -f title categories
seo analyze -u -f meta_description
seo category_propose
seo category_propose output/all_posts_2026-02-16.csv
seo category_apply -s mistergeek.net -c Medium
seo category_create -s webscroll.fr "Torrent Clients"
seo status
""")
return 0