Astro にて最適化された画像を css で利用する方法
前口上
個人で運用しているサイトに Astro を導入しました。
当初 v2 で導入して、しばらくそのまま使っていたのですが、v3 以降画像最適化が標準で有効になったのもあって、v4 にアップグレードしました。
-
pnpm dlx @astrojs/upgrade
の実行 -
pnpm add sharp
の 実施 -
public
フォルダに置いてあった画像リソースをsrc
内に移動 - 最適化された画像を読み込むよう細部修正
パッケージマネージャに pnpm
を使っていたので、sharp は手動でプロジェクトに追加する必要があるとのこと。
私の環境では、ただ単に pnpm add sharp
ではダメで、一度 node_modules
を消して、 pnpm install
を実施しないときちんと sharp を認識してくれませんでした。アップグレードでハマったところはこのぐらいでしょうか。
最適化された画像を css で参照する
<img />
タグを使っていたところは、Astro が提供している <Image />
に置き換えることで最適化された画像に差し替わってくれます。
一方、 css では、パスを指定して参照する必要があり、ビルド時に最適化画像を生成する Astro では、そのままでは利用できません。
---
import sampleImage from 'イメージパス?url';
---
{/* 注意!! 以下のコードは動作しません */}
<style>
main {
background-image: url({sampleImage});
}
</style>
とかできれば楽なんですが、 <style>
内では、コンポーネントスクリプト側で定義された定数・変数の参照が できません 。
ただ、 define:vars
属性には、コンポーネントスクリプトで定義された定数・変数を渡すことが可能です。
---
import sampleImage from 'イメージパス?url';
---
<style define:vars={{ sample: `url(${sampleImage})` }}>
main {
background-image: var(--sample);
}
</style>
import 時に ?url
を付与すると、最適される前のソース画像の path が渡されるだけなので、これをきちんと最適化された画像にするには、
---
import sampleImageSrc from 'イメージパス';
const sampleImage = await getImage({ src: sampleImageSrc, format: 'webp' });
---
<style define:vars={{ sample: `url(${sampleImage.src})` }}>
main {
background-image: var(--sample);
}
</style>
のように getImage
関数を通して取得します。
css で利用する画像をまとめて定義する
原理さえ分かれば、 css で最適化された画像を参照することは、そんなに難しくなく、 @media
クエリを利用した画像の切り替えを css で実施できます。
私の場合、以下のようなユーティリティスクリプトを用意して、css で参照される画像について --imgurl
という接頭子がついた css 変数で管理しました。
// css で利用する画像を import する
import imageFooterBlue from '../assets/footer-blue.png';
import imageFooterGreen from '../assets/footer-green.png';
import imageHeaderBlue from '../assets/header-blue.png';
import imageHeaderGreen from '../assets/header-green.png';
// ... それを名前を付けて参照できるようにする。
// `header_desktop` => `--imgurl-header-desktop` で参照
const imageMap = {
header_desktop: imageHeaderBlue, // --imgurl-header-desktop
header_mobile: imageHeaderGreen, // --imgurl-header-mobile
footer_desktop: imageFooterBlue, // --imgurl-footer-desktop
footer_mobile: imageFooterGreen, // --imgurl-footer-mobile
} as Record<string, ImageMetadata>;
export const imagePath = async (src: ImageMetadata, type = 'webp') => (await getImage({ src, format: type })).src;
export const images = await (async () => (
await Promise.all(
Object.entries(imageMap)
.map(async ([key, src]) => [key, await imagePath(src)])
)
).reduce((acc, [key, path]) => (
{ ...acc, [`imgurl-${key.replace(/_/g, '-')}`]: `url(${path})` }),
{},
))();
コンポーネントスクリプトで定義した変数をトップレベルの astro テンプレートで呼び出します。
import { images } from 'styles/images';
// ... 省略 ...
---
<!doctype html>
<html lang={lang}>
<head>
<Head {...rest} />
<style is:global define:vars={{ ...images }}></style>
</head>
<slot />
</html>
念のため is:global
指定していますが、無指定でも define:vars
で渡した css 変数は全域で参照できます。
後は css 変数を必要に応じて参照します。
background-image: var(--imgurl-header-mobile);
@media screen and (min-width: 768px) {
background-image: var(--imgurl-header-desktop);
}
サンプルコードは https://github.com/takkyun/astro-sample にあり、実際にデプロイしたものは https://astro-sample-ec5.pages.dev/ にあります。
ウィンドウ幅に応じて適用する背景画像を変えています。
Discussion
const images
で定義した css 変数、エディタ上で補完とかしてくれると嬉しいですが、そこまで対応していないです(Copilot とか使っているとなんとなく補完してくれたりはします)。