Storyblok PHP libraries kick off 2026: CDN proxy, image service, and less boilerplate
Storyblok is the first headless CMS that works for developers & marketers alike.
Storyblok's PHP ecosystem has had a busy start to 2026. Across three packages — the Symfony Bundle, the brand new PHP Image Service, and the PHP Content API Client — we've shipped features that give you more control over your assets, cleaner APIs for image transformations, and less boilerplate in your controllers.
These updates come largely from Silas Joisten, a Storyblok MVP and developer at our partner SensioLabs (the creators of Symfony). It's a great example of what happens when community contributors and partners push the ecosystem forward.
This post highlights the main features of the latest updates:
- Serve Storyblok assets via your own domain with custom caching
- Try the new fluent image transformations API
- Reduce boilerplate in your Symfony code
Let's dive in.
Asset CDN Controller
When you use Storyblok assets directly, you're relying on Storyblok's CDN. That works fine in most cases, but it means you have no control over HTTP cache headers, and downloading images during Twig rendering might block page generation, which is especially problematic when you have many assets on a page.
The new Asset CDN Controller in Symfony Bundle 1.15 solves this by letting you serve assets through your own domain. Think of it as a local proxy: lazy-load assets only when the browser requests them and set your custom cache headers.
Here's the flow:
- In your Twig template,
cdn_url(asset)generates a local URL (no download happens yet) - When the browser requests that URL, the bundle checks if the file is cached
- First request: download from Storyblok, store locally, serve to the browser
- Subsequent requests: serve directly from local cache
Quick setup
Enable the CDN route:
storyblok:
cdn:
storage:
path: '%kernel.project_dir%/var/cdn'
cache:
public: true
max_age: 31536000 # 1 year storyblok_cdn:
resource: '@StoryblokBundle/config/routes/cdn.php' Then, in your templates:
{# Generate a CDN URL for any asset #}
<img src="{{ cdn_url(asset) }}" alt="My image"> The bundle also includes a cleanup command (php bin/console storyblok:cdn:cleanup) with --dry-run and --expired options, plus a Symfony Profiler integration that tracks which assets are cached vs pending.
For advanced use cases, like S3 storage, check the CDN Asset Handling section of the README.
PHP Image Service
Alongside the CDN features, we've released a new standalone package: storyblok/php-image-service. It provides a fluent, immutable API for building Storyblok image transformation URLs.
This package is currently experimental. The API may change until we ship a stable 1.0 release. Use it, but be prepared for potential breaking changes.
The design prioritizes developer experience:
- Fluent interface: chain as many operations as you need
- Immutable: keep the original image unchanged, with a new instance returned by each method
- Automatic focal point: automatically apply the focal point defined in Storyblok's UI
use Storyblok\ImageService\Image;
$image = new Image('<https://a.storyblok.com/f/287488/1400x900/2fc896c892/image.jpg>');
$url = $image
->resize(800, 600)
->format('webp')
->quality(80)
->toString(); Available operations:
Operation | Description |
|---|---|
| Resize with optional aspect ratio preservation |
| Fit within bounds |
| Crop to coordinates |
| Convert to |
| Set quality (0-100) |
| Apply a blur effect |
| Adjust brightness (-100 to 100) |
| Rotate 90, 180, or 270 degrees |
| Flip horizontally or vertically |
| Convert to grayscale |
| Set focal point for smart cropping |
| Apply rounded corners |
| Set a fill color for |
| Avoid increasing the image dimensions |
You can also access metadata: $image->getWidth(), $image->getHeight(), $image->getName(), $image->getExtension().
Check the storyblok/php-image-service documentation on GitHub.
The storyblok_image Twig filter
The Symfony Bundle includes a storyblok_image Twig filter that bridges Storyblok assets and the Image Service. This is where the two features come together.
{# Basic conversion to Image #}
{% set image = asset|storyblok_image %}
<img src="{{ image }}" alt="My image">
{# Resize #}
{% set image = asset|storyblok_image(800, 600) %}
{# Combine with CDN #}
<img src="{{ cdn_url(asset|storyblok_image(400, 300)) }}" alt="Thumbnail">
{# Chain transformations #}
{% set image = asset|storyblok_image(800, 600).format('webp').quality(80) %}
<img src="{{ cdn_url(image) }}" alt="Optimized image"> DX improvements: repeatable attributes
Beyond assets and images, the Symfony Bundle 1.15 includes several developer experience improvements that reduce boilerplate.
Repeatable #[AsBlock]
A single PHP class can now handle multiple Storyblok block names. Useful when you have components that share the same structure:
#[AsBlock(name: 'youtube_embed')]
#[AsBlock(name: 'vimeo_embed')]
#[AsBlock(name: 'twitter_embed')]
final readonly class EmbedBlock
{
public string $url;
public ?string $caption;
public function __construct(array $values)
{
$this->url = $values['url'] ?? '';
$this->caption = $values['caption'] ?? null;
}
} Repeatable #[AsContentTypeController]
Similarly, one controller can handle multiple content types or specific slugs:
#[AsContentTypeController(contentType: LegalPage::class, slug: '/legal/imprint')]
#[AsContentTypeController(contentType: LegalPage::class, slug: '/legal/privacy-policy')]
#[AsContentTypeController(contentType: LegalPage::class, slug: '/legal/terms')]
final readonly class LegalController
{
public function __invoke(Request $request, LegalPage $page): Response
{
// Handle all legal pages with shared logic
}
} Relation resolution in attributes
You can now configure resolveRelations and resolveLinks directly in the #[AsContentTypeController] attribute, rather than making manual API calls:
#[AsContentTypeController(
contentType: Page::class,
resolveRelations: new RelationCollection([
new Relation('page.featured_articles'),
new Relation('page.author'),
]),
resolveLinks: new ResolveLinks(ResolveLinksType::Url),
)]
final readonly class PageController
{
public function __invoke(Request $request, Page $page): Response
{
// Relations and links are already resolved in $page
}
} PHP Content API Client updates
The Content API Client (v1.15.x) also received some updates:
full_slugonLinkobjects: the new property lets you access the full path to improve routing\Stringableon theEditableclass: the class now implements the interface explicitly, so you can cast directly to string in templates- Symfony 8 support: the bundle supports the latest version of Symfony and PHP 8.4
Get started
New to using Storyblok with Symfony? Our Symfony technology guide walks you through the fundamentals of integrating Storyblok with your app— from installation to rendering your first components. Start there, then come back for these advanced features.
Update your existing packages:
composer update storyblok/symfony-bundle storyblok/php-content-api-client Or try the new experimental Image Service:
composer require storyblok/php-image-service Links to the repositories:
Wrap-up
This batch of updates gives you more control over how you serve assets, a clean API for image transformations, and less boilerplate in your Symfony code. The CDN Controller alone solves a real pain point for teams that need custom caching strategies.
A big thanks to Silas Joisten and the team at SensioLabs for driving these improvements. The PHP ecosystem for Storyblok keeps getting better, and it's built by the community!