Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Story Laravel Package

orchestra/story

Orchestra Story is a Laravel CMS package for Orchestra Platform that adds a simple content publishing layer for stories, pages, and posts. It provides Eloquent-backed models, migrations, and admin-friendly workflows for managing structured content.

View on GitHub
Deep Wiki
Context7

Getting Started

Confirm the platform before the package. Story is not a standalone Laravel CMS — it is a first-party extension of Orchestra Platform v3.2, and it inherits Orchestra's service-provider lifecycle, ACL/role layer, routing, and admin scaffolding. If orchestra/platform (or orchestra/foundation) is not already installed and pinned to a 3.2-compatible line, stop and settle that decision first. Retrofitting Story onto plain Laravel costs more than writing a small posts/pages module yourself.

composer require orchestra/story
php artisan route:list --path=story
php artisan migrate --pretend

The provider is normally picked up by Orchestra's extension discovery. If migrations or views are not loading, look at StoryServiceProvider first — it extends Orchestra\Foundation\Support\Providers\ModuleServiceProvider, so registration flows through $config, $views, $migrations, and the module route conventions rather than raw loadMigrationsFrom/loadViewsFrom calls. A provider that "registers fine" but silently skips migrations is almost always a mismatch between the module convention and a hand-rolled override.

Read these five things, in this order:

  1. Orchestra\Story\Model\Content — the single model behind both posts and pages. Everything hangs off it.
  2. Orchestra\Story\Policies\ContentPolicy and Orchestra\Story\AuthServiceProvider — the authorization surface, and the highest-risk area in the package (see Gotchas).
  3. php artisan route:list --path=story -v — the admin CRUD routes and their middleware.
  4. Orchestra\Story\StoryPlugin — the sanctioned seam for menus, assets, and extra admin screens.
  5. Publishable config/views — copy them into your app only when you actually need an override.

First-day smoke test. Log into the Orchestra admin, create one post and one page through the CMS UI, then render them from your own front-end route:

use Orchestra\Story\Model\Content;

Route::get('/blog/{content}', function (Content $content) {
    abort_unless($content->status === 'published', 404);

    return view('blog.show', compact('content'));
});

Bodies are stored as Markdown. Render through Orchestra's formatter rather than echoing raw content:

{!! Orchestra\Format\Facades\Format::make($content->body) !!}

If Format is not present in your install, fall back to league/commonmark — but pick one and stay with it. Inline-HTML handling differs between the two, so mixing them in production produces inconsistent output across pages.

Do not build your front-end inside Story. Story owns the admin CRUD surface; presentation is yours. Treat it as a headless content source — query Content from your own controllers — and you stay insulated when the admin UI changes.

Implementation Patterns

Use the instance factories, not new Content. v3.2 added static constructors that carry per-type defaults (status, template, type-specific behaviour):

$post = Content::newPostInstance();
$page = Content::newPageInstance(['title' => 'About', 'slug' => 'about']);

Prefer the URL helpers over hand-built links. url(), editUrl(), and deleteUrl() keep links correct when Story changes its routing or slug strategy:

<a href="{{ $content->url() }}">{{ $content->title }}</a>
@can('update', $content)
    <a href="{{ $content->editUrl() }}">Edit</a>
@endcan

Put authorization in the controller, not the route. With CanManage removed, every content action is a policy check:

public function edit(Content $content)
{
    $this->authorize('update', $content);

    return view('content.edit', compact('content'));
}

The route-level equivalent (->middleware('can:update,content')) works, but the controller form is easier to test and harder to break by reordering middleware.

Scope queries explicitly. Don't lean on implicit global scopes for publication state; be explicit so admin views and tests can bypass the filter:

$published = Content::where('status', 'published')
    ->orderByDesc('published_at')
    ->paginate(15);

If you need a reusable published() scope, wrap it in your own service/query class rather than forking the model — you want to stay mergeable against upstream Content.

Extend through StoryPlugin, not vendor edits. Menus, nav items, and additional admin screens belong in the plugin. Anything outside that seam is a merge conflict on the next Orchestra bump.

Wire polymorphic relations through a morph map. Content::$morphClass is gone in v3.2, so define the alias explicitly in AppServiceProvider:

Relation::morphMap([
    'story_content' => \Orchestra\Story\Model\Content::class,
]);

Then use getMorphClass() anywhere another module needs to attach or link content.

Test the model + policy layer, not the admin HTTP surface. Story's admin routes are slow and heavy to exercise; the authorization logic is where the regressions live:

public function test_editor_cannot_delete_others_content(): void
{
    $editor  = UserFactory::new()->create();
    $content = Content::newPostInstance();
    $content->user_id = UserFactory::new()->create()->id;
    $content->save();

    $this->assertTrue($editor->cannot('delete', $content));
}

Cover view, create, update, and delete with a positive and a negative case per role. That suite is your regression gate for the authorization migration.

Cache rendered bodies if traffic warrants it. Key the cache off updated_at. The CMS itself lives admin-side and never touches the hot path, so caching the rendered front-end output is enough.

Gotchas and Tips

The authorization migration is the real upgrade risk. CanManage was removed in favour of AuthServiceProvider + ContentPolicy. Grep for CanManage — any surviving reference is a hard runtime failure on that route. The subtler failure runs the other way: a misregistered policy either 403s legitimate editors or, if never discovered at all, falls open to unrelated authenticated users. Stage the upgrade and exercise every content route under every role before rollout.

Content::$morphClass removal breaks custom polymorphic relations. If any relationship assumed the old property, switch to getMorphClass(), register an explicit Relation::morphMap(), and add a regression test on each relationship that traverses content.

Acl aliases are deprecated. Replace imports with Orchestra\Support\Facades\ACL. The old alias may still resolve today; do it now while the change is purely mechanical.

Provider base-class change. If you subclass StoryServiceProvider, or override its boot/config behaviour, verify against ModuleServiceProvider. Your own app providers should rebase too — mixing provider styles produces confusing double-registration of routes and views.

Re-publish SimpleMDE assets. v3.2.0 ships SimpleMDE v1.9.1. If you published assets from an earlier install, the editor keeps serving the old build until you re-publish and clear compiled caches. Symptom: toolbar renders but behaviour is inconsistent, or scroll-sync/autosave misbehaves.

Slug collisions on edit. Slugs are generated from titles, so renaming a published post without pinning the slug silently breaks live URLs. Pin the slug on publish, keep the slug field explicit in any custom admin view, and assert url() after a rename in tests — a broken content URL is the most user-visible failure after an authorization regression.

Debugging a 403 on a content route:

php artisan route:list --path=story -v   # confirm middleware
php artisan about                        # confirm provider registered

Then check, in order: is AuthServiceProvider registered? Is the policy auto-discovered, or does it need Gate::policy(Content::class, ContentPolicy::class)? Does the user's Orchestra role carry the matching ACL action? The failure is almost always in one of those three — and the ACL/role layer is the one people forget.

Treat the public API as unsettled. v3.2.0 removes two public surfaces and deprecates a third, with zero dependents to catch the next break. Concretely: don't extend Content, don't touch its internals, and don't couple Blade deeply to more than title, body, slug, status, and the URL helpers. If you expect to migrate off later, put an interface or DTO between Story and your views.

You are the maintainer. No dependents means no Stack Overflow answers and no third-party extensions. Budget Story maintenance as a recurring line item tied to every Orchestra release, and name an internal owner before adoption rather than after the first incident.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings