Set default attributes to markdown images to avoid cls

Hi!

To avoid negative Cumulative Layout Shift (CLS) ranking I need to set default width and height attributes to all images that an author writes in markdown content field.

Is there a possibilty to set them according to objects original dimensions? I need help to find out how to modify the output of the image.

In the same way I would like to set default classes to use own javascript like lazyload.

(https://web.dev/optimize-cls/?utm_source=lighthouse&utm_medium=devtools)

I am looking forward to an answer!

Stay healthy!
Christiana!

@christiana83, You could try the following:

Using a fresh install of Grav 1.7.5

  • In Twig template:

    // Twig
    
    {% set image = page.media.images|first %}
    {{ image.height().width().html('Image inserted in Twig', 'Alt', 'Class') | raw }}
    
    // Generated HTML
    
    <img height="800" width="1200" title="Image inserted in Twig" 
      alt="Alt" class="Class" src="/user/pages/03.cls/image.jpg">
    
  • Using a custom shortcode (see GitHub - getgrav/grav-plugin-shortcode-core: Grav Shortcode Core Plugin)

    • Install plugin ā€˜Shortcode Coreā€™: $ bin/gpm install shortcode-core

    • Create file ā€˜user/config/plugins/shortcode-core.yamlā€™ and add:

      custom_shortcodes: '/user/custom/shortcodes'
      
    • Create file ā€˜user/custom/shortcodes/ClsShortcode.phpā€™ with the following content:

      <?php
      namespace Grav\Plugin\Shortcodes;
      
      use Thunder\Shortcode\Shortcode\ShortcodeInterface;
      
      class ClsShortcode extends Shortcode
      {
         public function init()
         {
            $this->shortcode->getRawHandlers()->add('image', function(ShortcodeInterface $sc) {
               $image = $sc->getParameters()['src'];
               $title = $sc->getParameters()['title'];
               $alt = $sc->getParameters()['alt'];
               $classes = $sc->getParameters()['classes'];
      
               return "{{ page.media['$image'].width().height().html('$title', '$alt', '$classes') | raw }}";
            });
         }
      }
      
    • Create page ā€˜/user/pages/03.cls/default.mdā€™ with the following content:

      ---
      title: Cls
      process:
          twig: true
      ---
      
      [image src=image.jpg title="Image inserted in Markdown using Shortcode" alt=Alt classes="class1 class2"]
      
    • Add image ā€˜images.jpgā€™ to folder ā€˜/user/pages/03.clsā€™

    • Generated HTML

       <img width="1200" height="800" 
         title="Image inserted in Markdown using Shortcode" 
         alt="Alt" class="class1 class2" 
         src="/user/pages/03.cls/image.jpg">
      
    • Off course, you could create a shortcode plugin to reuse it in multiple sites.

1 Like

Thanks for your help and a first approach!

Yes, I would have to generate this for all pages by default, including the approx. 1000 pages that already exist. Without the editors having to do it.

I thought of a possibility to generally change the output of the image markup by changing the translation of the markdown into html. Maybe via a hook in a plugin. I just havenā€™t found out which one it is and whether there is an event for it.

@christiana83, My suggestions are not really of any help with the existing pagesā€¦

Take a look at event: onPageContentProcessed.

This event is fired after the pageā€™s content() method has processed the page content. This is particularly useful if you want to perform actions on the post-processed content but ensure the results are cached. Performance is not a problem because this event will not run on a cached page, only when the cache is cleared or a cache-clearing event occurs.

At the bottom of the Grav lifecycle it says:

Whenever a page has its content() method called, the following lifecycle occurs:

Page.php

  1. If content is NOT cached:
  2. Fire onPageContentRaw event
  3. Process the page according to Markdown and Twig settings. Fire onMarkdownInitialized event
  4. Fire onPageContentProcessed event
  5. Fire onPageContent event

During the event you will have to:

  • regex the content to find the image tag
  • get hold of the image to get its dimensions
  • update the content with dimensions
  • write content
1 Like

@christiana83, Did some playing with code. The following onPageContentProcessed event handler seems to work.

Note: Used images are in pageā€™s folder.

public function onPageContentProcessed($event) {
    $page = $event['page'];
    $media = $page->getMedia();
    $content = $page->getRawContent();

    // Get all <img> elements
    $matches = [];
    preg_match_all('#<img.*src="([^"]+)".*?/>#' , $content , $matches);

    $contentChanged = false;

    for($i = 0; $i < count($matches[0]); $i++) {
        // Skip <img> elements that already contains width and/or height
        if (preg_match('#(width|height)#', $matches[0][$i])) {
            continue;
        }

        $src = $matches[1][$i];
        $filename = substr($src, strrpos($src, '/') + 1);
        $img = $media->get($filename);

        $oldElement = substr($matches[0][$i], 0, -2);
        $newElement = $oldElement . 'width="' . $img['width'] . '" height="' . $img['height'] . '"';
        $content = str_replace($oldElement, $newElement, $content);

        $contentChanged = true;
    }

    if ($contentChanged) {
        $page->setRawContent($content);
    }
}

Pageā€™s Markdown:

![Image1](image1.jpg)
![Image2](image2.jpg)

Generated HTML:

<p>
<img alt="Image1" src="/user/pages/01.home/image1.jpg" width="1200" height="800"/>
<img alt="Image2" src="/user/pages/01.home/image2.jpg" width="639" height="481"/>
</p>

Alternatives:

1. Update Markdown by one-time run of plugin.
You could also loop through all pages in onPagesInitialized and update the Markdown. This should be a one-time action updating all pages once and for all.

// default.md

---
title: Home
body_classes: 'title-center title-h1h2'
---

![Image1](image1.jpg?classes=myclass)
![Image2](image2.jpg?resize=400,200)
// plugin onPagesInitialized event

public function onPagesInitialized($event) {
    $pages = $event['pages'];

    foreach($pages->all() as $page) {
        $raw = $page->raw();
        $media = $page->media();

        $matches = [];
        preg_match_all('#!\[[^\]]*\]\(([^?)]+)[^)]*\)#', $raw, $matches);

        for($i = 0; $i < count($matches[0]); $i++) {
            if (preg_match('/(height|width)/', $matches[0][$i])) {
                continue;
            }

            $src = $matches[1][$i];
            $img = $media[$src];

            $oldMarkdown = $matches[0][$i];
            $newMarkdown = str_replace('?', '?width=' . $img['width'] . '&height=' . $img['height'] . '&', $oldMarkdown);
            $raw = str_replace($oldMarkdown, $newMarkdown, $raw);

            $page->raw($raw);
            $page->save();
        }
    }
}
// Updated default.md

---
title: Home
body_classes: 'title-center title-h1h2'
---

![Image1](image1.jpg?width=639&height=481&classes=myclass)
![Image2](image2.jpg?width=1200&height=800&resize=400,200)

2. Admin onSave

You could of course do something similar when Admin saves a page.

1 Like

@pamtbaau in addition to say, I started including lazyloading for markdwon images with lazysizes.min.js (GitHub - aFarkas/lazysizes: High performance and SEO friendly lazy loader for images (responsive and normal), iframes and more, that detects any visibility changes triggered through user interaction, CSS or JavaScript without configuration.)

So I choose the solution to use event hook onMarkdownInitialized($event) for manipulating the image output. This way I can also use to add the dimensions of the image later.

$page = $event['page']; 
$images = $page->media()->images();
$markdown->addInlineType('!', 'ImageExtended', 0);

$imageExtended = function($excerpt) use ($images) {

    $inlineImage = $this->inlineImage($excerpt);

    if (!isset($inlineImage)) {
        return null;
    };

    if (!isset($inlineImage['element']['attributes']['data-srcset'])){
        if (isset($inlineImage['element']['attributes']['srcset'])) $inlineImage['element']['attributes']['data-srcset'] = $inlineImage['element']['attributes']['srcset'];
        if (isset($inlineImage['element']['attributes']['src'])) $inlineImage['element']['attributes']['data-src'] = $inlineImage['element']['attributes']['src'];

        $inlineImage['element']['attributes']['class'] = isset($inlineImage['element']['attributes']['class']) ? $inlineImage['element']['attributes']['class'] . ' lazyload' : 'lazyload';

        unset($inlineImage['element']['attributes']['srcset']);              

        $inlineImage['element']['attributes']['src'] = '/user/themes/tecart-website-front/assets/img/preview-images/showcase.jpg';

    }
    return $inlineImage;
};

$markdown->inlineImageExtended = $imageExtended->bindTo($markdown, $markdown);

}

