Content approval

People who can edit pages or posts but not publish them send their drafts to a publisher, who publishes them or sends them back with a note. Every step is kept, and every page or post going live or being taken down is logged.

Pages and posts (the items of every collection) work the same way. This guide describes pages; Posts lists what differs.

Who can do what

One permission decides it for pages, publish wranglerpages, and one for posts, publish posts.

Person Has Can
Editor update wranglerpages, not publish wranglerpages create and edit draft pages, submit them for review, withdraw a submission
Publisher update wranglerpages and publish wranglerpages everything an editor can, and publish or take down pages, change live pages, approve submissions and ask for changes

Super admins can do everything.

Upgrading changes nothing for a site: the migrations give publish wranglerpages to every role that can update pages, and publish posts to every role that can update posts. To turn approval on for a role, take the permission away from it (Admin → Roles).

What editors see

  • New pages they create start as drafts. The page's Status shows as text, not a menu.
  • A draft page shows Submit for review, with an optional note for the publisher. While it waits, the page shows Waiting for review and a Withdraw button, and it can't be changed (nor its blocks): what the publisher decides on is what was sent. Withdrawing it makes it editable again. If a publisher asks for changes, their note shows on the page, and the editor can submit again.
  • A live page opens read-only, with Edit a draft of this page. That makes a draft (a working copy) with the page's settings, tags and blocks, and opens it. Editors change the draft freely with the usual editor and page builder; nothing changes on the site. They submit it for review like a new page, or Discard draft. A live page with a draft shows Open the draft.
  • The dashboard widget Your work in review lists their open submissions.

