Storyblok integration for Laravel. It provides bridge, visual editor, image service, richtext rendering, and component dispatcher.
This is an opinionated Laravel package where i collected some helpers that typically I use in Laravel application that integrates Storyblok Content Delivery API.
This Laravel package leverages the storyblok/php-content-api-client, a type-safe PHP SDK for Storyblok.
composer require hi-folks/storyblok-laravel-helpersThe package auto-registers its service provider and facade via Laravel's package discovery.
Run the install command to scaffold your project:
php artisan storyblok:installThis will:
- Publish
config/storyblok.php - Create the Blade components directory (
resources/views/components/storyblok/) - Create the Storyblok definitions directory (
storyblok/) - Publish the
StoryControllerandstory.blade.phpstubs - Append Storyblok environment variables to
.envand.env.example
Then set your access token in .env:
STORYBLOK_ACCESS_TOKEN=your-access-tokenHere is how to go from a new Laravel app to rendering your first Storyblok page.
laravel new my-app
cd my-app
composer require hi-folks/storyblok-laravel-helpersphp artisan storyblok:installGrab the Preview access token from your Storyblok space settings and add it to .env:
STORYBLOK_ACCESS_TOKEN=your-preview-tokenSuppose you have a page component in Storyblok with a body field. Create the matching Blade component:
{{-- resources/views/components/storyblok/page.blade.php --}}
@props(['blok'])
<main @storyblokEditable($blok)>
@foreach ($blok->get('body', []) as $nestedBlok)
<x-storyblok::component :blok="$nestedBlok" />
@endforeach
</main>The storyblok:install command already created the resources/views/components/storyblok/ directory for you.
Note: The
$blokvariable passed to your Blade components is aBlokinstance (HiFolks\StoryblokLaravelHelpers\Blok). Use$blok->get('field')instead of$blok['field']to access fields — it supports dot notation for nested access (e.g.$blok->get('content.nested.value', 'default')).
Open routes/web.php and add:
use App\Http\Controllers\StoryController;
Route::get('/{slug?}', [StoryController::class, 'show'])
->where('slug', '.*')
->name('story.show');The StoryController was published by the install command at app/Http/Controllers/StoryController.php. It fetches a story by slug via the Content Delivery API and passes it to the story.blade.php view, which uses the <x-storyblok::component> dispatcher to render bloks.
php artisan serveOpen http://localhost:8000 — Laravel will fetch the home story from Storyblok and render it through your Blade components.
From here you can create more Blade components (one per Storyblok blok) inside resources/views/components/storyblok/.
Publish the config file manually if needed:
php artisan vendor:publish --tag=storyblok-configAvailable options in config/storyblok.php:
| Key | Description | Default |
|---|---|---|
access_token |
Storyblok Content Delivery API token | env() |
version |
Content version (published or draft) |
published |
hostname |
API base URL (see Regions below) | EU endpoint |
mapi_token |
Management API token (for CLI commands) | env() |
space_id |
Storyblok space ID (for CLI commands) | env() |
component_namespace |
Subdirectory for Storyblok Blade components | storyblok |
preview_path |
POST endpoint for bridge real-time preview | /api/preview |
preview_view |
Blade view rendered for preview requests | story |
| Area | Files | Description |
|---|---|---|
| Config | config/storyblok.php |
Access token, content version, Management API credentials, component namespace, preview settings |
| Service Provider | src/StoryblokLaravelHelpersServiceProvider.php |
Registers config, views, routes, Blade directives, Artisan commands, and publishable assets |
| Blok | src/Blok.php |
Wraps blok arrays with get() (dot-notation), has(), and toArray() |
| Story | src/Story.php |
Extends Blok with convenience methods for story data (content(), name(), slug(), fullSlug()) |
| Facade | src/Facades/Storyblok.php |
Facade for StoryblokManager (content delivery) |
| Manager | src/StoryblokManager.php |
Factory for StoriesApi instances with region and config support |
| Controller | src/Http/Controllers/PreviewController.php |
Handles real-time preview requests from the Storyblok Bridge |
| Middleware | src/Http/Middleware/SetStoryblokDraftVersion.php |
Auto-switches content version to draft when inside the Visual Editor |
| Services | src/Services/StoryblokEditable.php |
Generates data-blok-c and data-blok-uid attributes for the Visual Editor |
src/Services/StoryblokImageUrl.php |
Builds Storyblok Image Service URLs with transforms, srcset generation | |
src/Services/StoryblokRichtext.php |
Renders Storyblok richtext fields to HTML via Tiptap | |
src/Services/StoryblokRichtextMarkdown.php |
Renders Storyblok richtext fields to Markdown | |
src/Services/Markdown/MarkdownRenderer.php |
Recursive Tiptap JSON-to-Markdown renderer | |
src/Services/RichtextImageExtension.php |
Custom Tiptap node for richtext images with <figure>, copyright, and alt |
|
| Commands | storyblok:install |
Scaffolds the project (config, directories, stubs, env variables) |
storyblok:import-component |
Imports a component definition from a JSON file via the Management API | |
| Blade views | resources/views/components/bridge.blade.php |
Storyblok Bridge script with Idiomorph for real-time preview |
resources/views/components/component.blade.php |
Component dispatcher — resolves blok names to user Blade components | |
resources/views/components/image.blade.php |
Responsive image component with srcset and Image Service transforms | |
resources/views/components/richtext.blade.php |
Renders a richtext field | |
| Stubs | stubs/StoryController.php.stub |
Example controller for fetching and displaying a story |
stubs/story.blade.php.stub |
Example Blade view for rendering a story |
The Story class extends Blok and wraps a raw story array from the Storyblok API. It provides convenience methods for common story fields:
use HiFolks\StoryblokLaravelHelpers\Story;
$story = Story::make($response->story);
$story->name(); // e.g. "My Page"
$story->slug(); // e.g. "my-page"
$story->fullSlug(); // e.g. "en/my-page"
$story->content(); // returns a Blok instance wrapping the content arrayIn your story.blade.php view:
<x-storyblok-layout :title="$story->name()">
<x-storyblok::component :blok="$story->content()->toArray()" />
</x-storyblok-layout>Since Story extends Blok, you can also use get(), has(), and toArray() for any story field:
$story->get('published_at');
$story->get('content.component');
$story->has('tag_list');The component dispatcher maps a Storyblok blok to a Blade component in your application. Pass any blok array and it resolves the matching Blade view under resources/views/components/storyblok/:
<x-storyblok::component :blok="$story->content()->toArray()" />For example, a blok with "component": "hero" renders resources/views/components/storyblok/hero.blade.php. Underscores in component names are converted to hyphens (image_text_section → image-text-section).
If no matching Blade component is found and app.debug is true, a placeholder with the missing component name is shown — it is also editable in the Visual Editor so you can still click on it to see which blok it refers to.
Use it recursively inside your own Blade components to render nested bloks:
{{-- resources/views/components/storyblok/page.blade.php --}}
@props(['blok'])
<main @storyblokEditable($blok)>
@foreach ($blok->get('body', []) as $nestedBlok)
<x-storyblok::component :blok="$nestedBlok" />
@endforeach
</main>Renders a responsive <img> tag with srcset and Storyblok Image Service transforms. Pass the Storyblok image asset array and the component automatically picks up filename, alt, title, and focus:
{{-- Basic usage --}}
<x-storyblok::image :image="$blok->get('image')" />
{{-- Custom widths for srcset breakpoints --}}
<x-storyblok::image :image="$blok->get('image')" :widths="[400, 800, 1200]" />
{{-- With aspect ratio (width/height) --}}
<x-storyblok::image :image="$blok->get('image')" :ratio="16/9" />
{{-- With smart cropping --}}
<x-storyblok::image :image="$blok->get('image')" :smart="true" />
{{-- Priority image (above the fold) --}}
<x-storyblok::image :image="$blok->get('image')" loading="eager" fetchpriority="high" />Available props:
| Prop | Type | Default | Description |
|---|---|---|---|
image |
array |
required | Storyblok image asset (filename, alt, title, focus) |
sizes |
string |
100vw |
The sizes attribute for responsive images |
widths |
array |
[400, 600, 800, 1200, 1600, 2000] |
Widths used to generate the srcset |
ratio |
float|null |
null |
Aspect ratio (width/height), null = auto height |
class |
string |
'' |
CSS class(es) for the <img> tag |
loading |
string |
lazy |
Loading strategy (lazy or eager) |
fetchpriority |
string|null |
null |
Fetch priority (high, low, auto) |
quality |
int |
80 |
JPEG quality (0–100) |
smart |
bool |
false |
Enable smart cropping (face detection) |
The src attribute uses the third width in the widths array (default 800) as the fallback image.
Renders a Storyblok richtext field to HTML. Nested bloks inside the richtext are resolved through the component dispatcher, so your Blade components are reused automatically:
<x-storyblok::richtext :content="$blok->get('body')" />Images inside richtext are rendered as <figure> elements with <figcaption> (from alt) and <footer><small> (from copyright).
Use StoryblokRichtextMarkdown::render() to convert a Storyblok richtext field to Markdown instead of HTML. It accepts the same Tiptap JSON structure:
use HiFolks\StoryblokLaravelHelpers\Services\StoryblokRichtextMarkdown;
$markdown = StoryblokRichtextMarkdown::render($blok->get('body'));Supported elements:
| Element | Markdown output |
|---|---|
| Paragraph | text\n\n |
| Heading (level N) | # text\n\n (# repeated per level) |
| Bold | **text** |
| Italic | *text* |
| Strikethrough | ~~text~~ |
| Inline code | `text` |
| Link | [text](url) |
| Bullet list | - item\n |
| Ordered list | 1. item\n |
| Blockquote | > text\n\n |
| Code block | ```lang\ncode\n``` |
| Horizontal rule | ---\n\n |
| Image |  |
Adds the Storyblok Bridge script for real-time visual editing. The script is only rendered when the request comes from the Visual Editor (detected via _storyblok query parameter). Place it before </body> in your layout:
<x-storyblok::bridge />The bridge uses Idiomorph to morph the <main> element without a full page reload when content changes in the editor.
Use the @storyblokEditable Blade directive to make components clickable/editable in the Visual Editor:
<div @storyblokEditable($blok)>
{{-- component content --}}
</div>This outputs data-blok-c and data-blok-uid attributes parsed from the _editable field that Storyblok injects when using draft content.
Use the @storyblokImageUrl directive to build Storyblok Image Service URLs directly in Blade — useful for CSS background images or any place where you need the URL without the <img> tag. Pass the whole image array and the directive automatically picks up filename and focus:
{{-- Background image --}}
<div style="background-image: url('@storyblokImageUrl($blok->get('image'), width: 1200)')">
{{-- With height and quality --}}
<div style="background-image: url('@storyblokImageUrl($blok->get('image'), width: 800, height: 600, quality: 90)')">
{{-- With smart cropping --}}
<div style="background-image: url('@storyblokImageUrl($blok->get('image'), width: 800, height: 400, smart: true)')">Apply the middleware to your story routes so the Visual Editor always sees draft content:
use HiFolks\StoryblokLaravelHelpers\Http\Middleware\SetStoryblokDraftVersion;
Route::get('/{slug?}', [StoryController::class, 'show'])
->where('slug', '.*')
->middleware(SetStoryblokDraftVersion::class);The Storyblok facade provides a convenient way to build StoriesApi instances and fetch stories:
use HiFolks\StoryblokLaravelHelpers\Facades\Storyblok;
// Build a StoriesApi with config defaults
$stories = Storyblok::makeStoriesApi();
$response = $stories->bySlug('home');
// Convenience methods (use config defaults)
$response = Storyblok::storyBySlug('home');
$response = Storyblok::storyById(new \Storyblok\Api\Domain\Value\Id(123));
// Override token or version
$stories = Storyblok::makeStoriesApi(token: 'other-token', version: 'draft');
// Use a specific region (see Regions below)
$stories = Storyblok::region('us')->makeStoriesApi();
// Region + overrides
$stories = Storyblok::region('us')->makeStoriesApi(token: 'us-token');
// Fully custom hostname (e.g. self-hosted)
$stories = Storyblok::makeStoriesApi(hostname: 'https://custom.endpoint.com/v2/cdn/');Storyblok spaces can live in different regions. You can set the region globally via config or per-request via the facade.
Global (config/env):
Set STORYBLOK_HOSTNAME in your .env:
STORYBLOK_HOSTNAME=https://api-us.storyblok.com/v2/cdn/Per-request (facade):
$stories = Storyblok::region('us')->makeStoriesApi();Available region codes:
| Code | Hostname |
|---|---|
eu |
https://api.storyblok.com/v2/cdn/ (default) |
us |
https://api-us.storyblok.com/v2/cdn/ |
ca |
https://api-ca.storyblok.com/v2/cdn/ |
ap |
https://api-ap.storyblok.com/v2/cdn/ |
cn |
https://app.storyblokchina.cn/v2/cdn/ |
Priority order: explicit hostname parameter > region() > config storyblok.hostname > EU default.
composer testOr directly:
vendor/bin/phpunitMIT