Astro is an amazing framework. Perspect is framework agnostic, but some of us have taken a particular liking to qualities of Astro that allow for both statically generated content and client-side "hydration" where it makes sense. One can even combine those two things together. One of the areas where it makes sense to combine static page generation and dynamism is for images. It's ideal for the overall page to be statically generated, but for the image sizes to be dynamically chosen by the browser so that a very large resolution image does not get used for small resolution oriented mobile browsers. Enter the concept of "islands." Here's how to make this work in Perspect, but you can probably adapt it to other services too:
In src/components/content.js
, define the getMediaFile
function that will run client-side and allow the browser to select the correct image size to be displayed:
export const getMediaFile = (media) => {
// Define media query conditions for different image size categories
const mediaQueries = {
very_small: "(max-width: 319px)",
small: "(min-width: 320px) and (max-width: 499px)",
medium_small: "(min-width: 500px) and (max-width: 749px)",
medium: "(min-width: 750px) and (max-width: 999px)",
medium_large: "(min-width: 1000px) and (max-width: 1499px)",
large: "(min-width: 1500px) and (max-width: 2499px)",
very_large: "(min-width: 2500px)",
};
let preferredSize = null;
// Determine the preferred size based on the media query
if (typeof window !== 'undefined' && window.matchMedia) {
// Determine the preferred size based on the media query
for (const size in mediaQueries) {
if (window.matchMedia(mediaQueries[size]).matches) {
preferredSize = size;
break;
}
}
} else {
// Set a default size if window is not available
preferredSize = 'medium';
}
// If a preferred size is determined, attempt to find a media file that matches
if (preferredSize) {
// Flatten the array of arrays to make it easier to search through all media items
const flatMedia = media.flat();
const file = flatMedia.find(file => file.size === preferredSize);
if (file) return file;
}
// Fallback logic: return the smallest available size or a specific default if no size matches
// Flatten the media array for the fallback scenario as well
const flatMedia = media.flat();
return flatMedia.find(file => file.size) || null;
};
Create a React component in src/components/DynamicImage.jsx
with the following code:
import React, { useState, useEffect } from 'react';
import { getMediaFile } from './content';
function DynamicImage({ media, id }) {
const [imageSrc, setImageSrc] = useState('');
const [imageAlt, setImageAlt] = useState('');
useEffect(() => {
const mediaFile = getMediaFile(media);
if (mediaFile) {
setImageSrc(`//${mediaFile.link}`);
setImageAlt(mediaFile.description || 'Dynamic Image');
}
}, [media]); // Re-run effect if media prop changes
return (
<img id={id} width="1020" height="510" src={imageSrc} alt={imageAlt} />
);
}
export default DynamicImage;
Use the DynamicImage component in your BlogPost.astro
(or whatever Astro component that needs to display images):
---
import type { CollectionEntry } from 'astro:content';
import BaseHead from '../components/BaseHead.astro';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import FormattedDate from '../components/FormattedDate.astro';
import DynamicImage from '../components/DynamicImage';
type Props = CollectionEntry<'compositions'>['data'];
const { title, description, pubDate, updatedDate, media } = Astro.props;
---
<html lang="en">
<head>
<BaseHead title={title} description={description} />
<style>
main {
width: calc(100% - 2em);
max-width: 100%;
margin: 0;
}
.hero-image {
width: 100%;
}
.hero-image img {
display: block;
margin: 0 auto;
border-radius: 12px;
box-shadow: var(--box-shadow);
}
.prose {
width: 720px;
max-width: calc(100% - 2em);
margin: auto;
padding: 1em;
color: rgb(var(--gray-dark));
}
.title {
margin-bottom: 1em;
padding: 1em 0;
text-align: center;
line-height: 1;
}
.title h1 {
margin: 0 0 0.5em 0;
}
.date {
margin-bottom: 0.5em;
color: rgb(var(--gray));
}
.last-updated-on {
font-style: italic;
}
</style>
</head>
<body>
<Header />
<main>
<article>
<div class="hero-image">
{media && <DynamicImage client:load media={media} id="post-hero-image" />}
</div>
<div class="prose">
<div class="title">
<div class="date">
<FormattedDate date={pubDate} />
{updatedDate && (
<div class="last-updated-on">
Last updated on <FormattedDate date={updatedDate} />
</div>
)}
</div>
<h1>{title}</h1>
<hr />
</div>
<slot />
</div>
</article>
</main>
<Footer />
</body>
</html>
One of the reasons we use a React component for the portion that actually handles dynamic image size selection rather than using an Astro component is because we ran into an issue where using client:load
with an Astro component would result in the following exception while building Astro 4.9.3:
[astro:build] The argument 'path' must be a string, Uint8Array, or URL without null bytes. Received '/Users/username/dev/project/\x00astro-entry:/Users/username/dev/project/src/components/tsconfig.json'