Skip to content

Publishing

Abdulkader Safi edited this page Aug 18, 2026 · 1 revision

Publishing, drafts and revisions

Editing can never change the live page by accident. That is not a workflow preference, it is the reason the content is stored in two columns rather than one.

Two columns

atelier_pages carries draft_content and published_content as separate JSON columns.

  • Editing writes the draft. Every change in the builder, immediately.
  • Publishing copies draft to published, stamps published_at, and snapshots a revision.
  • The public route reads published and nothing else. There is no code path where a draft reaches a visitor.

An unpublished page 404s rather than rendering, so a half-finished page cannot leak by somebody guessing the URL.

There is no Save button

The builder writes the draft on every change. That is deliberate: a client who loses ten minutes of work to a closed tab does not trust the tool again.

It has one consequence worth knowing. Two people editing the same page overwrite each other, silently, with no conflict warning. Fine for one editor, and worth saying out loud before you put two people in the panel.

Status

The badge in the builder toolbar and the Pages table shows one of three states:

Badge Means
Draft Never published. The public URL 404s.
Published Live, and the draft matches what is live.
Unpublished changes Live, and the draft has moved on. Publish to catch up.

The third is the one most CMSs get wrong by not showing it. It is computed by comparing the two columns, so it cannot drift out of sync with reality.

Publishing and unpublishing

Publish is in the builder toolbar and on the page settings screen. It copies the draft across, sets published_at, and writes a revision.

Unpublish is on the page settings screen, behind a confirmation. The page 404s immediately and keeps all of its content, so it is a way to take something down rather than lose it. Page::unpublish() does the same from code.

Preview links

The builder's toolbar has a link that opens the draft in a new tab. It is a temporarySignedRoute, so you can send it to a client with no panel login.

  • Expires after atelier.preview.link_expiry_minutes, 24 hours by default.
  • Carries noindex, nofollow as both a meta tag and an X-Robots-Tag header.
  • Sends Cache-Control: no-store, so no proxy keeps a copy.
  • Renders the draft in whichever locale the editor is on.
  • Uses a relative signature, so the link still validates when you reach the site through 127.0.0.1, a tunnel, or any host that is not APP_URL.

There is no permanent, non-expiring link. Every share expires.

Revisions

Every publish snapshots the tree that went live into atelier_page_revisions, with who published it.

$page->revisions;                       // newest first
$page->revisions()->count();
$page->restoreRevision($revision);      // copies it back into the draft

Snapshots hold the published tree, not the draft, so a revision is always something that was once live. That is what "put it back" means to the person asking for it.

restoreRevision() writes the draft, deliberately not the published column. Restoring is an undo you then look at and publish, not a silent change to the live site.

Old snapshots are pruned on every publish, keeping the newest atelier.revisions.keep (20 by default). Set it higher for a site that publishes rarely, lower for one that publishes constantly.

⚠️ There is no UI for revisions yet. They are written, pruned and restorable from tinker or your own code, and there is no screen for browsing or comparing them. Which means the delete confirmation in the builder is telling the truth: deleting a section is not reversible until you have published at least once since.

What publishing does not do

  • No cache to clear. Pages render per request, and the sitemap is built from two queries, so publishing takes effect immediately with nothing to invalidate.
  • No scheduled publishing. There is no publish-at date. It is on the roadmap and is not built.
  • No approval workflow. Anyone who can reach the page in the panel can publish it. Restrict that with Filament policies if a client needs it.

Doing it from code

use Safi\Atelier\Models\Page;

$page = Page::create([
    'title' => 'Pricing',
    'draft_content' => [[
        'id' => 'b_hero',
        'type' => 'hero',
        'attributes' => ['heading' => ['en' => 'Pricing', 'ar' => 'الأسعار']],
        'children' => [],
    ]],
]);

$page->setSlugs(['en' => 'pricing', 'ar' => 'as3ar']);
$page->publish();

Useful for seeders and for standing up a client site with its skeleton already in place. setSlugs() generates a slug from the title when you pass an empty string, and writes a 301 from the old URL whenever a slug changes.

Clone this wiki locally