You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The publish transition has no interception point. The hook surface lets a plugin cancel a save (content:beforeSave — throw, or errorPolicy: "abort" as noted in #526), a delete (content:beforeDelete), an email (email:beforeSend), and a comment (comment:beforeCreate) — but publishing an entry promotes an existing draft revision to live without going through any cancellable hook. content:afterPublish fires after the content is already serving.
Concrete motivator: we run EmDash for a credit-card affiliate site — a regulated vertical where partner agreements require specific disclosures and accurate rate/fee claims on every public page. We built an automated compliance reviewer (deterministic checks + rule IDs) that today runs post-publish via content:afterPublish and nightly sweeps. That leaves a window where a non-compliant edit (missing disclosure, a claim contradicting the partner data feed) is publicly served until the review runs and a human reacts. We want to close that window: review the draft revision before it goes live and refuse the publish on a rule violation, showing the editor which rule failed and why.
Preview Mode already solves the other half of this for us — a plugin can generate a signed preview URL and review the rendered draft. The cancellable publish hook is the only missing piece.
Proposed hook
New hook content:beforePublish, dispatched before a draft revision is promoted to live (and before content:afterPublish naturally). Non-exclusive; all registered handlers run.
exportinterfaceContentBeforePublishEvent{/** The draft revision that would become live */content: Record<string,unknown>;collection: string;/** The currently-live version, if the entry was already published */publishedContent: Record<string,unknown>|null;/** How the publish was initiated: admin UI, REST API token, MCP, scheduled */source: "admin"|"api"|"mcp"|"schedule";}
Dispatch semantics:
Throw (or return false) to cancel the publish. The entry stays in its current state (draft, or live-with-pending-draft).
All handlers run; if any cancels, the publish is cancelled.
Runs on every publish path — admin UI, Admin REST publish endpoint, MCP content_publish, and scheduled publishes — otherwise a gate is trivially bypassed.
Error surface: the same problem #526 raises for saves applies here — a thrown error should reach the editor as a structured, human-readable message ("Publish blocked: missing advertiser disclosure (rule SITE-2)"), not an opaque 500. If content:validate lands with its structured ContentValidationError[] shape, a publish-phase dispatch of that same shape would be ideal and this could be one hook family rather than two.
source matters for real gates: our feed-import pipeline publishes rate updates on already-published cards via the API on a nightly schedule; a compliance gate must be able to exempt or special-case automated publishes so it can't deadlock time-sensitive data syncs. Exposing how the publish was initiated lets the plugin decide instead of core guessing.
Open questions
Scheduled publishes. When the scheduler fires and a handler cancels: does the entry stay scheduled (and retry), or get unscheduled? Leaning unscheduled + surfaced in the admin, so a failing gate doesn't silently retry forever.
Latency budget. Handlers doing network work (e.g. fetching a preview render) add seconds to the publish action. Is a timeout with fail-open/fail-closed policy per hook registration acceptable, or should slow gates be expected to keep their own budget?
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Problem
The publish transition has no interception point. The hook surface lets a plugin cancel a save (
content:beforeSave— throw, orerrorPolicy: "abort"as noted in #526), a delete (content:beforeDelete), an email (email:beforeSend), and a comment (comment:beforeCreate) — but publishing an entry promotes an existing draft revision to live without going through any cancellable hook.content:afterPublishfires after the content is already serving.Concrete motivator: we run EmDash for a credit-card affiliate site — a regulated vertical where partner agreements require specific disclosures and accurate rate/fee claims on every public page. We built an automated compliance reviewer (deterministic checks + rule IDs) that today runs post-publish via
content:afterPublishand nightly sweeps. That leaves a window where a non-compliant edit (missing disclosure, a claim contradicting the partner data feed) is publicly served until the review runs and a human reacts. We want to close that window: review the draft revision before it goes live and refuse the publish on a rule violation, showing the editor which rule failed and why.Preview Mode already solves the other half of this for us — a plugin can generate a signed preview URL and review the rendered draft. The cancellable publish hook is the only missing piece.
Proposed hook
New hook
content:beforePublish, dispatched before a draft revision is promoted to live (and beforecontent:afterPublishnaturally). Non-exclusive; all registered handlers run.Dispatch semantics:
false) to cancel the publish. The entry stays in its current state (draft, or live-with-pending-draft).publishendpoint, MCPcontent_publish, and scheduled publishes — otherwise a gate is trivially bypassed.Error surface: the same problem #526 raises for saves applies here — a thrown error should reach the editor as a structured, human-readable message ("Publish blocked: missing advertiser disclosure (rule SITE-2)"), not an opaque 500. If
content:validatelands with its structuredContentValidationError[]shape, a publish-phase dispatch of that same shape would be ideal and this could be one hook family rather than two.sourcematters for real gates: our feed-import pipeline publishes rate updates on already-published cards via the API on a nightly schedule; a compliance gate must be able to exempt or special-case automated publishes so it can't deadlock time-sensitive data syncs. Exposing how the publish was initiated lets the plugin decide instead of core guessing.Open questions
content:validateis accepted, is this better shaped ascontent:validatewith aphase: "publish"rather than a separate hook?Happy to iterate on the shape here, and we'd use this immediately once it exists.
Disclosure: drafted with AI assistance (Claude Code / Claude Fable 5); the use case, review, and follow-up are human (@cyface).
All reactions