Optimizing Image Delivery in an Obsidian Publish website using Cloudflare Images Transformations

What I’m trying to do

I am trying to optimize the delivery of images on my blog that is delivered via Obsidian Publish. My site is already delivered on a custom domain via Cloudflare, and I already have the Cloudflare Images product on my account, so I’m trying to use Transform Images to dynamically optimize and cache images via Cloudflare.

Essentially, the process would simply work by prefixing all image URLs so that they are run through the Cloudflare Images infrastructure, so instead of the original image URL from Obsidian Publish:

https://publish-01.obsidian.md/access/fce28e11a3060e1ae5f0c9ef0da24dab/XML%20Aficionado/Files/afalk42_A_highly_detailed_futuristic_humanoid_robot_portrait_ag_ee942afe-312d-44d3-bed2-f6364a68b4bd.png

I would need to be able to programmatically add this prefix to the beginning of the URL:

https://xmlaficionado.com/cdn-cgi/image/width=1000,quality=75,fit=scale-down,format=auto/

So that the final URL in any <img src=“…”> tag would end up being this:

https://xmlaficionado.com/cdn-cgi/image/width=1000,quality=75,fit=scale-down,format=auto/https://publish-01.obsidian.md/access/fce28e11a3060e1ae5f0c9ef0da24dab/XML%20Aficionado/Files/afalk42_A_highly_detailed_futuristic_humanoid_robot_portrait_ag_ee942afe-312d-44d3-bed2-f6364a68b4bd.png

Doing that little image transformation magic would reduce the original .png with a size of 2,043 KB to a tiny little .webp with a size of only 46 KB, in other words a 97.7% reduction in file size, which would be amazing for site performance and user friendliness.

Things I have tried

For some of my other websites that are based on different backends, I have solved this already perfectly by using a Cloudflare Worker to transform the HTML pages and do a dynamic replacement of all <img src=“…”> URLs on the fly.

However, when I tried to apply the same technique to my Obsidian Publish site, this approach did not work, because the HTML code that displays the Obsidian Publish site is not originally part of the HTML page, but rather is dynamically constructed via JavaScript only in the browser on the user’s machine. Hence I am unable to intercept and manipulate the HTML code to insert those URL prefixes.

Has anyone tried to use the publish.js file to do dynamic replacement of image source URLs in an Obsidian Publish site yet? Would that work? Would that code run at the right time, i.e. before those images are actually loaded?

Here are the images for comparison

Original image from Obsidian Publish (2,043 KB)

Shrunken version via Cloudflare Images (46 KB)

Actually, I have now also already tried to add code to the publish.js file to do the dynamic replacement of image source URLs in the HTML page after it has been constructed.

But unfortunately that didn’t work either (except for the site logo image), because of the lazy-loading implementation that renders the page first and then only loads the images. So when my JavaScript code in publish.js runs, there aren’t yet any <img> tags to find and replace their src attributes for.

Any other suggestions? It would be awesome, if there was a JavaScript variable that I could simply replace with my own prefix, so that the lazy-loading code knows where to get the images from…

Well, since nobody else replied or offered a solution, I developed my own. Hopefully this is helpful for anyone who wants to solve the same problem for their site…

The following code inserted into publish.js seems to work pretty well and does the job for me.

The short explanation is:

  • The prefix is the special URL for Cloudflare Images Transformations and it is set up to shrink the image to a max width of 1000, reduce quality to 75, and convert to a compressed format such as avif or webp
  • The function addCloudflareImagesPrefix() expects to be called with an <img> element node and will look at the src= attribute. If it isn’t an .svg file (as those cannot really be optimized much) and also hasn’t already been prefixed with the prefixURL earlier, we change the src to a new URL that starts with the prefix
  • The next step is to go through all existing images on the page and add the prefix
  • The reminder of the code is intended to defined observers and handlers that will catch dynamically created <img> elements that are added to the DOM by JavaScript code:
    • We need a handleAttributeChange() function to handle any changes to a src= attribute
    • We also need a handleChildAddition() function to handle any additions of new elements anywhere in the DOM tree, and in that function we need to iterate over added nodes and determine if any of them are either image elements themselves or have children anywhere in their subtree that are image elements.
    • The final step is to create a mutation observer and set it up to watch for child additions anywhere using the above handlers
// Rewrite all <img> sources to use Cloudflare Images Transformations
const prefix = "https://YOUR_CLOUDFLARE_SITE/cdn-cgi/image/width=1000,quality=75,fit=scale-down,format=auto/";

// Function to add prefix to source for cloudflare images transformations
function addCloudflareImagesPrefix(image_node) {
  // Get the current value of the src attribute
  const currentSrc = image_node.getAttribute("src");
  // Check if the src attribute is not empty and doesn't already start with the prefix
  if (currentSrc && !currentSrc.startsWith(prefix) && !currentSrc.endsWith(".svg")) {
    // Add the prefix to the src attribute
    const newSrc = prefix + currentSrc;
    image_node.setAttribute("src", newSrc);
    // console.log('Image src attribute changed to:', newSrc);
  }
}

// Find all the <img> elements on the page
const images = document.getElementsByTagName("img");
// console.log('Found this many images in document at the start:', images.length);
// Loop through each <img> element
for (let i = 0; i < images.length; i++) {
  addCloudflareImagesPrefix( images[i] );
}

// Also set up an observer to catch future additions of image elements and then any changes of their image sources

// Function to handle the attribute change event
function handleAttributeChange(mutationsList, observer) {
  for (let mutation of mutationsList) {
    if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
      // console.log('Image src attribute observer triggered:', mutation.target.src);
      addCloudflareImagesPrefix( mutation.target );
    }
  }
}

// Function to process one img element and add observer for future src changes
function processImages(node) {
  // console.log('Image node added observer found child node:', node);
  addCloudflareImagesPrefix( node );
  // Also create a MutationObserver instance for future attribute changes
  const attributeObserver = new MutationObserver(handleAttributeChange);
  attributeObserver.observe(node, { attributes: true });
}

// Function to handle the child addition event
function handleChildAddition(mutationsList, observer) {
  for (let mutation of mutationsList) {
    if (mutation.type === 'childList') {
      for (let node of mutation.addedNodes) {
        if (node.nodeType === Node.ELEMENT_NODE) {
          const imgElements = node.querySelectorAll('img');
          for (let img of imgElements) {
            processImages(img);
          }
        }
      }
    }
  }
}

// Create a MutationObserver instance for child additions
const childObserver = new MutationObserver(handleChildAddition);

// Observe the entire document for child additions
childObserver.observe(document, { childList: true, subtree: true });

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.