Out of 300 global security teams, 297 see their growth stalled by security threats. See how the winning 3 break through barriers with the State of Security 2026.

Storyblok PHP libraries kick off 2026: CDN proxy, image service, and less boilerplate

Developers
Edoardo Dusi

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:

  1. In your Twig template, cdn_url(asset) generates a local URL (no download happens yet)
  2. When the browser requests that URL, the bundle checks if the file is cached
  3. First request: download from Storyblok, store locally, serve to the browser
  4. Subsequent requests: serve directly from local cache

Quick setup

Enable the CDN route:

config/packages/storyblok.yaml
storyblok:
    cdn:
        storage:
            path: '%kernel.project_dir%/var/cdn'
        cache:
            public: true
            max_age: 31536000  # 1 year
config/routes/storyblok.yaml
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.

Note:

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
src/Controller/ImageController.php
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(w, h)

Resize with optional aspect ratio preservation

fitIn(w, h)

Fit within bounds

crop(x1, y1, x2, y2)

Crop to coordinates

format(fmt)

Convert to webp, jpeg, png, or avif

quality(q)

Set quality (0-100)

blur(r, s)

Apply a blur effect

brightness(b)

Adjust brightness (-100 to 100)

rotate(deg)

Rotate 90, 180, or 270 degrees

flipX() / flipY()

Flip horizontally or vertically

grayscale()

Convert to grayscale

focalPoint(coords)

Set focal point for smart cropping

roundedCorners(r)

Apply rounded corners

fill(color)

Set a fill color for fitIn operations

noUpscale()

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:

src/Block/EmbedBlock.php
#[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:

src/Controller/LegalController.php
#[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:

src/Controller/PageController.php
#[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_slug on Link objects: the new property lets you access the full path to improve routing
  • \Stringable on the Editable class: 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!