---
title: "The Posts facade — Posts for Laravel | Roundly"
description: "One facade for every write and read — the draft() builder, lifecycle verbs, SEO, tags, publishDue() and slug lookups on the configured model."
url: https://roundly-consulting.com/open-source/docs/posts-for-laravel/facade
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Posts for Laravel](https://roundly-consulting.com/open-source/docs/posts-for-laravel.md)

# The Posts facade

Everything a host does with posts goes through one facade, RoundlyConsulting\\Posts\\Facades\\Posts, also aliased as Posts:

```php
use RoundlyConsulting\Posts\DataTransferObjects\SeoData;
use RoundlyConsulting\Posts\Facades\Posts;

// Write a post with the builder, then save it as a draft, publish it or schedule it
$post = Posts::draft()
    ->title('en', 'Hello world')->title('sk', 'Ahoj svet')
    ->perex('en', 'A short intro')
    ->content('en', '<p>…</p>')
    ->by($user)                                   // any author model, bigint/uuid/ulid keyed
    ->tags(['laravel', 'php'])                    // Tag models, or names found/created in the current locale
    ->seo(new SeoData(canonical: 'https://example.test/hello-world'))
    ->publish();                                  // or ->save() (draft), ->publish($at), ->schedule($at)

// Or from a DTO
Posts::create($createPostData);

// Lifecycle
Posts::publish($post);                            // now, or Posts::publish($post, $at)
Posts::schedule($post, now()->addDay());
Posts::archive($post);
Posts::unpublish($post);                          // back to draft
Posts::publishDue();                              // publish every scheduled post whose time has come → int

// SEO and tags
Posts::seo($post, new SeoData(metaTitle: 'Custom title', robots: 'index,follow'));
Posts::syncTags($post, ['eloquent', $tag]);

// Reads — always the configured posts.model
Posts::findBySlug('hello-world');                 // current locale → fallback → any locale
Posts::findBySlug('ahoj-svet', 'sk');             // exactly one locale — needs sk in sluggable.locales.supported
Posts::published()->latest('published_at')->paginate();
Posts::query()->inCategory('laravel')->get();
```

| Posts::… | Returns | What it does |
| --- | --- | --- |
| `create(CreatePostData $data)` | `Post` | Create a post from a DTO. |
| `draft()` | `PendingPost` | The fluent builder — below. |
| `publish(Post $post, ?CarbonInterface $at = null)` | `Post` | Publish now or at $at; fires PostPublished. |
| `schedule(Post $post, CarbonInterface $at)` | `Post` | Schedule; fires PostScheduled. |
| `archive(Post $post)` | `Post` | Archive, keeping the publish date; fires PostArchived. |
| `unpublish(Post $post)` | `Post` | Back to draft, clearing the publish date; fires PostDrafted. |
| `seo(Post $post, SeoData $data)` | `Post` | Store the SEO fields and save. |
| `syncTags(Post $post, iterable $tags)` | `Post` | Sync tags — models or current-locale names. |
| `publishDue()` | `int` | Publish every due scheduled post (what posts:publish-scheduled runs). |
| `findBySlug(string $slug, ?string $locale = null)` | `?Post` | Find by slug through the posts.model seam. |
| `published()` | `Builder<Post>` | Published posts whose date has passed. |
| `query()` | `Builder<Post>` | A query on the configured posts.model. |
| `fake()` | `PostsFake` | Swap in the recording fake — see Testing. |

Reads always go through the configured posts.model, so a subclass you configure is what findBySlug(), published() and query() return.

## The draft() builder

Posts::draft() returns a PendingPost. Set per-locale fields, the author, tags and SEO, then finish with a terminal:

```php
$post = Posts::draft()
    ->title('en', 'Hello world')          // per-locale fields, any number of locales
    ->slug('sk', 'Vlastny Slug')          // manual slug: normalised + made unique
    ->perex('en', 'Intro')
    ->content('en', '<p>…</p>')           // stored and rendered as is — sanitise untrusted input first
    ->metaTitle('en', 'Meta title')
    ->metaDescription('en', 'Meta description')
    ->by($author)                         // any Model; its morph class + key
    ->tags(['php', $tag])                 // synced after create
    ->seo(new SeoData(canonical: '…'))
    ->save();                             // Draft

$data = Posts::draft()->title('en', 'Queued')->data();   // the CreatePostData it would create
```

| Terminal | Does |
| --- | --- |
| `save(): Post` | create(data()) as a Draft, then syncTags() when tags were given. |
| `publish(?CarbonInterface $at = null): Post` | save(), then publish($post, $at). |
| `schedule(CarbonInterface $at): Post` | save(), then schedule($post, $at). |
| `data(): CreatePostData` | The DTO it would create — for a queued job or create(). |

Each step goes through the manager, so the fake records create, syncTags and publish or schedule separately. An unsaved author model contributes no author.

## Model verbs

The model keeps its convenience methods, and each goes through the same manager — behaviour, events and the fake are identical whichever form you use. A posts.model subclass can override them to hook in:

```php
$post->publish();                  // == Posts::publish($post)
$post->schedule(now()->addDay());  // == Posts::schedule($post, $at)
$post->archive();                  // == Posts::archive($post)
$post->unpublish();                // == Posts::unpublish($post)
$post->syncTags(['php']);          // == Posts::syncTags($post, [...])
```

There are no sub-accessors or scoped handles: a post has no parent scope.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/posts-for-laravel.md)
