GoWindGoWind
Home
Framework
GoWind Toolkit
  • Open Source

    • GoWind Admin (Admin Platform)
    • GoWind CMS (Content Management)
    • GoWind UBA (Behavior Analytics)
    • GoWind Shop (E-commerce)
    • GoWind Kanban (Kanban Boards)
    • GoWind Ledger (Personal Finance)
    • GoWind ShortPlay (Short Drama)
    • GoWind Ops (Operations Platform)
    • RushWind (Rust Ecosystem)
  • Open Core (Community + Enterprise)

    • GoWind OA (Office Collaboration)
    • GoWind ERP (Procure-to-Pay)
    • GoWind WMS (Warehousing)
  • Commercial

    • GoWind IM (Instant Messaging)
    • GoWind IoT (IoT Platform)
    • GoWind Edu (AI Education)
    • GoWind MES (Manufacturing Execution)
    • GoWind CDC (Data Pipeline)
    • GoWind SDP (Zero-Trust Gateway)
    • GoWind Quant (Quant Research)
    • GoWind Trade Radar (Export Sourcing)
    • GoWind Game Radar (Game Greenlight)
  • 简体中文
  • English
GitHub
Home
Framework
GoWind Toolkit
  • Open Source

    • GoWind Admin (Admin Platform)
    • GoWind CMS (Content Management)
    • GoWind UBA (Behavior Analytics)
    • GoWind Shop (E-commerce)
    • GoWind Kanban (Kanban Boards)
    • GoWind Ledger (Personal Finance)
    • GoWind ShortPlay (Short Drama)
    • GoWind Ops (Operations Platform)
    • RushWind (Rust Ecosystem)
  • Open Core (Community + Enterprise)

    • GoWind OA (Office Collaboration)
    • GoWind ERP (Procure-to-Pay)
    • GoWind WMS (Warehousing)
  • Commercial

    • GoWind IM (Instant Messaging)
    • GoWind IoT (IoT Platform)
    • GoWind Edu (AI Education)
    • GoWind MES (Manufacturing Execution)
    • GoWind CDC (Data Pipeline)
    • GoWind SDP (Zero-Trust Gateway)
    • GoWind Quant (Quant Research)
    • GoWind Trade Radar (Export Sourcing)
    • GoWind Game Radar (Game Greenlight)
  • 简体中文
  • English
GitHub
  • Introduction

    • GoWind CMS Product Introduction
    • GoWind CMS Installation Guide
  • Backend

    • CMS Backend Architecture Overview
    • CMS Backend Modules Overview
    • CMS Protobuf API Definitions
    • CMS Configuration and Deployment Guide
    • Backend Extension Mechanisms Guide
  • Frontend

    • CMS Frontend Architecture
    • CMS Frontend Modules Overview
  • Getting Started

    • Adding a Content Type, Full Stack in Practice
    • Multi-Client API Client Code Generation Tutorial
  • Core Tutorials

    • Content Multilingual Translation Tutorial
    • Content Publishing Workflow Tutorial
    • Section Editor in Practice
    • Headless API Multi-Client Integration in Practice
    • Frontend Application Development in Practice
  • Intermediate Tutorials

    • Multi-Site Management in Practice
    • Media Asset Management in Practice
    • Comment System Tutorial
    • Event Bus Architecture in Practice
    • Lua Script Extensions in Practice
  • Advanced Topics

    • Permission System in Practice
    • Dual-Client Login Security in Practice
    • Encryption and Security Toolkit Tutorial
    • Full-Text Search in Practice
    • Task Scheduling in Practice
    • Real-Time Message Push (SSE) in Practice
    • Performance Monitoring in Practice
    • Dictionary Management System Tutorial
  • Full-Stack Guides

    • Full-Stack Integration in Practice
    • Three-Service Deployment Tutorial

Content Publishing Workflow Tutorial

GoWind CMS provides a complete publishing workflow for content management, covering the entire content lifecycle from drafting, review and approval, scheduled publishing, to offline archiving. This tutorial explains the content state machine, the review flow, scheduled publishing, and workflow extensions.

Prerequisites

  • Have read CMS Backend Architecture Overview
  • Understand the CMS content model (Post / Page)

