I couldn't find any real world example of this library's usage in a CMS, so I wanted to ask my question here to the community. Apologies if this was already answered before.
I was previously using the v6 and I was simply adding the full path of any converted image to the editor content when I was populating blog posts.
Now that v7 automatically provides srcset as well as some inline javascript code for responsive images, how would you handle these conversions when saving/displaying a blog post?
Would you simply put the main image + an identifier ID as a reference when you save the content, and then dynamically pull all the conversions with their srcset attribute while displaying the content? Or would you directly embed the laravel-medialibrary srcset output to the document?
Thanks for answering.
For anyone interested, I simply ended up by adding images to the blog content including their srcset, but not including their sizes & onload attributes.
When rendering images I dynamically add "sizes=1px" property to each image on the server side, then I run a jQuery function to get each image and set their "sizes" attribute.
This is an interesting question. Albeit not a concern of the media library itself, I think there's value in writing up some thoughts.
At Spatie we do use the medialibrary in Blender (https://github.com/spatie/blender), but always in the context of a model. From what I understand, you're talking about embedding medialibrary images in a WYSIWYG or another "text source".
There's two things you'll need to keep in mind.
First, a medialibrary should be connected to a model, it shouldn't live on its own. Note that it is possible to save media items without a related model to it, but this package wasn't designed in this way. If you're going this road, you're on your own 😄
I too have encountered "images on their own", for which I made a simple Image model which a media item can connect to. Here's my implementation of this model:
class Image extends Model implements HasMedia
{
use HasMediaTrait;
protected $guarded = [];
public static function boot()
{
self::created(function (Image $image) {
$image->addMedia(resource_path($image->path))
->withResponsiveImages()
->preservingOriginal()
->toMediaCollection();
});
}
public static function findByPath(string $path): ?Image
{
return self::wherePath($path)->first();
}
public static function createWithPath(string $path): Image
{
return self::create([
'path' => self::normalizePath($path),
]);
}
public function scopeWherePath(Builder $builder, string $path): Builder
{
return $builder->where('path', self::normalizePath($path));
}
private static function normalizePath(string $path): string
{
$resourcePath = 'assets/images';
$fullResourcePath = "resources/{$resourcePath}";
$basePath = resource_path($resourcePath);
return
rtrim($resourcePath, '/')
. '/'
. ltrim(str_replace([$basePath, $fullResourcePath, $resourcePath], '', $path), '/');
}
}
As you can see, I've added some helper methods so that I can use the path itself as the identifier. So to answer this question:
Would you simply put the main image + an identifier ID as a reference when you save the content
I would probably use the path as the ID, and load the Image model from there. This means you'll have to parse your WYSIWYG content somewhere, I'd do this on save, and keep a rendered version and editor version. Another approach would be to introduce some kind of "shortcode tags" as known in WordPress. How exactly you'd parse the content is up to you 🙂
Second, the question about sizes. I won't go into much detail about how the sizes spec was designed, but it's important to know that you cannot solve this problem in a reusable way. There's no way around it: you need to configure sizes for every image, and what will be in that sizes attribute will depend on where the image will be shown on your page. A full-width hero image will have different sizes than an overview thumbnail image. You can read more about the why and how of sizes in the media library in this issue: https://github.com/spatie/laravel-medialibrary/issues/810
So what you're doing, determining the sizes with JS is an option, though to me personally it doesn't feel like the best one. From my experience - and certainly in a CMS setting - you should configure a few different size-presets. Eg. "hero", "thumbnail", "article-aside", "article-full" etc. Each preset has their own sizes. When uploading an image, you should configure select what kind of preset you want to use for this image, and save it in either the Image model I mentioned earlier, or in the Media model itself, as it has custom properties support.
There's a lot more to say about this, but I'm going to close this issue for now. Feel free to still respond, but it's not a "real issue" concerning this package.
Hi @brendt , thank you for sharing your thoughts on this.
I too always use medialibrary in the context of my Post model. I then list a post's attachments in the sidebar of an editing window and I provide an option to occasionally add them to the WYSIWYG editor. I like the way the medialibrary works as I don’t want to have a separate “Media” panel, I just want to keep all attachments live under their respective posts in order to prevent clutter. But anyway it’s good to know that we also have an easy way to use the library without a related model. :)
I think storing the same content twice (both for the editor and for the frontend) would be a bit redundant, but maybe parsing it before rendering it on the frontend + using some frontend caching would be preferable.
For the sizes, I was thinking that I had already managed to resolve it thanks to responsive images + progressive loading options provided by the library. I’m not sure what could go wrong with this approach but I’m happy to get some more thoughts about it.
When you render all the images (within the content ) with the “sizes=1px” tag on the first go, you only get the blurry svg previews (which are already prepared by the library), and then when you dynamically set the sizes attribute via JavaScript, browsers read the srcset attribute and automatically download the most appropriate version from the responsive images folder, as it was also documented in the media library documentation.
If I’m not wrong, this approach makes it unnecessary to deal with different size presets (except thumbnails which require a square format - I use a separate media conversion for them).
The original src attribute always remains as the fallback, and I also consider keeping this under control by limiting the maximum dimensions of the main image to 1024 x 1024 pixels by the following code piece on the upload process:
request()->validate([
'upload' => ['image']
]);
$file = request()->file('upload');
$image_size = getimagesize($file);
if ($image_size[0] > 1024 or $image_size[1] > 1024) {
Image::load($file)->width(1024)->save();
}
return $this
->addMedia($file)
->withResponsiveImages()
->toMediaCollection('attachments');
And in my Post model, I use an accessor for the parsed content (a single line for now):
public function getParsedContentAttribute()
{
$content = str_replace("<img", "<img sizes=\"1px\"", $this->attributes['content']);
return $content;
}
And finally, I use this piece of JavaScript to find the appropriate size for each image in the post content:
<script defer>
jQuery('.blog-image').each(function(){
jQuery(this).attr('sizes', Math.ceil(this.getBoundingClientRect().width/window.innerWidth*100)+'vw');
});
</script>
This is how I managed to make it work for my case and I just wanted to share it with you Brent and also for anybody who may require working for such a thing.
If I’m not wrong, this approach makes it unnecessary to deal with different size presets (except thumbnails which require a square format - I use a separate media conversion for them).
It certainly is a valid approach. There's a downside with the JS solution though. The real image is loaded slightly later than when it could be loaded if you knew the sizes beforehand.
If the correct srcset and sizes are defined in the HTML, that is: the HTML that's downloaded from the server; almost every browser can pre-load the correct image way earlier. Loading the correct image is based on both srcset and sizes attributes and the current viewport width. If the browser knows both, it can request the image from the server as soon as the HTML is downloaded and pre-parsed.
On the other hand, if you use JS to dynamically calculate the sizes of the image, you'll have to wait for a few things:
This pushes back the load time of the image quiet far. It will of course show the blurred preset immediately, but on devices with a slower CPU, there might be a noticeable delay.
Now what with client-side cached images? The same flash can happen with your approach, because even though the image doesn't need downloading, the browser still has to parse DOM and CSS, and on top of that execute some JS code for every image.
It's a detail of course, and chances are your users won't be bothered by it. But keep in mind that this little improvement is exactly why the sizes attribute was introduced. There's the downside of more manual configuration, but you'll have the benefit of images appearing a little faster on your screen in a lot of cases.
The real image is loaded slightly later than when it could be loaded if you knew the sizes beforehand.
You made a very good point and I also figured out that we could consider disabling that "load SVG first + find the appropriate image with JS while displaying the content" approach in the future.
So I changed my way a little and decided to include the correct sizes parameter directly when saving the content to the database, but as an additional "data-sizes" attribute. This makes me able to still support the previous approach upon our future needs.
Here is how I implemented this.
_Note: In order this to work properly, your editor width and the actual content holder in frontend should be identical or at least close enough, because we calculate the sizes according to the images' actual sizes in the editor window._
1) In the post editor, just before saving the content, I grab all the included images' sizes and add them as "data-sizes" attribute:
save: function(formData) {
jQuery('.blog-image').each(function(){
jQuery(this).attr('data-sizes', Math.ceil(this.getBoundingClientRect().width/window.innerWidth*100)+'vw');
});
let content = jQuery('#editor1').summernote('code');
...
2) Before displaying the content, In the parsed post content accessor, I decide to/not to use those attributes we already saved:
public function getParsedContentAttribute()
{
if (config('medialibrary.responsive_images.use_tiny_placeholders')) {
return str_replace("<img", "<img sizes=\"1px\"", $this->attributes['content']);
} else {
return str_replace("data-sizes=", "sizes=", $this->attributes['content']);
}
}
3) Finally, we display the content directly (without a JS manipulation) if sizes attributes are already set, or dynamically calculate them if we enabled tiny placeholders:
<script defer>
jQuery('.blog-image[sizes=1px]').each(function(){
jQuery(this).attr('sizes', Math.ceil(this.getBoundingClientRect().width/window.innerWidth*100)+'vw');
});
</script>
Cool! It's a nice example on how to use the medialibrary in a CMS context 👍
Most helpful comment
You made a very good point and I also figured out that we could consider disabling that "load SVG first + find the appropriate image with JS while displaying the content" approach in the future.
So I changed my way a little and decided to include the correct sizes parameter directly when saving the content to the database, but as an additional "data-sizes" attribute. This makes me able to still support the previous approach upon our future needs.
Here is how I implemented this.
_Note: In order this to work properly, your editor width and the actual content holder in frontend should be identical or at least close enough, because we calculate the sizes according to the images' actual sizes in the editor window._
1) In the post editor, just before saving the content, I grab all the included images' sizes and add them as "data-sizes" attribute:
2) Before displaying the content, In the parsed post content accessor, I decide to/not to use those attributes we already saved:
3) Finally, we display the content directly (without a JS manipulation) if sizes attributes are already set, or dynamically calculate them if we enabled tiny placeholders: