orchardcore-content-items
Scannednpx machina-cli add skill CrestApps/CrestApps.AgentSkills/orchardcore-content-items --openclawOrchard Core Content Items - Prompt Templates
Working with Content Items
You are an Orchard Core expert. Generate code and configuration for managing content items in Orchard Core.
Guidelines
- Content items are the fundamental data units in Orchard Core, composed of parts and fields.
- Every content item has a
ContentItemId(stable across versions) and aContentItemVersionId(unique per version). - Use
IContentManagerfor all content item CRUD operations; do not manipulate the database directly. - Content items support draft and published states when the content type is configured as
Draftable. - Content items support versioning when the content type is configured as
Versionable. - Always call
await ISession.SaveChangesAsync()or let the request pipeline flush changes when modifying content outside ofIContentManager. - Use
IContentHandlerto hook into lifecycle events such as creating, publishing, and removing content. - Enable
OrchardCore.ContentManagementfeature to access content management APIs.
Content Item Lifecycle
A content item progresses through the following states:
- New - Created in memory with
NewAsync, not yet persisted. - Draft - Saved but not published. Only visible to editors.
- Published - Live and visible to all users. Sets
Published = trueandLatest = true. - Unpublished - Previously published, now reverted to draft state.
- Removed - Soft-deleted from the system.
When a published content item is edited, a new draft version is created while the published version remains live. Publishing the draft replaces the previous published version.
Content Item JSON Structure
{
"ContentItemId": "{{unique-content-item-id}}",
"ContentItemVersionId": "{{unique-version-id}}",
"ContentType": "{{ContentTypeName}}",
"DisplayText": "{{Display Title}}",
"Latest": true,
"Published": true,
"CreatedUtc": "2025-01-01T00:00:00Z",
"ModifiedUtc": "2025-01-01T00:00:00Z",
"PublishedUtc": "2025-01-01T00:00:00Z",
"Owner": "{{userId}}",
"Author": "{{userName}}",
"TitlePart": {
"Title": "{{Display Title}}"
},
"AutoroutePart": {
"Path": "{{url-slug}}",
"SetHomepage": false
},
"{{ContentTypeName}}": {
"{{FieldName}}": {
"Text": "{{field value}}"
}
}
}
IContentManager API
Use IContentManager to perform all content item operations:
public sealed class {{ServiceName}}
{
private readonly IContentManager _contentManager;
public {{ServiceName}}(IContentManager contentManager)
{
_contentManager = contentManager;
}
public async Task ManageContentAsync()
{
// Create a new content item in memory (not yet saved).
var contentItem = await _contentManager.NewAsync("{{ContentTypeName}}");
// Persist the content item as a draft.
await _contentManager.CreateAsync(contentItem, VersionOptions.Draft);
// Retrieve a content item by its ID (latest version).
var latest = await _contentManager.GetAsync(
contentItem.ContentItemId, VersionOptions.Latest);
// Retrieve the published version of a content item.
var published = await _contentManager.GetAsync(
contentItem.ContentItemId, VersionOptions.Published);
// Update an existing content item.
await _contentManager.UpdateAsync(contentItem);
// Publish a draft content item.
await _contentManager.PublishAsync(contentItem);
// Unpublish a published content item (revert to draft).
await _contentManager.UnpublishAsync(contentItem);
// Remove (soft-delete) a content item.
await _contentManager.RemoveAsync(contentItem);
}
}
Accessing Parts and Fields
Content items store data in parts and fields. Use the As<T>() and Get<T>() extension methods to access them:
public sealed class {{ServiceName}}
{
private readonly IContentManager _contentManager;
public {{ServiceName}}(IContentManager contentManager)
{
_contentManager = contentManager;
}
public async Task ReadPartAndFieldValuesAsync(string contentItemId)
{
var contentItem = await _contentManager.GetAsync(
contentItemId, VersionOptions.Published);
// Access a well-known part using the strongly-typed helper.
var titlePart = contentItem.As<TitlePart>();
var title = titlePart?.Title;
// Access a named part as a content element.
var customPart = contentItem.Get<ContentPart>("{{ContentTypeName}}");
// Access a field within the named part.
var textField = customPart?.Get<TextField>("{{FieldName}}");
var fieldValue = textField?.Text;
}
public async Task SetPartAndFieldValuesAsync()
{
var contentItem = await _contentManager.NewAsync("{{ContentTypeName}}");
// Set a well-known part value.
contentItem.Alter<TitlePart>(part =>
{
part.Title = "{{Title}}";
});
// Set a field value on a named part.
contentItem.Alter<ContentPart>("{{ContentTypeName}}", part =>
{
part.Alter<TextField>("{{FieldName}}", field =>
{
field.Text = "{{value}}";
});
});
await _contentManager.CreateAsync(contentItem, VersionOptions.Published);
}
}
Querying Content Items with YesSql
Use ISession to query content items through YesSql indexes:
public sealed class {{ServiceName}}
{
private readonly ISession _session;
public {{ServiceName}}(ISession session)
{
_session = session;
}
public async Task<IEnumerable<ContentItem>> QueryContentItemsAsync()
{
// Query published content items of a specific type.
var items = await _session
.Query<ContentItem, ContentItemIndex>(index =>
index.ContentType == "{{ContentTypeName}}"
&& index.Published)
.ListAsync();
return items;
}
public async Task<ContentItem?> FindByDisplayTextAsync(string displayText)
{
// Query by display text.
var item = await _session
.Query<ContentItem, ContentItemIndex>(index =>
index.DisplayText == displayText
&& index.Published)
.FirstOrDefaultAsync();
return item;
}
}
Content Handlers
Implement IContentHandler to respond to content lifecycle events. Handlers are invoked by IContentManager during create, update, publish, unpublish, and remove operations:
public sealed class {{HandlerName}} : ContentHandlerBase
{
private readonly ILogger<{{HandlerName}}> _logger;
public {{HandlerName}}(ILogger<{{HandlerName}}> logger)
{
_logger = logger;
}
public override Task PublishedAsync(PublishContentContext context)
{
if (context.ContentItem.ContentType == "{{ContentTypeName}}")
{
_logger.LogInformation(
"Content item '{DisplayText}' was published.",
context.ContentItem.DisplayText);
}
return Task.CompletedTask;
}
public override Task RemovedAsync(RemoveContentContext context)
{
if (context.ContentItem.ContentType == "{{ContentTypeName}}")
{
_logger.LogInformation(
"Content item '{DisplayText}' was removed.",
context.ContentItem.DisplayText);
}
return Task.CompletedTask;
}
}
Register the handler in a Startup class:
public sealed class Startup : StartupBase
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddContentHandler<{{HandlerName}}>();
}
}
Common handler methods include:
CreatingAsync/CreatedAsync- Before and after a content item is persisted.UpdatingAsync/UpdatedAsync- Before and after a content item is updated.PublishingAsync/PublishedAsync- Before and after a content item is published.UnpublishingAsync/UnpublishedAsync- Before and after a content item is unpublished.RemovingAsync/RemovedAsync- Before and after a content item is removed.LoadingAsync/LoadedAsync- When a content item is loaded from the store.
Content Item Import/Export via Recipes
Use the content recipe step to import or export content items:
{
"steps": [
{
"name": "Content",
"data": [
{
"ContentItemId": "{{unique-id-1}}",
"ContentType": "{{ContentTypeName}}",
"DisplayText": "{{Title}}",
"Latest": true,
"Published": true,
"TitlePart": {
"Title": "{{Title}}"
},
"{{ContentTypeName}}": {
"{{FieldName}}": {
"Text": "{{value}}"
}
}
}
]
}
]
}
- Each object in the
dataarray represents a content item to import. - Set
ContentItemIdto a stable unique value to allow re-importing without duplicates. - Set
Publishedtotrueto publish the item immediately upon import. - Part and field data are stored as nested objects matching their technical names.
ContentElement, ContentPart, and ContentField
ContentElementis the base class for all content components. It exposes the parentContentItemreference and JSON data access.ContentPartextendsContentElementand represents a composable unit attached to a content type (e.g.,TitlePart,AutoroutePart).ContentFieldextendsContentElementand represents a data field within a part (e.g.,TextField,HtmlField,NumericField).- Custom parts and fields can be created by inheriting from
ContentPartorContentFieldrespectively.
public sealed class {{PartName}} : ContentPart
{
public string? Subtitle { get; set; }
public bool IsFeatured { get; set; }
}
Source
git clone https://github.com/CrestApps/CrestApps.AgentSkills/blob/main/src/CrestApps.AgentSkills/orchardcore/orchardcore-content-items/SKILL.mdView on GitHub Overview
This skill teaches working with Orchard Core content items—from their lifecycle to accessing parts and fields. It covers the IContentManager API, content item JSON structure, YesSql querying, content handlers, and recipe import/export, enabling robust programmatic content workflows.
How This Skill Works
Content items are created and manipulated through IContentManager, with states such as New, Draft, Published, Unpublished, and Removed. Each item carries ContentItemId (stable across versions) and ContentItemVersionId (unique per version). Changes outside IContentManager require SaveChangesAsync, and lifecycle events can be extended via IContentHandler. YesSql queries enable filtering by type, status, and other criteria to drive dashboards and reports.
When to Use It
- When creating a new content type that supports draft/published workflows
- When creating or editing content items and handling draft versions
- When querying content items via YesSql for reports or dashboards
- When importing/exporting content via recipes
- When extending behavior with IContentHandler during create/publish/remove events
Quick Start
- Step 1: Inject IContentManager and call NewAsync for a new item, e.g., var item = await contentManager.NewAsync("BlogPost");
- Step 2: Persist as a draft with CreateAsync and VersionOptions.Draft, e.g., await contentManager.CreateAsync(item, VersionOptions.Draft);
- Step 3: Retrieve latest or published via GetAsync, then publish/unpublish as needed with PublishAsync/UnpublishAsync
Best Practices
- Always perform CRUD through IContentManager; do not manipulate the database directly
- Understand and respect Draftable and Versionable settings for content types
- Call SaveChangesAsync when modifying content outside IContentManager
- Use the ContentItem JSON structure as the consistent serialization contract
- Register and implement IContentHandler to hook into create, publish, and remove lifecycle events
Example Use Cases
- Create a blog post content type, create a draft, then publish to update live content
- Query latest vs published versions to power a content previews feature
- Import a batch of content items from a recipe and ensure IDs map correctly across environments
- Access TitlePart and custom fields programmatically to render in a custom widget
- Hook into content lifecycle with IContentHandler to enforce metadata on publish