sensiolabs-de/storyblok-bundle
Symfony bundle for integrating the Storyblok headless CMS into your application, with configuration and services to fetch and render Storyblok content in Symfony projects.
This package is a Symfony bundle, not a Laravel package — there is no service provider, no config/storyblok.php to publish, and no package auto-discovery. For a Laravel developer the practical value is threefold: it wraps storyblok/php-client with sane DI wiring, it shows you the reference implementation of Storyblok's published-vs-draft switching, and it ships a webhook handler abstraction worth copying.
Minimal path with a Laravel app:
composer require storyblok/php-client (the underlying client this bundle wraps, installable standalone).DependencyInjection/Configuration.php first — it is the single source of truth for every knob you'll want to mirror in config/storyblok.php: API key, preview token, version (draft/published), base URI, HTTP client, cache adapter, webhook secret, and webhook controller path.AppServiceProvider with the token selected by environment (preview token locally/staging, public token in production).$this->app->singleton(StoryblokClient::class, fn () => new StoryblokClient(
apiKey: config('storyblok.token'), // preview token outside prod
version: config('storyblok.version'), // draft | published
client: new \Storyblok\Client\Http\SymfonyHttpClient(), // optional
));
Look at the bundle's webhook controller and WebhookHandlerInterface second — that is the part most teams re-implement badly, and the pattern (one handler per event/content type, dispatched by a resolver) transfers directly to Laravel.
Token and version switching by environment. Two separate tokens exist and they are not interchangeable: the public token only ever returns published content, the preview token returns drafts but leaks unpublished CMS data if it reaches a browser. Drive version off the request (a signed preview flag in session) and the token off config, never the other way round. A request-scoped binding is safer than a singleton if you support preview mode.
Repository layer over the client. Wrap fetches in a StoryblokRepository with story(string $slug): ?array and stories(array $params), so controllers and Blade never touch the client directly. This is also where you normalise Storyblok's shape into something your domain understands.
Cache with version-aware keys. Cache on full_slug plus the version, e.g. storyblok:v1:{version}:{slug} — a shared key means a preview render silently poisons production for everyone until TTL expiry. Use Cache::remember with a generous TTL and rely on webhooks, not TTL, for freshness.
Webhook endpoint that mirrors the bundle's handler contract. Register POST /webhooks/storyblok in routes/api.php (exclude it from CSRF), verify the signature, then resolve a handler:
interface WebhookHandlerInterface {
public function supports(string $action, string $storyType): bool;
public function handle(array $payload): void;
}
Resolve handlers from a tagged collection (bind WebhookHandlerInterface as an array of implementations, or a HandlerRegistry map), then dispatch real work onto a queue: ClearStoryCache for published/unpublished/deleted, RebuildStaticSite for full publishes. Keep the controller under ~100ms or Storyblok will retry.
Blok rendering via Blade partials. Storyblok's body is an array of bloks with a component string. Map it in a single recursive component: @includeIf('bloks.'.$blok['component'], ['blok' => $blok]) with a bloks.fallback for unknown components — this beats a match statement that silently renders nothing when editors add a component.
Preview mode. A route toggling a session flag keyed on a signed URL parameter, a middleware that sets version=draft from that flag, and a visible banner so preview content is never mistaken for production output.
0.4.1 in context. This release is almost entirely Dependabot codecov/codecov-action bumps; the only functional change is "Fixes auto configuration for WebhookHandlerInterface". That auto-configuration is Symfony DI tagging — if you ported the pattern to Laravel, verify your handler registry explicitly, because the equivalent failure mode (interface registered but handlers never collected) is silent: webhooks arrive, nothing happens, no error. No breaking changes, no new features, no deprecations. Upgrading is safe but changes nothing for a Laravel consumer.
You will not get WebhookHandlerInterface auto-wiring in Laravel. The bundle relies on registerForAutoconfiguration(); Laravel has no equivalent. Either tag your handlers manually in a service provider or use a HandlerRegistry with an explicit array — do not assume the container will find them.
Verify the signature against the raw body. Laravel's $request->all() is the parsed payload; signature verification (HMAC of the body with your webhook secret, per current Storyblok docs) must use $request->getContent(). Any middleware that re-encodes the JSON first will invalidate the check.
A missing story is a 200, not a 404. The API often returns a successful response with a null/empty story. Always null-check $response->getStory() — do not assume a thrown exception on unknown slugs.
Cache-busting races. The webhook can fire a second or two before the CDN serves the new content; a handler that flushes cache and immediately re-fetches may re-cache the old story. ->delay(now()->addSeconds(5)) on the queued job is the cheap fix.
Rate limits and transient 5xx. Wrap client calls in retry(3, 200, ...) (or your own backoff) and set an explicit timeout — the default PSR-18 timeout is unbounded and a slow Storyblok region will hang your workers.
Preview tokens and browser exposure. If you use the live-preview JS bridge (_storyblok param), keep the preview token server-side and proxy through your own route; putting it in the page source is a data leak, not a convenience.
Extension points worth copying, not installing. The pluggable PSR-18 HTTP client and PSR-16 cache adapter in the bundle's config are the right seams: swap in Illuminate\Http\Client or your own cache repository rather than enabling the client's internal cache alongside Laravel's, or you'll have two cache layers with independent invalidation and no way to debug which one served stale content.
How can I help you explore Laravel packages today?