Skip to content

Content Platform — Publishing Workflow

Editorial workflow from first draft to archived content. State machine enforced at API level — no invalid transitions.


Content State Machine

                    ┌─────────────┐
                    │    Draft    │
                    └──────┬──────┘
                           │ submit
                    ┌──────▼──────┐
              ┌─────│  In Review  │─────┐
              │     └──────┬──────┘     │
              │ reject     │ approve    │
        ┌─────▼─────┐ ┌───▼────────┐   │
        │   Draft   │ │  Approved  │   │
        │ (returned)│ └───┬────────┘   │
        └───────────┘     │            │
                ┌─────────┼─────────┐  │
                │ publish │schedule │  │
          ┌─────▼───┐ ┌───▼──────┐ │  │
          │Published│ │Scheduled │ │  │
          └────┬────┘ └────┬─────┘ │  │
               │           │ (cron) │  │
               │     ┌─────▼───┐   │  │
               │     │Published│   │  │
               │     └────┬────┘   │  │
               │          │        │  │
               └────┬─────┘        │  │
                    │ archive       │  │
               ┌────▼────┐         │  │
               │Archived │         │  │
               └─────────┘         │  │

State Definitions

SCOPE_ITEM: Draft — author is working, content not visible to public SCOPE_ITEM: In Review — submitted by author, waiting for editorial review SCOPE_ITEM: Approved — editor approved, ready for publish or scheduling SCOPE_ITEM: Scheduled — approved with future publish date set SCOPE_ITEM: Published — live, publicly accessible, indexed by search engines SCOPE_ITEM: Archived — removed from public, preserved in system OPTIONAL: Rejected — returned to author with reviewer feedback OPTIONAL: Expired — auto-archived after configured content TTL

Transition Rules

SCOPE_ITEM: Only authors can submit draft → in_review SCOPE_ITEM: Only reviewers/editors can transition in_review → approved/rejected SCOPE_ITEM: Only editors/admins can transition approved → published/scheduled SCOPE_ITEM: Only editors/admins can archive published content SCOPE_ITEM: Every transition logged in audit trail (who, when, from, to)

CHECK: No transition skips a state — draft cannot go directly to published CHECK: Reject requires a comment explaining why CHECK: Archive does not delete — content remains queryable by admins


Editorial Review Process

Review Assignment

SCOPE_ITEM: Content submitted for review appears in reviewer dashboard SCOPE_ITEM: Review queue sorted by submission date (FIFO) OPTIONAL: Auto-assign reviewer based on category expertise OPTIONAL: Review deadline with escalation notification OPTIONAL: Dual review requirement (two approvals before publish)

Review Actions

SCOPE_ITEM: Approve — content moves to "Approved" state SCOPE_ITEM: Reject — content returns to "Draft" with feedback SCOPE_ITEM: Request Changes — similar to reject, but flagged as minor fixes needed OPTIONAL: Inline commenting on specific paragraphs (annotation layer) OPTIONAL: Suggest edits (tracked changes view)

Review Checklist (Automated)

OPTIONAL: SEO completeness check — title, description, OG image present OPTIONAL: Accessibility check — all images have alt text OPTIONAL: Readability score (Flesch-Kincaid or similar) OPTIONAL: Broken link detection OPTIONAL: Spell check and grammar suggestions


Content Scheduling

Schedule Publish

SCOPE_ITEM: Set publish date and time on approved content SCOPE_ITEM: Cron job checks for due scheduled content every 60 seconds SCOPE_ITEM: On publish, trigger ISR revalidation for the content page SCOPE_ITEM: Dashboard shows upcoming scheduled content with timeline view

Implementation

// Cron job — runs every minute
async function processScheduledContent() {
  const due = await db.query.articles.findMany({
    where: and(
      eq(articles.status, 'scheduled'),
      lte(articles.scheduled_at, new Date())
    ),
  });
  for (const article of due) {
    await db.update(articles)
      .set({ status: 'published', published_at: new Date() })
      .where(eq(articles.id, article.id));
    await revalidatePath(`/articles/${article.slug}`);
    await revalidatePath('/');  // homepage with latest articles
  }
}

OPTIONAL: Schedule unpublish — auto-archive after expiry date OPTIONAL: Timezone display — show schedule time in author's local timezone

CHECK: Scheduled content is NOT accessible to public before publish time CHECK: Draft Mode preview works for scheduled content (editors can preview)


Preview Deployments

Draft Mode (Next.js)

SCOPE_ITEM: Preview URL with token-protected access SCOPE_ITEM: Draft Mode uses cookies to bypass ISR cache SCOPE_ITEM: Preview renders same components as published page SCOPE_ITEM: Visual indicator banner — "You are viewing a draft"

SCOPE_ITEM: Generate shareable preview link with expiring token SCOPE_ITEM: Token expires after 24 hours or on publish SCOPE_ITEM: Preview link works without CMS login (for stakeholder review)

IF: Client needs multi-viewport preview THEN: Add responsive preview frame (iframe with width toggle)

IF: Client needs before/after comparison THEN: Add side-by-side diff view (current published vs. draft)


Versioning and Rollback

Revision Storage

SCOPE_ITEM: Every publish creates an immutable revision snapshot SCOPE_ITEM: Revision stores full content body + metadata at that point in time SCOPE_ITEM: Revision list shows author, timestamp, and change summary

Diff View

SCOPE_ITEM: Visual diff between any two revisions SCOPE_ITEM: Inline diff for text content (added/removed highlighting) SCOPE_ITEM: Metadata diff (title, SEO fields, categories changed)

Rollback

SCOPE_ITEM: One-click rollback creates new draft from selected revision SCOPE_ITEM: Rollback does NOT overwrite current published version immediately SCOPE_ITEM: Rolled-back draft goes through normal review/publish cycle

CHECK: Rollback preserves the revision being rolled back to (no data loss) CHECK: Rollback audit trail shows which revision was restored and by whom


Content Archival

Archive Policy

SCOPE_ITEM: Archived content removed from public pages and sitemap SCOPE_ITEM: Archived content returns 410 Gone (not 404) SCOPE_ITEM: Archived content remains searchable in admin dashboard SCOPE_ITEM: Archived content can be restored to draft state

OPTIONAL: Bulk archive by date range or category OPTIONAL: Auto-archive policy (content older than N months without updates) OPTIONAL: 301 redirect setup when archiving (redirect to replacement content)

SEO Impact

IF: Archived content had significant backlinks THEN: Set up 301 redirect to most relevant current page

IF: Archived content had no significant traffic THEN: Return 410 Gone — signals intentional removal to search engines

CHECK: Sitemap regenerated on archive to exclude removed content CHECK: Internal links to archived content flagged in broken link report


Notifications

SCOPE_ITEM: Email notification to reviewer when content submitted for review SCOPE_ITEM: Email notification to author when content approved or rejected SCOPE_ITEM: Dashboard notification for pending review items OPTIONAL: Slack/webhook integration for publish events OPTIONAL: Daily digest of scheduled content for editorial team OPTIONAL: Notification when scheduled publish completes