🧑‍🚀

Astro にて最適化された画像を css で利用する方法

2024/02/11に公開
1

前口上

個人で運用しているサイトに Astro を導入しました。

当初 v2 で導入して、しばらくそのまま使っていたのですが、v3 以降画像最適化が標準で有効になったのもあって、v4 にアップグレードしました。

  1. pnpm dlx @astrojs/upgrade の実行
  2. pnpm add sharp の 実施
  3. public フォルダに置いてあった画像リソースを src 内に移動
  4. 最適化された画像を読み込むよう細部修正

パッケージマネージャに 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

Takuya OtaniTakuya Otani

const images で定義した css 変数、エディタ上で補完とかしてくれると嬉しいですが、そこまで対応していないです(Copilot とか使っているとなんとなく補完してくれたりはします)。