1. Content State Machine

1.1 Status Definitions

StatusProtobuf EnumVisible on FrontendEditableDescription
DraftPOST_STATUS_DRAFT (0)NoYesInitial status, visible only to the author
Pending reviewPOST_STATUS_PENDING_REVIEW (3)NoNoWaiting for admin review
PublishedPOST_STATUS_PUBLISHED (1)YesYesOfficially published, visible on the frontend
OfflinePOST_STATUS_OFFLINE (2)NoYesTemporarily offline, can be republished

1.2 Protobuf Definitions

// content/service/v1/post.proto
message Post {
  optional Status status = 5;
  enum Status {
    POST_STATUS_DRAFT = 0;
    POST_STATUS_PUBLISHED = 1;
    POST_STATUS_OFFLINE = 2;
    POST_STATUS_PENDING_REVIEW = 3;
  }

  // Publishing controls
  optional google.protobuf.Timestamp publish_at = 30 [json_name = "publishAt"];
  optional google.protobuf.Timestamp offline_at = 31 [json_name = "offlineAt"];
  optional bool pinned = 32 [json_name = "pinned"];
  optional uint32 sort_order = 33 [json_name = "sortOrder"];
}

2. Status Transitions

2.1 Status Control in the Core Service

// app/core/service/internal/service/post_service.go

// SubmitReview submits for review
func (s *PostService) SubmitReview(ctx context.Context, req *contentV1.SubmitReviewRequest) (*contentV1.Post, error) {
    post, err := s.postRepo.Get(ctx, &contentV1.GetPostRequest{
        QueryBy: &contentV1.GetPostRequest_Id{Id: req.GetId()},
    })
    if err != nil {
        return nil, err
    }

    // Status check: only drafts can be submitted for review
    if post.GetStatus() != contentV1.Post_POST_STATUS_DRAFT {
        return nil, errors.BadRequest("INVALID_STATUS", "Only drafts can be submitted for review")
    }

    return s.postRepo.UpdateStatus(ctx, req.GetId(), contentV1.Post_POST_STATUS_PENDING_REVIEW)
}

// Approve approves → auto-publish
func (s *PostService) Approve(ctx context.Context, req *contentV1.ApprovePostRequest) (*contentV1.Post, error) {
    post, err := s.postRepo.Get(ctx, &contentV1.GetPostRequest{
        QueryBy: &contentV1.GetPostRequest_Id{Id: req.GetId()},
    })
    if err != nil {
        return nil, err
    }

    if post.GetStatus() != contentV1.Post_POST_STATUS_PENDING_REVIEW {
        return nil, errors.BadRequest("INVALID_STATUS", "Only posts pending review can be approved")
    }

    published, err := s.postRepo.UpdateStatus(ctx, req.GetId(), contentV1.Post_POST_STATUS_PUBLISHED)
    if err != nil {
        return nil, err
    }

    // Publish event → triggers index sync and notification push
    s.eventbus.Publish(ctx, "post.published", published)
    return published, nil
}

// Reject rejects → back to draft
func (s *PostService) Reject(ctx context.Context, req *contentV1.RejectPostRequest) (*contentV1.Post, error) {
    return s.postRepo.UpdateStatus(ctx, req.GetId(), contentV1.Post_POST_STATUS_DRAFT)
}

2.2 Frontend Filtering

The App API query returns only published content:

// app/core/service/internal/data/post_repo.go
func (r *PostRepo) ListForApp(ctx context.Context, req *paginationV1.PagingRequest) (*contentV1.ListPostResponse, error) {
    query := r.data.db.Post.Query()

    // Mandatory frontend filter: return published posts only
    query = query.Where(post.StatusEQ(int(contentV1.Post_POST_STATUS_PUBLISHED)))

    // Publish-time filter: posts not yet due are hidden
    query = query.Where(
        post.Or(
            post.PublishAtIsNil(),
            post.PublishAtLTE(time.Now()),
        ),
    )
    // ...
}

3. Scheduled Publishing

3.1 Scheduled Publishing Mechanism

3.2 Scheduled Publishing Implementation

