372 lines
15 KiB
Markdown
372 lines
15 KiB
Markdown
# Scheduled Content Publishing Workflow
|
|
|
|
**Last Updated:** January 12, 2026
|
|
**Module:** Publishing / Automation
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
IGNY8 provides automated content publishing to WordPress sites. Content goes through a scheduling process before being published at the designated time.
|
|
|
|
---
|
|
|
|
## Content Lifecycle for Publishing
|
|
|
|
### Understanding Content.status vs Content.site_status
|
|
|
|
Content has **TWO separate status fields**:
|
|
|
|
1. **`status`** - Editorial workflow status
|
|
- `draft` - Being created/edited
|
|
- `review` - Submitted for review
|
|
- `approved` - Ready for publishing
|
|
- `published` - Legacy (not used for external publishing)
|
|
|
|
2. **`site_status`** - External site publishing status
|
|
- `not_published` - Not yet published to WordPress
|
|
- `scheduled` - Has a scheduled_publish_at time
|
|
- `publishing` - Currently being published
|
|
- `published` - Successfully published to WordPress
|
|
- `failed` - Publishing failed
|
|
|
|
### Publishing Flow
|
|
|
|
```
|
|
┌─────────────────┐
|
|
│ DRAFT │ ← Content is being created/edited
|
|
│ status: draft │
|
|
└────────┬────────┘
|
|
│ User approves content
|
|
▼
|
|
┌─────────────────┐
|
|
│ APPROVED │ ← Content is ready for publishing
|
|
│ status: approved│ status='approved', site_status='not_published'
|
|
│ site_status: │
|
|
│ not_published │
|
|
└────────┬────────┘
|
|
│ Hourly: schedule_approved_content task
|
|
▼
|
|
┌─────────────────┐
|
|
│ SCHEDULED │ ← Content has a scheduled_publish_at time
|
|
│ status: approved│ site_status='scheduled'
|
|
│ site_status: │ scheduled_publish_at set to future datetime
|
|
│ scheduled │
|
|
└────────┬────────┘
|
|
│ Every 5 min: process_scheduled_publications task
|
|
│ (when scheduled_publish_at <= now)
|
|
▼
|
|
┌─────────────────┐
|
|
│ PUBLISHING │ ← WordPress API call in progress
|
|
│ status: approved│ site_status='publishing'
|
|
│ site_status: │
|
|
│ publishing │
|
|
└────────┬────────┘
|
|
│
|
|
┌────┴────┐
|
|
│ │
|
|
▼ ▼
|
|
┌────────┐ ┌────────┐
|
|
│PUBLISHED│ │ FAILED │
|
|
│status: │ │status: │
|
|
│approved │ │approved│
|
|
│site_ │ │site_ │
|
|
│status: │ │status: │
|
|
│published│ │failed │
|
|
└─────────┘ └────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Celery Tasks
|
|
|
|
### 1. `schedule_approved_content`
|
|
|
|
**Schedule:** Every hour at :00
|
|
**Task Name:** `publishing.schedule_approved_content`
|
|
**File:** `backend/igny8_core/tasks/publishing_scheduler.py`
|
|
|
|
#### What It Does:
|
|
1. Finds all sites with `PublishingSettings.auto_publish_enabled = True`
|
|
2. Gets approved content (`status='approved'`, `site_status='not_published'`, `scheduled_publish_at=null`)
|
|
3. Calculates available publishing slots based on:
|
|
- `publish_days` - which days are allowed (e.g., Mon-Fri)
|
|
- `publish_time_slots` - which times are allowed (e.g., 09:00, 14:00, 18:00)
|
|
- `daily_publish_limit` - max posts per day
|
|
- `weekly_publish_limit` - max posts per week
|
|
- `monthly_publish_limit` - max posts per month
|
|
4. Assigns `scheduled_publish_at` datetime and sets `site_status='scheduled'`
|
|
|
|
#### Configuration Location:
|
|
`PublishingSettings` model linked to each Site. Configurable via:
|
|
- Admin: `/admin/integration/publishingsettings/`
|
|
- API: `/api/v1/sites/{site_id}/publishing-settings/`
|
|
|
|
---
|
|
|
|
### 2. `process_scheduled_publications`
|
|
|
|
**Schedule:** Every 5 minutes
|
|
**Task Name:** `publishing.process_scheduled_publications`
|
|
**File:** `backend/igny8_core/tasks/publishing_scheduler.py`
|
|
|
|
#### What It Does:
|
|
1. Finds all content where:
|
|
- `site_status='scheduled'`
|
|
- `scheduled_publish_at <= now`
|
|
2. For each content item:
|
|
- Updates `site_status='publishing'`
|
|
- Gets the site's WordPress integration
|
|
- Queues `publish_content_to_wordpress` Celery task
|
|
3. Logs results and any errors
|
|
|
|
---
|
|
|
|
### 3. `publish_content_to_wordpress`
|
|
|
|
**Type:** On-demand Celery task (queued by `process_scheduled_publications`)
|
|
**Task Name:** `publishing.publish_content_to_wordpress`
|
|
**File:** `backend/igny8_core/tasks/wordpress_publishing.py`
|
|
|
|
#### What It Does:
|
|
1. **Load Content & Integration** - Gets content and WordPress credentials
|
|
2. **Check Already Published** - Skips if `external_id` exists
|
|
3. **Generate Excerpt** - Creates excerpt from HTML content
|
|
4. **Get Taxonomy Terms** - Loads categories and tags from `ContentTaxonomy`
|
|
5. **Get Images** - Loads featured image and gallery images
|
|
6. **Build API Payload** - Constructs WordPress REST API payload
|
|
7. **Call WordPress API** - POSTs to WordPress via IGNY8 Bridge plugin
|
|
8. **Update Content** - Sets `external_id`, `external_url`, `site_status='published'`
|
|
9. **Log Sync Event** - Records in `SyncEvent` model
|
|
|
|
#### WordPress Connection:
|
|
- Uses the IGNY8 WordPress Bridge plugin installed on the site
|
|
- API endpoint: `{site_url}/wp-json/igny8-bridge/v1/publish`
|
|
- Authentication: API key stored in `Site.wp_api_key`
|
|
|
|
---
|
|
|
|
## Database Models
|
|
|
|
### Content Fields (Publishing Related)
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `status` | CharField | **Editorial workflow**: `draft`, `review`, `approved` |
|
|
| `site_status` | CharField | **WordPress publishing status**: `not_published`, `scheduled`, `publishing`, `published`, `failed` |
|
|
| `site_status_updated_at` | DateTimeField | When site_status was last changed |
|
|
| `scheduled_publish_at` | DateTimeField | When content should be published (null if not scheduled) |
|
|
| `external_id` | CharField | WordPress post ID after publishing |
|
|
| `external_url` | URLField | WordPress post URL after publishing |
|
|
|
|
**Important:** These are separate concerns:
|
|
- `status` tracks editorial approval
|
|
- `site_status` tracks external publishing
|
|
- Content typically has `status='approved'` AND `site_status='not_published'` before scheduling
|
|
|
|
### PublishingSettings Fields
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `site` | ForeignKey | The site these settings apply to |
|
|
| `auto_publish_enabled` | BooleanField | Whether automatic scheduling is enabled |
|
|
| `publish_days` | JSONField | List of allowed days: `['mon', 'tue', 'wed', 'thu', 'fri']` |
|
|
| `publish_time_slots` | JSONField | List of times: `['09:00', '14:00', '18:00']` |
|
|
| `daily_publish_limit` | IntegerField | Max posts per day (null = unlimited) |
|
|
| `weekly_publish_limit` | IntegerField | Max posts per week (null = unlimited) |
|
|
| `monthly_publish_limit` | IntegerField | Max posts per month (null = unlimited) |
|
|
|
|
---
|
|
|
|
## Celery Beat Schedule
|
|
|
|
From `backend/igny8_core/celery.py`:
|
|
|
|
```python
|
|
app.conf.beat_schedule = {
|
|
# ...
|
|
'schedule-approved-content': {
|
|
'task': 'publishing.schedule_approved_content',
|
|
'schedule': crontab(minute=0), # Every hour at :00
|
|
},
|
|
'process-scheduled-publications': {
|
|
'task': 'publishing.process_scheduled_publications',
|
|
'schedule': crontab(minute='*/5'), # Every 5 minutes
|
|
},
|
|
# ...
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Manual Publishing
|
|
|
|
Content can also be published immediately via:
|
|
|
|
### API Endpoint
|
|
```
|
|
POST /api/v1/content/{content_id}/publish/
|
|
```
|
|
|
|
### Admin Action
|
|
In Django Admin, select content and use "Publish to WordPress" action.
|
|
|
|
---
|
|
|
|
## Monitoring & Debugging
|
|
|
|
### Log Files
|
|
- **Publish Logs:** `backend/logs/publish-sync-logs/`
|
|
- **API Logs:** `backend/logs/wordpress_api.log`
|
|
|
|
### Check Celery Status
|
|
```bash
|
|
docker compose -f docker-compose.app.yml -p igny8-app logs igny8_celery_worker
|
|
docker compose -f docker-compose.app.yml -p igny8-app logs igny8_celery_beat
|
|
```
|
|
|
|
### Check Scheduled Content
|
|
```python
|
|
# Django shell
|
|
from igny8_core.business.content.models import Content
|
|
from django.utils import timezone
|
|
|
|
# Past due content (should have been published)
|
|
Content.objects.filter(
|
|
site_status='scheduled',
|
|
scheduled_publish_at__lt=timezone.now()
|
|
).count()
|
|
|
|
# Upcoming scheduled content
|
|
Content.objects.filter(
|
|
site_status='scheduled',
|
|
scheduled_publish_at__gt=timezone.now()
|
|
).order_by('scheduled_publish_at')[:10]
|
|
```
|
|
|
|
### Manual Task Execution
|
|
```python
|
|
# Django shell
|
|
from igny8_core.tasks.publishing_scheduler import (
|
|
schedule_approved_content,
|
|
process_scheduled_publications
|
|
)
|
|
|
|
# Run scheduling task
|
|
schedule_approved_content()
|
|
|
|
# Process due publications
|
|
process_scheduled_publications()
|
|
```
|
|
|
|
---
|
|
|
|
## Error Handling
|
|
|
|
### Common Failure Reasons
|
|
|
|
| Error | Cause | Solution |
|
|
|-------|-------|----------|
|
|
| No active WordPress integration | Site doesn't have WordPress connected | Configure integration in Site settings |
|
|
| API key invalid/expired | WordPress API key issue | Regenerate API key in WordPress plugin |
|
|
| Connection timeout | WordPress site unreachable | Check site availability |
|
|
| Plugin not active | IGNY8 Bridge plugin disabled | Enable plugin in WordPress |
|
|
| Content already published | Duplicate publish attempt | Check `external_id` field |
|
|
|
|
### Retry Policy
|
|
- `publish_content_to_wordpress` has `max_retries=3`
|
|
- Automatic retry on transient failures
|
|
- Failed content marked with `site_status='failed'`
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Content Not Being Scheduled
|
|
|
|
1. Check `PublishingSettings.auto_publish_enabled` is `True`
|
|
2. Verify content has `status='approved'` and `site_status='not_published'`
|
|
3. Check `scheduled_publish_at` is null (already scheduled content won't reschedule)
|
|
4. Verify publish limits haven't been reached
|
|
|
|
### Content Not Publishing
|
|
|
|
1. Check Celery Beat is running: `docker compose logs igny8_celery_beat`
|
|
2. Check Celery Worker is running: `docker compose logs igny8_celery_worker`
|
|
3. Look for errors in worker logs
|
|
4. Verify WordPress integration is active
|
|
5. Test WordPress API connectivity
|
|
|
|
### Resetting Failed Content
|
|
|
|
```python
|
|
# Reset failed content to try again
|
|
from igny8_core.business.content.models import Content
|
|
|
|
Content.objects.filter(site_status='failed').update(
|
|
site_status='not_published',
|
|
scheduled_publish_at=None
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## Architecture Diagram
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ IGNY8 Backend │
|
|
├─────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌──────────────────┐ ┌──────────────────┐ │
|
|
│ │ Celery Beat │ │ Celery Worker │ │
|
|
│ │ │ │ │ │
|
|
│ │ Sends tasks at │───▶│ Executes tasks │ │
|
|
│ │ scheduled times │ │ │ │
|
|
│ └──────────────────┘ └────────┬─────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌────────────────────────────────────────────────────────┐ │
|
|
│ │ Publishing Tasks │ │
|
|
│ │ │ │
|
|
│ │ 1. schedule_approved_content (hourly) │ │
|
|
│ │ - Find approved content │ │
|
|
│ │ - Calculate publish slots │ │
|
|
│ │ - Set scheduled_publish_at │ │
|
|
│ │ │ │
|
|
│ │ 2. process_scheduled_publications (every 5 min) │ │
|
|
│ │ - Find due content │ │
|
|
│ │ - Queue publish_content_to_wordpress │ │
|
|
│ │ │ │
|
|
│ │ 3. publish_content_to_wordpress │ │
|
|
│ │ - Build API payload │ │
|
|
│ │ - Call WordPress REST API │ │
|
|
│ │ - Update content status │ │
|
|
│ └────────────────────────────────────────────────────────┘ │
|
|
│ │ │
|
|
└───────────────────────────────────┼─────────────────────────────┘
|
|
│
|
|
▼ HTTPS
|
|
┌───────────────────────────────────────────────────────────────┐
|
|
│ WordPress Site │
|
|
├───────────────────────────────────────────────────────────────┤
|
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
|
│ │ IGNY8 Bridge Plugin │ │
|
|
│ │ │ │
|
|
│ │ /wp-json/igny8-bridge/v1/publish │ │
|
|
│ │ - Receives content payload │ │
|
|
│ │ - Creates/updates WordPress post │ │
|
|
│ │ - Handles images, categories, tags │ │
|
|
│ │ - Returns post ID and URL │ │
|
|
│ └─────────────────────────────────────────────────────────┘ │
|
|
└───────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
- [Publisher Module](../10-MODULES/PUBLISHER.md)
|
|
- [WordPress Integration](../60-PLUGINS/WORDPRESS-INTEGRATION.md)
|
|
- [Content Pipeline](CONTENT-PIPELINE.md)
|