Image Optimization with Astro and Squidex

June 4, 2024

In modern web application development, prioritizing performance and content flexibility has become indispensable. This is why using a headless Content Management System "is no longer an option, it's a necessity" .

Although integrating Astro with most CMS is a trivial process, "challenges" arise as a result of using an independent server to store our website's content:

  • Slow image loading
  • Requests to different domains
  • Risk of overloading the CMS

Proposed solution

A possible solution is to use an Astro endpoint that acts as an intermediary between the web browser and the CMS. With this, we'll have the opportunity to make modifications and transformations that we need to the input URLs and output images of the endpoint.

Our endpoint receives image requests, with parameters such as id, width, height, and quality. We move from directly using the CMS domain to using the same domain as our website:

If we had:

<Image src="https://cms.server.com/api/assets/imageId" />

Now we would have:

<Image src="https://ourwebsite.com/api/image/imageId" />

Additionally, it opens up the possibility of adding parameters for a perfect adjustment of the image dimensions and, if applicable, we can lower the image quality to reduce the size:

// "w" for width
// "h" for height
// "q" for quality (in percentage)

<Image
	src="https://ourwebsite.com/api/image/imageId?w=200&h=200&q=80"
	width="200"
	height="200"
/>

Now let's look at the code

💡 Note : In this article, we'll use the Squidex CMS as an implementation example. However, it can be adapted to any CMS compatible with Astro .

The first thing we need to do is add the [id].ts endpoint file inside our pages folder. In my case, I'm using TypeScript and I'm going to create an api folder to better organize my endpoints:

src/pages/
    └── api/
        └── image/
            └── [id].ts // id will be the image identifier parameter

Astro allows us to define HTTP verbs in our endpoint. In this case, we're going to use GET to receive requests:

import type { APIRoute } from 'astro';

export const GET: APIRoute = async ({ params, url }) => {
    const { id } = params;
    const width = url.searchParams.get("w");
    const heigth = url.searchParams.get("h");
    const quality = url.searchParams.get("q");

    const cmsImage = await getFromCms(id, width, heigth, quality);
    if (!cmsImage) {
        return new Response('Image not found', { status: 404 });
    }

    const imageBuffer = cmsImage.buffer;
    const contentType = cmsImage.type;
    const origin = cmsImage.origin ?? "server";
		
    // In my case, I set the cache to one year
    const cacheHeader = 'max-age=31536000, immutable';

    return new Response(buffer, {
        headers: {
            'Content-Type': contentType,
            'Cache-Control': cacheHeader, 
            'Astro-Origin': origin, // This header is optional, it's just informative
        },
    });
};

Now we define the function that will get the image from the CMS. In this case, a simple key/value caching implementation was made using the Map object. If the image doesn't exist in the cache, the CMS URL is built, the image is downloaded and saved in the cache to avoid downloading the same image on a next opportunity:

const rootCmsUrl = "https://cms.server.com";
const imageCache = new Map();

export type CmsImage = {
    id: string;
    buffer: ArrayBuffer;
    type: string;
    origin?: string;
}

async function getFromCms(id: string, width?: string, heigth?: string, quality?: string): Promise<CmsImage> | undefined {
    const type = "WEBP";
    const imageQuality = quality ?? "100";

    // We try to get the image from the cache
    const cacheKey = `${id}/w${width ?? ""}/h${heigth ?? ""}/q${quality ?? ""}`;
    const cachedData = imageCache.get(cacheKey);
    if (cachedData) {
        return {
            id: id,
            buffer: cachedData.buffer,
            type: cachedData.contentType,
            origin: "cache-cms",
        };
    }

    let imageUrl = `${rootCmsUrl}/api/assets/devcloud-landing/${id}?quality=${imageQuality}&format=${type}`;
    if (width && heigth) {
        imageUrl += `&width=${width}&height=${heigth}&mode=Max`;
    }
		
    const response = await fetch(imageUrl);
    if (!response.ok) {
        return null;
    }

    const contentType = response.headers.get('Content-Type') || 'image/jpeg';
    const imageBuffer = await response.arrayBuffer();

    // The image is saved in the cache
    imageCache.set(cacheKey, {
        buffer: imageBuffer,
        contentType: contentType
    });

    return {
        id: id,
        buffer: imageBuffer,
        type: contentType,
        origin: "server-cms",
    };
}

This simple implementation not only solves the identified problems but also allows us to create helpers in the project. These will automatically convert CMS URLs to endpoint URLs, facilitating their use in all sections of our page where images are displayed.

Proof of Concept

A practical example of the implementation, is this image stored in the CMS of this website:

Astro Banner (directly from CMS)
Astro Banner (directly from CMS)

And now the same image, but using the endpoint, adjusted to 768x402 and with a quality of 90%, since for the human eye this difference is almost imperceptible, but the image size is indeed reduced:

Astro Banner (using endpoint)
Astro Banner (using endpoint)

In this example, we went from a file size of 54kB for the CMS image to just 23kB for the image processed through the endpoint..
Can you notice the difference between both images?

Conclusions and Next Steps

The implementation of an Astro endpoint as an intermediary for image management offers significant improvements in performance and flexibility, as evidenced by the substantial reduction in image size without perceptible loss of quality.

To further enhance this solution, the following steps are recommended:

  • Implement a more robust caching system, for example, using Cloudflare Workers KV for distributed, high-performance caching.
  • Expand support to multiple image sources beyond the CMS, such as allowing optimization of images stored in Amazon S3 or Google Cloud Storage buckets.
  • Integrate optimization techniques for modern formats like AVIF, which can further reduce image size while maintaining high visual quality.