Claude
Skills
Sign in
Back

content-versioning

Included with Lifetime
$97 forever

Use when implementing draft/publish workflows, version history, content rollback, or audit trails. Covers versioning strategies, snapshot storage, diff generation, and version comparison APIs for headless CMS.

Security

What this skill does


# Content Versioning

Guidance for implementing version control, draft/publish workflows, and audit trails for CMS content.

## When to Use This Skill

- Implementing draft/publish workflows
- Adding version history to content types
- Building content rollback features
- Creating audit trails for compliance
- Comparing content versions

## Versioning Strategies

### Strategy 1: Separate Draft/Published Records

```csharp
public class ContentItem
{
    public Guid Id { get; set; }
    public string ContentType { get; set; } = string.Empty;
    public ContentStatus Status { get; set; }

    // Version tracking
    public int Version { get; set; }
    public Guid? PublishedVersionId { get; set; }
    public Guid? DraftVersionId { get; set; }

    // Timestamps
    public DateTime CreatedUtc { get; set; }
    public DateTime ModifiedUtc { get; set; }
    public DateTime? PublishedUtc { get; set; }
}

public class ContentVersion
{
    public Guid Id { get; set; }
    public Guid ContentItemId { get; set; }
    public int VersionNumber { get; set; }

    // Snapshot of content at this version
    public string DataJson { get; set; } = string.Empty;

    // Metadata
    public string CreatedBy { get; set; } = string.Empty;
    public DateTime CreatedUtc { get; set; }
    public string? ChangeNote { get; set; }
    public bool IsPublished { get; set; }
}

public enum ContentStatus
{
    Draft,
    Published,
    Unpublished,
    Archived
}
```

### Strategy 2: History Table Pattern

```csharp
// Current content (always latest)
public class Article
{
    public Guid Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Body { get; set; } = string.Empty;
    public int CurrentVersion { get; set; }
    public ContentStatus Status { get; set; }
}

// Automatic history tracking
public class ArticleHistory
{
    public Guid Id { get; set; }
    public Guid ArticleId { get; set; }
    public int VersionNumber { get; set; }

    // Copy of all fields at this version
    public string Title { get; set; } = string.Empty;
    public string Body { get; set; } = string.Empty;

    // Audit info
    public DateTime ValidFrom { get; set; }
    public DateTime ValidTo { get; set; }
    public string ModifiedBy { get; set; } = string.Empty;
    public ChangeType ChangeType { get; set; }
}

public enum ChangeType
{
    Created,
    Updated,
    Published,
    Unpublished,
    Deleted
}
```

### Strategy 3: Event Sourcing

```csharp
public abstract class ContentEvent
{
    public Guid Id { get; set; }
    public Guid ContentItemId { get; set; }
    public DateTime OccurredUtc { get; set; }
    public string UserId { get; set; } = string.Empty;
    public int SequenceNumber { get; set; }
}

public class ContentCreatedEvent : ContentEvent
{
    public string ContentType { get; set; } = string.Empty;
    public string InitialDataJson { get; set; } = string.Empty;
}

public class ContentUpdatedEvent : ContentEvent
{
    public Dictionary<string, FieldChange> Changes { get; set; } = new();
}

public class ContentPublishedEvent : ContentEvent
{
    public int PublishedVersion { get; set; }
}

public class FieldChange
{
    public object? OldValue { get; set; }
    public object? NewValue { get; set; }
}
```

## Draft/Publish Workflow

### Basic Implementation

