RyanParsleyDotCom

Refactoring my Astro Gallery Component

Last updated on

One of the first components I wrote for this blog migration was a naive image gallery. It’s pretty straight forward and got mostly got the job done, but there was a few implementation details I’ve been itching to improve and I recently took the time to make those enhancements. Let’s dig into the details of my refactor and talk about some of the benefits of this new iteration.

How it started

Let’s start by looking at the first iteration that I shipped. This component was functional, and a completely respectable thing to ship to production, but there’s always room for improvement. Does anything jump out to you as problematic before you read on?

---
type Image = {
 url: string;
 alt: string;
};
type Props = {
 images: Image[];
};
const { images } = Astro.props;
---
<div class="gallery">
 {
  images.map((image: Image, index: number) => (
   <>
    <figure>
     <a href={`#image-${index}`} title={image.alt}>
      <img src={image.url} alt={image.alt} />
     </a>
    </figure>
    <a
     href="#_"
     class="lightbox"
     id={`image-${index}`}
     title="Click to return to post"
    >
     <div style={`background-image:url(${image.url})`} />
    </a>
   </>
  ))
 }
</div>

How it’s going

Now, let’s take a look at my recently refactored version. It’s got more going on to be sure.

---
import { Image } from "astro:assets";
import type { ImageMetadata } from "astro";
type ImageData = {
 url: string;
 alt: string;
};
type Props = {
 images: ImageData[];
 path: string;
};
const { images, path } = Astro.props;
const getFullPath = (url: string) =>
 url.startsWith("./")
  ? `/src/content${path}/${url.replace("./", "")}`
  : `/src/assets/${url.replace("./", "")}`;
const imagePaths = images.map((img) => ({
 ...img,
 url: getFullPath(img.url),
}));
const imageAssets = import.meta.glob<{ default: ImageMetadata }>(
 `/src/**/*.{jpeg,jpg,png,gif}`,
);
const deriveSrc = async (image: ImageData) => {
 const source = imageAssets?.[image?.url]?.();
 if (source === undefined) {
  throw new Error(
   `"${image.url}" does not exist in glob: "src/assets/*.{jpeg,jpg,png,gif}"`,
  );
 }
 return source;
};
---
<div class="gallery">
 {
  imagePaths.map((image: ImageData, index: number) => (
   <>
    <figure>
     <a href={`#image-${index}`} title={image.alt}>
      <Image src={deriveSrc(image)} alt={image.alt} />
     </a>
    </figure>
    <a
     href="#_"
     class="lightbox"
     id={`image-${index}`}
     title="Click to return to post"
    >
     <div>
      <Image src={deriveSrc(image)} alt={image.alt} />
     </div>
    </a>
   </>
  ))
 }
</div>

Key improvements

My initial design failed to take advantage of Astro’s build-time image optimization. The reason I couldn’t take advantage of that was this component looked in the public directory for assets. Only assets in the src directory are optimized at build time. As I wanted my component to receive props from markdown frontmatter, relative pathing was a little tricker than I wanted to sort out upfront. Supporting this is the primary reason for all the extra complexity in the refactor.

Astro Image integration

The refactor starts with the introduction of the Image component from astro:assets. This is better than the native img tag for the following reasons.

  • Automatic image optimization
  • Lazy loading out of the box
  • Proper sizing and srcset generation

With great power… yada yada

Switching to this richer image solution comes with some complexity to get it up and running. First, I can’t simply use a src string prop and call it a day. I I need to import images so Astro is enabled to do it’s thing at build time. The docs make this look simple enough because they’re just importing a single static asset. In my case, this gets more complicated because I want to handle this dynamically. Fortunately, a more advanced recipe introduced me to a vite method import.meta.glob to help. The following code will gather all images found in the src directory and make them available for the Image component.

const imageAssets = import.meta.glob<{ default: ImageMetadata }>(
  `/src/**/*.{jpeg,jpg,png,gif}`,
);

Flexible path handling

Access to images is only half of the equation though. I need to sort out a sensible strategy for mapping my frontmatter to this collection to retrieve the images I want. The new getFullPath function allows for this. I may build this out more later, but for now, I images relative to the markdown, or absolute by way of a designated asset directory.

const getFullPath = (url: string) =>
  url.startsWith("./")
    ? `/src/content${path}/${url.replace("./", "")}`
    : `/src/assets/${url.replace("./", "")}`;

Conclusion

When I first read about the Image component, I naively thought I’d get performance “for free”. While supporting this took some additional work, both my developer experience and the performance of my site are noticeably better for having made this refactor. I’m trying to make it a little better every day.

Continue the converstion elsewhere

Let's chat more on the platform of your choice.

Published by