What publishers see

  • On a draft of a live page, Compare with the live page lists the settings that differ and each block added, changed, moved or removed, with previews of both (in the site's look when the site has a preview frame).

  • Approve & publish copies the draft over the live page: its settings, tags and blocks. The live page keeps its id, URL, place in the menu and status, and its blocks keep their ids and history (they take on the draft's content; blocks added in the draft are added, removed ones go to the trash). The draft is removed. If the live page changed after the draft was made, the publisher is warned and can Publish anyway.

  • A submission remembers what it looked like when it was sent. If a publisher changed it since, publishing warns and needs Publish anyway (under the two-person rule it has to be sent for review again instead).

  • A draft of a page that has been taken down can't be published over it: publish the page again first, or discard the draft. The page itself can still be edited and submitted.

  • Each decision (publish, schedule, request changes, unschedule) is taken once: a second click, or a scheduled publish at the same moment, is refused.

  • A publisher can also publish a draft nobody submitted; it is recorded as submitted and approved by them.

  • A page waiting for review shows who sent it, when, and two buttons: Approve & publish, and Request changes (which needs a note).

  • The dashboard widget Waiting for review lists every page and post waiting for someone who can publish it.

  • Publishers edit and publish pages directly, as before.

Scheduling

  • Beside Approve & publish, a publisher can pick a date and time and Approve & schedule. The author is emailed. The panel, the dashboard widget and the live page's banner show when it goes live.
  • Until then, people who can't publish can't change it (or a draft's blocks). The author can Withdraw it; a publisher can Unschedule it (back to review) or Publish now.
  • textstem:publish-due, run every minute by the package's schedule, publishes it as the publisher who scheduled it. If the live page changed since the draft was made, or that publisher can no longer publish, it isn't published: it goes back to review, with a note saying why, and publishers are emailed.
  • The site's scheduler has to be running (php artisan schedule:run every minute from cron) for scheduled publishing to happen.

The two-person rule

With TEXTSTEM_APPROVALS_TWO_PERSON=true, publishers can't approve, schedule or send back their own submissions: another publisher has to. A publisher's own drafts then show Submit for review like an editor's. Off by default. A submission a publisher changed after it was sent can't be approved until it is sent for review again, so another publisher sees those changes. Publishers can still edit and publish live content directly.

Idle drafts

Drafts of live pages and posts that nobody has changed, and whose review history hasn't moved, for TEXTSTEM_APPROVALS_COPY_DAYS days (90 by default) are cleared by textstem:clear-idle-drafts, run daily at 04:00. The live page or post is untouched. A week before, the people who worked on the draft (its last editor and whoever submitted it) are emailed once. Drafts waiting for review or scheduled are never cleared; a draft sent back with changes requested is, and its review is recorded as withdrawn, with a "cleared" step. Set the days to 0 to keep drafts for good; --dry-run lists what would happen.

Emails

Queued, and always in English:

  • to every publisher (except the author) when a page or post is submitted, or when a scheduled one goes back to review;
  • to the author when their page or post is published, scheduled, or sent back with a note;
  • to the people who worked on a draft a week before it is cleared for being idle.

Records

  • textstem_reviews: each submission, its status (in_review, changes_requested, scheduled, published, withdrawn), who submitted and who decided, when it is scheduled to go live (publish_at), and the version that was published; content_signature fingerprints what was sent (and, once scheduled, what was approved).
  • textstem_review_events: every step (submitted, changes requested, scheduled, unscheduled, published, not published at its time, withdrawn, discarded, cleared, closed because the page or post was deleted), with who (none for steps the scheduler took), when and any note. Kept for the life of the page; the page editor shows them under Review history.
  • The events log gets a published or unpublished entry whenever a page or post goes live or is taken down, however it happens, with who did it (for a scheduled publication, the publisher who scheduled it), the version that went live and the review that approved it. These entries are kept when old events are pruned.
  • The version that went live is taken at that moment (whatever the snapshot throttle) and marked published_at in wrangler_page_versions / post_versions; those versions are kept when old versions are pruned.

Drafts of live pages

A draft is a hidden page (wrangler_pages.working_copy_of = the live page's id) with copies of its blocks (wrangler_components.copied_from = the live block). A global scope (WithoutWorkingCopies) hides drafts from every page query, so they never appear in lists, menus, the sitemap, search, the router or the AI tools; WranglerPage::withWorkingCopies() reaches them. A draft keeps its live page's slug and URL, can never be made live itself, and is removed with its live page (closing its submission). There is one draft per live page (working_copy_of is unique); working_copy_by records who started it. working_copy_base fingerprints the live page when the draft was made, to spot changes made to it since.

Posts

  • The post editor has the same review panel, read-only view of live posts and Edit a draft of this post. Compare with the live post lists the settings that differ (posts have no blocks).
  • A draft carries the post's title, slug, description, keywords, content, picture, visibility, publish and unpublish dates, fields (meta), category, author, location, area and tags. The live post keeps its collection, place in it and status.
  • Drafts are rows of posts with working_copy_of set, hidden the same way as page drafts (Post::withWorkingCopies() reaches them), and never searchable.
  • The post policy's update and delete need publish posts on a live post; edit opens the editor without it.

Other content

Approval for a kind of content is a ContentApprovals subclass (PageApprovals, PostApprovals): which column means live, which settings a draft carries, its editor and comparison URLs, its versions, and anything that belongs to it (a page's blocks). The model uses the HasWorkingCopies trait, implements the Medialight\Textstem\Contracts\HasWorkingCopies contract, and calls LiveContentGuard and Publications from its saving and saved events. ContentApprovals::for($model) finds the right one.

How it is enforced

The rules hold however a change is made: the page editor, the page builder, table actions, the AI tools, the API and imports.

  • The page policy's update and delete need publish wranglerpages on a live page, and so do the block policy's update and delete. A new edit ability opens the editor without it, read-only on live pages.
  • Underneath, LiveContentGuard stops anyone signed in without the permission from making a page or post live, taking it down, or changing a live page (blocks included) or post, and makes the pages and posts they create drafts. Work done without anyone signed in (console commands, queued jobs) isn't affected.
esc