func (s *PostService) Create(ctx context.Context, req *contentV1.CreatePostRequest) (*contentV1.Post, error) {
    if req.Data.Status == nil {
        req.Data.Status = trans.Ptr(contentV1.Post_POST_STATUS_PUBLISHED)
    }

    post, err := s.postRepo.Create(ctx, req)
    if err != nil {
        return nil, err
    }

    // Scheduled publishing
    if publishAt := post.GetPublishAt(); publishAt != nil && publishAt.AsTime().After(time.Now()) {
        s.postRepo.UpdateStatus(ctx, post.GetId(), contentV1.Post_POST_STATUS_DRAFT)

        payload, _ := json.Marshal(task.PublishPostPayload{PostId: post.GetId()})
        s.asynqClient.Enqueue(
            asynq.NewTask(task.TypePublishPost, payload),
            asynq.ProcessAt(publishAt.AsTime()),
            asynq.Queue("default"),
        )
    }
    return post, nil
}

4. Admin Console Frontend

4.1 Status Actions

<!-- views/content/post/form.vue -->
<script setup lang="ts">
const formData = reactive({ status: 0, publishAt: null, pinned: false });

// The current status determines available actions
const canSubmitReview = computed(() => formData.status === 0);
const canPublish = computed(() => [0, 3].includes(formData.status));
const canOffline = computed(() => formData.status === 1);
const canEdit = computed(() => [0, 2].includes(formData.status));
</script>

<template>
  <Page title="Edit Post">
    <Form :model="formData">
      <FormItem label="Title">
        <Input v-model:value="formData.title" :disabled="!canEdit" />
      </FormItem>
      <FormItem label="Scheduled Publishing">
        <DatePicker v-model:value="formData.publishAt" show-time :disabled="!canEdit" />
      </FormItem>

      <Space>
        <Button v-if="canEdit" type="primary">Save Draft</Button>
        <Button v-if="canSubmitReview">Submit for Review</Button>
        <Button v-if="canPublish" type="primary">Publish Now</Button>
        <Button v-if="canOffline" danger>Take Offline</Button>
      </Space>
    </Form>
  </Page>
</template>

4.2 List Status Filtering

<template>
  <Tabs v-model:activeKey="filterStatus">
    <TabPane key="" tab="All" />
    <TabPane key="0" tab="Drafts" />
    <TabPane key="3" tab="Pending Review" />
    <TabPane key="1" tab="Published" />
    <TabPane key="2" tab="Offline" />
  </Tabs>
</template>

5. Pinning and Sorting

// Frontend query: pinned first, then by publish time descending
func (r *PostRepo) ListForApp(ctx context.Context, req *paginationV1.PagingRequest) {
    query := r.data.db.Post.Query().
        Where(post.StatusEQ(int(contentV1.Post_POST_STATUS_PUBLISHED))).
        Order(
            ent.Desc("pinned"),      // Pinned first
            ent.Desc("sort_order"),  // Custom sort
            ent.Desc("publish_at"),  // Publish time descending
        )
}

6. Recycle Bin

6.1 Soft Delete and Restore

# Soft delete (moves to the recycle bin)
DELETE /admin/v1/posts/{id}

# View the recycle bin
GET /admin/v1/posts?deleted=true

# Restore a post
PUT /admin/v1/posts/{id}/restore

# Permanent delete
DELETE /admin/v1/posts/{id}/permanent

7. Checklist

ItemDescription
Status enum definitionsDRAFT / PENDING_REVIEW / PUBLISHED / OFFLINE
Status transition endpointsSubmitReview / Approve / Reject / Publish / Offline
Scheduled publishingAsynq scheduled tasks
Frontend filteringMandatory PUBLISHED status filter
Admin UIStatus tabs + action buttons
Pinning and sortingpinned + sortOrder + publishAt
Recycle binSoft delete + restore + permanent delete

Related Documentation

  • CMS Backend Architecture Overview
  • Section/Block Editor in Practice
  • Task Scheduling in Practice
  • Event Bus Architecture
Edit this page
Last Updated:: 11/3/26, 2:14 PM
Contributors: Bobo
Prev
Content Multilingual Translation Tutorial
Next
Section Editor in Practice