Automatically transform WordPress media URLs into dynamic, optimized URLs powered by image transformation services like Cloudinary.
- PHP 8.3+
- WordPress 6.2+
- A Cloudinary account with auto-upload mapping configured
composer require aysnc/wordpress-dynamic-mediaThe plugin auto-activates through Composer's wordpress-plugin type. If your setup doesn't support that, activate it
manually in wp-admin.
When you upload an image to WordPress, it gets stored at a URL like this:
https://example.com/wp-content/uploads/2024/03/hero-image.jpg
With this plugin active, that same image is served through Cloudinary with on-the-fly transformations:
https://res.cloudinary.com/your-cloud/images/w_800,h_600,c_fill/my-site/2024/03/hero-image/hero-image.jpg
The plugin hooks into WordPress's image handling at multiple levels, so this happens automatically - no need to change your templates or content.
Here's what gets transformed:
wp_get_attachment_image()and related functions- Responsive
srcsetattributes - Images embedded in post/page content
- Any code using
image_downsize() - REST API media endpoints (
/wp/v2/media)
The plugin transforms URLs in REST API responses by default. When you fetch media via /wp/v2/media, both source_url
and all size URLs in media_details.sizes are transformed.
This is enabled by default. To disable it globally:
add_filter( 'aysnc_wordpress_dynamic_media_config', function () {
return [
'rest_api_enabled' => false,
];
} );Or disable it per-request (useful for admin/editor contexts):
add_filter( 'aysnc_wordpress_dynamic_media_rest_enabled', function ( bool $enabled, WP_REST_Request $request, WP_Post $attachment ): bool {
// Disable for authenticated requests (likely editor)
if ( is_user_logged_in() ) {
return false;
}
return $enabled;
}, 10, 3 );The real power is in generating dynamic URLs on demand. Given any attachment ID, you can build a URL with whatever transformations you need.
These parameters work across all adapters:
use Aysnc\WordPress\DynamicMedia\Media;
// Basic resize
$url = Media::get_dynamic_url( $attachment_id, [
'width' => 800,
'height' => 600,
] );
// Hard crop (fills the exact dimensions)
$url = Media::get_dynamic_url( $attachment_id, [
'width' => 400,
'height' => 400,
'hard_crop' => true,
] );Need more than dimensions? The transform array lets you pass parameters directly to your adapter:
// Cloudinary: auto-optimize with face detection
$url = Media::get_dynamic_url( $attachment_id, [
'width' => 400,
'height' => 400,
'hard_crop' => true,
'transform' => [
'quality' => 'auto',
'fetch_format' => 'auto',
'gravity' => 'face',
],
] );
// Cloudinary: low-quality placeholder for lazy loading
$placeholder = Media::get_dynamic_url( $attachment_id, [
'width' => 100,
'transform' => [
'effect' => 'blur:1000',
'quality' => 30,
],
] );On portability: width, height, and hard_crop work across all adapters - the plugin translates these for each
service. The transform array does not. It's passed directly to your adapter, untouched.
This is intentional. Image services have different capabilities - Cloudinary's gravity: 'face' has no equivalent in
every provider. Rather than maintain a leaky abstraction, we give you direct access. The trade-off: if you switch
adapters, any code using transform needs to be updated.
Best practice: Don't call Media::get_dynamic_url() with transform scattered throughout your codebase. Wrap it in
your own function:
function get_image_url( int $id, array $args = [] ): string {
return Media::get_dynamic_url( $id, $args );
}That way if you change adapters - or just want to tweak your transforms - you have one place to update instead of hunting through templates.
The plugin needs to know your Cloudinary details. Add this filter to your theme or a mu-plugin:
add_filter( 'aysnc_wordpress_cloudinary_config', function () {
return [
'cloud_name' => 'your-cloud-name',
'auto_mapping_folder' => 'your-auto-upload-folder',
];
} );That's the minimum config. The auto_mapping_folder should match the folder name you set up in Cloudinary's auto-upload
settings.
| Option | Default | Description |
|---|---|---|
| cloud_name | (required) | Your Cloudinary cloud name |
| auto_mapping_folder | (required) | The folder configured in Cloudinary's auto-upload mapping |
| domain | res.cloudinary.com | Custom domain if you're using a CNAME |
| default_hard_crop | fill | Cloudinary crop mode for hard-cropped images |
| default_soft_crop | fit | Cloudinary crop mode for proportionally-scaled images |
Since configuration happens through a filter, you can pull values from wherever makes sense for your setup:
add_filter( 'aysnc_wordpress_cloudinary_config', function () {
return [
'cloud_name' => getenv( 'CLOUDINARY_CLOUD_NAME' ),
'auto_mapping_folder' => getenv( 'CLOUDINARY_FOLDER' ),
'domain' => getenv( 'CLOUDINARY_DOMAIN' ) ?: 'res.cloudinary.com',
];
} );Global plugin configuration.
add_filter( 'aysnc_wordpress_dynamic_media_config', function (): array {
return [
'rest_api_enabled' => true, // Enable REST API transformation (default: true)
];
} );Control REST API transformation on a per-request basis. Receives the request and attachment objects for context.
add_filter( 'aysnc_wordpress_dynamic_media_rest_enabled', function ( bool $enabled, WP_REST_Request $request, WP_Post $attachment ): bool {
// Skip transformation for specific attachments
if ( get_post_meta( $attachment->ID, '_skip_dynamic_media', true ) ) {
return false;
}
return $enabled;
}, 10, 3 );Parameters:
$enabled- Whether REST API transformation is enabled (default:true)$request- The REST request object$attachment- The attachment post object
Configure the Cloudinary adapter. See Configuration above.
Modify transformation arguments before the Cloudinary URL is built. Use this for site-wide settings like auto-optimization:
add_filter( 'aysnc_wordpress_cloudinary_args', function ( array $args, int $attachment_id ): array {
// Apply auto quality and format to all images
$args['transform']['quality'] = 'auto';
$args['transform']['fetch_format'] = 'auto';
return $args;
}, 10, 2 );This filter is Cloudinary-specific. If you switch adapters, you'll replace this with the equivalent for your new service.
Supported transformation parameters:
| Parameter | Cloudinary | Parameter | Cloudinary |
|---|---|---|---|
| width | w | height | h |
| crop | c | gravity | g |
| quality | q | fetch_format | f |
| effect | e | opacity | o |
| radius | r | angle | a |
| background | b | border | bo |
| overlay | l | underlay | u |
| dpr | dpr | zoom | z |
| aspect_ratio | ar | flags | fl |
| progressive | fl_progressive | named_transformation | t |
See Cloudinary's transformation reference for the complete list.
Modify the final dynamic URL before it's returned. Works with any adapter.
add_filter( 'aysnc_wordpress_dynamic_media_url', function ( string $url, int $attachment_id, array $args ): string {
// Log all generated URLs
error_log( "Dynamic URL for {$attachment_id}: {$url}" );
return $url;
}, 10, 3 );Parameters:
$url- The generated URL$attachment_id- WordPress attachment ID$args- Transformation arguments (width, height, hard_crop, etc.)
Adjust dimensions for individual srcset entries.
add_filter( 'aysnc_wordpress_dynamic_media_srcset_dimensions', function ( array $dimensions, int $attachment_id, array $image_meta, string $image_src ): array {
// Force soft crop for all srcset images
$dimensions['hard_crop'] = false;
return $dimensions;
}, 10, 4 );Parameters:
$dimensions- Array withwidth,height, and optionallyhard_crop$attachment_id- WordPress attachment ID$image_meta- WordPress attachment metadata$image_src- Original image source URL
Override the URL for images in post content. Return a string to use your custom URL, or null to let the plugin
generate one.
add_filter( 'aysnc_wordpress_dynamic_media_content_image_src', function ( ?string $src, int $attachment_id, ?string $original_src, $width, $height, string $size ): ?string {
// Skip transformation for full-size images
if ( $size === 'full' ) {
return $original_src;
}
return null; // Let the plugin handle it
}, 10, 6 );Parameters:
$src- Current source (null on first pass)$attachment_id- WordPress attachment ID$original_src- The originalsrcattribute value$width- Image width attribute$height- Image height attribute$size- Image size name extracted from class (e.g.,large,thumbnail)
Override the base upload URL used to determine the path within Cloudinary. Useful for multisite or custom upload configurations.
add_filter( 'aysnc_wordpress_cloudinary_upload_url', function ( string $upload_url ): string {
// Use a consistent URL for multisite
return 'https://example.com/wp-content/uploads';
} );The plugin uses an adapter pattern, so you can add support for other image services:
use Aysnc\WordPress\DynamicMedia\Adapter;
use Aysnc\WordPress\DynamicMedia\Adapters\MediaAdapter;
class ImgixAdapter implements MediaAdapter {
public static function get_dynamic_url( int $id, array $args ): string {
$original_url = wp_get_attachment_url( $id );
// Build your Imgix URL here
$imgix_url = 'https://your-source.imgix.net/' . basename( $original_url );
if ( ! empty( $args['width'] ) ) {
$imgix_url .= '?w=' . $args['width'];
}
// Handle $args['transform'] for Imgix-specific params
return $imgix_url;
}
}
// Register and activate your adapter
add_action( 'after_setup_theme', function () {
Adapter::register( 'imgix', new ImgixAdapter() );
Adapter::set( 'imgix' );
}, 20 ); // Priority 20 to run after default registrationIf you have multiple adapters registered, you can switch between them:
Adapter::set( 'cloudinary' );
Adapter::set( 'imgix' );Need to temporarily disable transformations? Maybe for debugging or a specific request:
use Aysnc\WordPress\DynamicMedia\Plugin;
Plugin::pause(); // Disable transformations
// ... do something with original URLs ...
Plugin::pause( false ); // Re-enablecomposer install
npm installnpm run test:phpcomposer lint # PHP CodeSniffer
composer format # PHP CS Fixer
composer static-analysis # PHPStan (level max)npm run lint:test # Runs lint, tests, and static analysis