Draft & Publish Workflow
Every publishable resource has two versions: a draft and a published version. The live site always shows the published version. Admins edit only the draft. Nothing goes live until they explicitly publish.
The Problem It Solves
Without a draft system, every edit is immediately visible to all visitors. A content editor halfway through rewriting a section would show a broken state to the public. The draft/publish workflow eliminates this.
How It Works
For anonymous users: the API returns the published version of every resource.
For admins in edit mode: the API returns the draft version. Edits via useCwaResourceModel (the debounced v-model binding) patch the draft.
When an admin publishes: a PATCH to the API promotes the draft to the published version. A Mercure event broadcasts the change to all open browser sessions.
The PHP Side
#[Silverback\Publishable]
#[ApiResource(mercure: true)]
#[ORM\Entity]
class Title extends AbstractComponent
{
use PublishableTrait;
#[Assert\NotBlank(groups: ['Title:published'])]
public ?string $title = null;
}
#[Silverback\Publishable]+PublishableTrait— the API bundle manages the draft/published twin automaticallypublishedAtis set on the published version when it goes liveAssert\NotBlank(groups: ['Title:published'])— validation only runs at publish time, not when saving a draft. This lets admins save incomplete work
Validation Groups
The groups: ['Title:published'] constraint pattern is important:
- Saving a draft: no
Title:publishedvalidation → empty title is allowed - Publishing: runs the
Title:publishedvalidation group → empty title fails
This prevents publishing invalid content while still allowing in-progress work to be saved.
Real-Time Updates
When the published version changes, Mercure broadcasts the update. The Nuxt module's resource store updates reactively — visitors see the new content without a page refresh. No polling, no stale cache.
Component Groups and Layouts
PublishableTrait applies per-component — the draft/publish distinction lives on each individual component entity. But adding or removing a component from a group (creating or deleting a ComponentPosition) is a separate operation with no draft state of its own.
When an admin adds a new component to a group via the inline editor, a ComponentPosition record is created immediately. The component itself starts as a draft (no publishedAt), so it is invisible to anonymous visitors — but the position exists from that moment. Publishing the component makes it visible; deleting it removes both the component and its position.
There is no "undo" for position changes beyond deleting the position. Layout-level ComponentGroup changes (e.g. adding a nav link to a shared navigation group) are also immediate and do not go through a draft state.