```csharp
public class ContentPublishingService
{
    public async Task<ContentItem> CreateDraftAsync(
        string contentType,
        object data,
        string userId)
    {
        var item = new ContentItem
        {
            Id = Guid.NewGuid(),
            ContentType = contentType,
            Status = ContentStatus.Draft,
            Version = 1,
            CreatedUtc = DateTime.UtcNow,
            ModifiedUtc = DateTime.UtcNow
        };

        var version = new ContentVersion
        {
            Id = Guid.NewGuid(),
            ContentItemId = item.Id,
            VersionNumber = 1,
            DataJson = JsonSerializer.Serialize(data),
            CreatedBy = userId,
            CreatedUtc = DateTime.UtcNow,
            IsPublished = false
        };

        item.DraftVersionId = version.Id;

        await _repository.AddAsync(item);
        await _versionRepository.AddAsync(version);

        return item;
    }

    public async Task PublishAsync(Guid contentItemId, string userId)
    {
        var item = await _repository.GetAsync(contentItemId);
        if (item == null || item.DraftVersionId == null)
            throw new InvalidOperationException("No draft to publish");

        var draft = await _versionRepository.GetAsync(item.DraftVersionId.Value);

        // Create published version from draft
        var published = new ContentVersion
        {
            Id = Guid.NewGuid(),
            ContentItemId = item.Id,
            VersionNumber = item.Version + 1,
            DataJson = draft!.DataJson,
            CreatedBy = userId,
            CreatedUtc = DateTime.UtcNow,
            IsPublished = true
        };

        await _versionRepository.AddAsync(published);

        // Update content item
        item.Version = published.VersionNumber;
        item.PublishedVersionId = published.Id;
        item.Status = ContentStatus.Published;
        item.PublishedUtc = DateTime.UtcNow;
        item.ModifiedUtc = DateTime.UtcNow;

        await _repository.UpdateAsync(item);

        // Raise event
        await _mediator.Publish(new ContentPublishedEvent(item.Id));
    }

    public async Task UnpublishAsync(Guid contentItemId, string userId)
    {
        var item = await _repository.GetAsync(contentItemId);
        if (item == null)
            throw new InvalidOperationException("Content not found");

        item.Status = ContentStatus.Unpublished;
        item.PublishedVersionId = null;
        item.ModifiedUtc = DateTime.UtcNow;

        await _repository.UpdateAsync(item);
        await _mediator.Publish(new ContentUnpublishedEvent(item.Id));
    }
}
```

### Simultaneous Draft and Published

```csharp
public class ContentQueryService
{
    public async Task<ContentVersion?> GetPublishedAsync(Guid contentItemId)
    {
        var item = await _repository.GetAsync(contentItemId);
        if (item?.PublishedVersionId == null)
            return null;

        return await _versionRepository.GetAsync(item.PublishedVersionId.Value);
    }

    public async Task<ContentVersion?> GetDraftAsync(Guid contentItemId)
    {
        var item = await _repository.GetAsync(contentItemId);
        if (item?.DraftVersionId == null)
            return null;

        return await _versionRepository.GetAsync(item.DraftVersionId.Value);
    }

    public async Task<ContentVersion?> GetLatestAsync(
        Guid contentItemId,
        bool preferDraft = false)
    {
        var item = await _repository.GetAsync(contentItemId);
        if (item == null) return null;

        if (preferDraft && item.DraftVersionId != null)
            return await _versionRepository.GetAsync(item.DraftVersionId.Value);

        if (item.PublishedVersionId != null)
            return await _versionRepository.GetAsync(item.PublishedVersionId.Value);

        return null;
    }
}
```

## Version History

### Retrieving History

```csharp
public async Task<List<ContentVersionSummary>> GetVersionHistoryAsync(
    Guid contentItemId,
    int page = 1,
    int pageSize = 20)
{
    return await _context.ContentVersions
        .Where(v => v.ContentItemId == contentItemId)
        .OrderByDescending(v => v.VersionNumber)
        .Skip((page - 1) * pageSize)
        .Take(pageSize)
        .Select(v => new ContentVersionSummary
        {
            Id = v.Id,
            VersionNumber = v.VersionNumber,
            CreatedBy = v.CreatedBy,
            CreatedUtc = v.CreatedUtc,
            ChangeNote = v.ChangeNote,
            IsPublished = v.IsPublished
        })
        .ToListAsync();
}
```

### Rollback

```csharp
public async Task RollbackToVersionAsync(
    Guid contentItemId,
    int targetVer

Related in Security