The javascript file is included in my base.html.twig

{% do assets.addJs('theme://assets/js/lazysizes/lazysizes.min.js', {'priority': 100, 'group': 'bottom'}) %}

The initial hint for my solution I found in a grav lazysizes plugin.

1 Like

@christiana83, Iā€™ll have to study your solution a bit more, but apart from thatā€¦

Just curious, what made you choose for an external lazyloading library while:

  • browsers now accept the loading="lazy|eager" attribute natively
  • Grav has added to function loading to set the attribute

@pamtbaau Yes, but native lazyloading by loading=ā€œlazyā€ is still not working in all browsers, e.g. safari.
Additionally the website ist that big and connected to other applications like attlassian jira and additionally with several static exports so that we haventā€™t yet updated to the latets grav version. that issue is in progress.
And we have own image markups from twig files page modules with <picture> tags that need an lazyloading library.

@christiana83, Fair enoughā€¦

1 Like

A post was split to a new topic: Migrating pages to flex-pages

@christiana83, Iā€™ve created a PR with a proposal to add the height and width automatically when an <img> tag is created in Markdown.

2 Likes

Additional infos that may be interessting for responsive images when width and height attributes are set I found on: Fixing The Resizing Problem

Above PR has been merged (albeit, without much of my initial code :slight_smile: ), which will allow height and width to be automatically added to images.

system.yaml has received a new option:

images:
  cls:                   # Cumulative Layout Shift: See https://web.dev/optimize-cls/
    auto_sizes: false    # Automatically add height/width to image
    aspect_ratio: false  # Reserve space with aspect ratio style
    retina_scale: 1      # scale to adjust auto-sizes for better handling of HiDPI resolutions

In markdown, you can use:

![](sample-image.jpg)

or override settings like:

![](sample-image.jpg?autoSizes=true)

Which will generate:

<img alt="" 
  src="/path/to/sample-image.jpg" 
  width="1024" height="768" />
2 Likes