Astroの画像インテグレーションAstro ImageToolsを使うときに注意すること

## はじめに

Astroの画像インテグレーションは、公式Docsで記載されている[@astrojs/image](https://docs.astro.build/ja/guides/integrations-guide/image/)か、[Astro ImageTools](https://github.com/RafidMuhymin/astro-imagetools)が有力な選択肢です。

その２つから私が選択したのは、Astro ImageToolsだったのですが、これをプロダクション用にそのまま使おうとすると少し手直しが必要だと感じたので、それをまとめたいと思います。

:::note warn
シンタックスハイライトがAstroに対応していないので、コードブロック内に下線が出てしまいますがご了承ください。
:::

## Astro ImageToolsを導入した理由: アートディレクション対応がしたい

私がデザインを頂いてコーディングする上では、よく**PCとSPで画像の解像度・比率が異なる**ことがよくあります。
PCだと横長画像だけど、SPだと縦長画像になる、ような感じです。
アートディレクション[^1]に対応させるHTMLを書くとこうなります。

[^1]:異なるトリミングの画像を表示サイズに合わせて変更すること

```html:通常のHTML
<picture>
　　<source srcset="/assets/img/dummy_pc.png" media="(min-width: 768px)">
　　<img src="/assets/img/dummy_sp.png" alt="">
</picture>
```

@astrojs/imageだと上記のように、レスポンシブ画像を2枚指定して出力することはできません。（たぶん。ソースレベルまでは見てないが、少なくともドキュメントには書いてない）

そこで、Astro ImageToolsの出番で、こちらには[`artDirectives`](https://astro-imagetools-docs.vercel.app/en/components/Picture#artdirectives)というプロパティが存在しており、まさに上記のようなHTMLを出力することができます。

```html:PictureコンポーネントでartDirectivesプロパティを使う
---
import { Picture } from "astro-imagetools/components";
---

<Picture
  src="/src/assets/img/dummy_sm.png"
  alt=""
  artDirectives={[
    {
      media: '(min-width: 768px)',
      src: '/src/assets/img/dummy_lg.png',
    },
  ]}
/>
```

## Astro ImageToolsの概要

Astro ImageToolsは、Astroで画像最適化するためのインテグレーションです。
`<img>`や`<picture>`の通常の画像に加えて、背景画像の最適化もサポートしています。

lazyloadはもちろん、ブレイクポイントの自動計算、ロード完了までのプレースホルダー画像を設定したり、SVGトレースをしたりなど、多彩な機能を備えています。

## 導入

では、導入手順を確認します。

```bash
yarn add astro-imagetools
```

追加できたら、Astroの設定ファイルにインテグレーションを登録します。
設定ファイルはデフォルトであれば、`astro.config.mjs`になります。

```js:astro.config.mjs
import { defineConfig } from 'astro/config';
import { astroImageTools } from 'astro-imagetools';

export default defineConfig({
  integrations: [astroImageTools],
});
```

## 使い方

導入が完了すると、Viteプラグイン、コンポーネント、APIが利用可能になります。

### Viteプラグイン

Astro ImageToolsには、Viteプラグインが付属しており、後述のコンポーネントとAPIは、画像の変換と最適化を行うために内部的にViteプラグインを使用します。

したがって、提供されたコンポーネントやAPIを使用することができない場合、ESモジュールのimportを使用して簡単な画像変換と最適化を行うことができます。

```jsx
import React from 'react';
import src from '../images/image.jpg';

export default function ReactImage() {
  return <img src={src} />;
}
```

### コンポーネント

さてこちらが本命です。

`Img`、`Picture`、`BackgroundImage`、`BackgroundPicture`、`ImageSupportDetection`といったコンポーネント群が、`astro-imagetools/components`からnamed exportされています。

コンポーネントはいろいろとカスタマイズが可能で、使う度に設定することも可能ですし、グローバルに設定することも可能です。

```html:Imgコンポーネント使用例
---
import { Img } from 'astro-imagetools/components';
---

<Img src="/img/image.jpg" alt="image" />
```

### API

こちらはプログラム上でHTMLを生成するためのAPI群です。

`renderImg`、`renderPicture`、`renderBackgroundImage`、`renderBackgroundPicture`が`astro-imagetools/api`からnamed exportされています。

```js:renderImg API使用例
import { renderImg } from 'astro-imagetools/api';

const { link, style, img } = await renderImg({
  src: '/img/image.jpg',
  alt: 'image',
});
```

## ビルドしてみる

それでは、早速ですが求めているものをちゃんと出力してくれるかビルドを行ってみます。

公式のガイド通りにAstroプロジェクトを作成して、Astro ImageToolsを導入した状態で、ImgコンポーネントとPictureコンポーネントを使って画像を出力してみます。

画面幅が768px以上の場合1024x768の画像を、それより小さい場合は500x500の画像を表示します。

```html:ビルド元のastroファイル（抜粋）
<Img src="/src/assets/img/image_lg.jpg" alt="image" />
<Picture
  src="/src/assets/img/image_sm.jpg"
  alt=""
  artDirectives={[
    {
      media: '(min-width: 768px)',
      src: '/src/assets/img/image_lg.jpg',
    },
  ]}
/>
```

出力されるファイル一覧は以下の通りです。

```bash:tree
dist/
├── _astro
│   ├── image_lg@1024w.44e8bace.webp
│   ├── image_lg@1024w.d1f2ff39.jpeg
│   ├── image_lg@1024w.edd808c0.avif
│   ├── image_lg@320w.20cccab6.avif
│   ├── image_lg@320w.8da65973.webp
│   ├── image_lg@320w.b3aeb44e.jpeg
│   ├── image_lg@672w.10f8ccc8.webp
│   ├── image_lg@672w.15ea8aba.jpeg
│   ├── image_lg@672w.c53046fc.avif
│   ├── image_lg@907w.3c527de1.avif
│   ├── image_lg@907w.7591f11c.jpeg
│   ├── image_lg@907w.d8da83c0.webp
│   ├── image_sm@320w.44368211.jpeg
│   ├── image_sm@320w.6732ecec.avif
│   ├── image_sm@320w.8c12440e.webp
│   ├── image_sm@500w.8661ef7c.jpeg
│   ├── image_sm@500w.afd14d43.avif
│   ├── image_sm@500w.f371b3af.webp
│   └── index.74874213.css
└── index.html
```

また、Astro ImageToolsで書いた部分はこのようなHTMLになりました。

```html:ビルドされたHTML（mainタグの中）
<style >
  .astro-imagetools-img-CD354CD3 {
    object-fit: cover;
    object-position: 50% 50%;
  }

  .astro-imagetools-img-CD354CD3 {
    background-size: cover;
    background-image: url("data:image/jpeg;base64,/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAPABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAYEBwj/xAAiEAACAQMDBQEAAAAAAAAAAAABAgMABAUREiEGB0FRYdH/xAAVAQEBAAAAAAAAAAAAAAAAAAACAf/EABoRAAMAAwEAAAAAAAAAAAAAAAABEQMEEiH/2gAMAwEAAhEDEQA/AGbsZ1OkWIuLPKXiR21uqGBWAGhJYtz+1aq9Q4cnaL2Pd681h+XJxi2EbXCvoNQI96n15Gnuo4yrR3O63WdFQlgDOWIB+6VFtRSAeL20aMtOpy98QeDO5HP2ilx7x5pZJFBIdiaKHQof/9k=");
    background-position: 50% 50%;
  }
</style><img

src="/_astro/image_lg@1024w.d1f2ff39.jpeg"
alt="image"
srcset="/_astro/image_lg@320w.b3aeb44e.jpeg 320w, /_astro/image_lg@672w.15ea8aba.jpeg 672w, /_astro/image_lg@907w.7591f11c.jpeg 907w, /_astro/image_lg@1024w.d1f2ff39.jpeg 1024w"
sizes="(min-width: 1024px) 1024px, 100vw"
width="1024"
height="768"
loading="lazy"
decoding="async"
class="astro-imagetools-img astro-imagetools-img-CD354CD3"
style="display: inline-block; overflow: hidden; vertical-align: middle; ; max-width: 100%; height: auto;"
onload=""
/>
<style >


.astro-imagetools-picture-141BAA34 {
--opacity: 1;
--z-index: 0;
}


      .astro-imagetools-picture-141BAA34 img {
        z-index: 1;
        position: relative;
      }
    

.astro-imagetools-picture-141BAA34::after {
inset: 0;
content: "";
left: 0;
width: 100%;
height: 100%;
position: absolute;
pointer-events: none;
transition: opacity 1s;
opacity: var(--opacity);
z-index: var(--z-index);
}

  .astro-imagetools-picture-141BAA34 img {
    object-fit: cover;
    object-position: 50% 50%;
  }

  .astro-imagetools-picture-141BAA34::after {
    background-size: cover;
    background-image: url("data:image/jpeg;base64,/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAUABQDASIAAhEBAxEB/8QAGQABAAMBAQAAAAAAAAAAAAAAAAMGBwQI/8QAKhAAAgEDAwIDCQAAAAAAAAAAAQIDAAQRBQYSEyEHFFEjMTNSYXKBkZL/xAAXAQADAQAAAAAAAAAAAAAAAAABAgME/8QAHhEAAgIABwAAAAAAAAAAAAAAAAECERITFCFBgfD/2gAMAwEAAhEDEQA/ANr2Bq8uubXtL+9aLryF1biAoPFiAcfirKvT+dP6FePLbeutWFrFbaQ6NADhcHJDFs4PeppfErdFvLifpxsGPJVAwBkfWn1UKp2Ry58Je6Lp41so37cAEfBi9320rPNc3DLrN7526wJXUKQDkdiR6mlDGnuh6oz+bUZbjPOO3XJz7OJVx+hUaRq10EPYH0pSsKLHVE8nDiZHIUlRn0pSlUsU/9k=");
    background-position: 50% 50%;
  }
@media (min-width: 768px) { 
  .astro-imagetools-picture-141BAA34 img {
    object-fit: cover;
    object-position: 50% 50%;
  }

  .astro-imagetools-picture-141BAA34::after {
    background-size: cover;
    background-image: url("data:image/jpeg;base64,/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAPABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAYEBwj/xAAiEAACAQMDBQEAAAAAAAAAAAABAgMABAUREiEGB0FRYdH/xAAVAQEBAAAAAAAAAAAAAAAAAAACAf/EABoRAAMAAwEAAAAAAAAAAAAAAAABEQMEEiH/2gAMAwEAAhEDEQA/AGbsZ1OkWIuLPKXiR21uqGBWAGhJYtz+1aq9Q4cnaL2Pd681h+XJxi2EbXCvoNQI96n15Gnuo4yrR3O63WdFQlgDOWIB+6VFtRSAeL20aMtOpy98QeDO5HP2ilx7x5pZJFBIdiaKHQof/9k=");
    background-position: 50% 50%;
  }
 }</style><picture

class="astro-imagetools-picture astro-imagetools-picture-141BAA34"
style="position: relative; display: inline-block; ; max-width: 100%; height: auto;"
><source
      srcset="/_astro/image_lg@320w.20cccab6.avif 320w, /_astro/image_lg@672w.c53046fc.avif 672w, /_astro/image_lg@907w.3c527de1.avif 907w, /_astro/image_lg@1024w.edd808c0.avif 1024w"
      sizes="(min-width: 1024px) 1024px, 100vw"
      width="1024"
      height="768"
      type="image/avif"
      media="(min-width: 768px)"
    />
<source
      srcset="/_astro/image_lg@320w.8da65973.webp 320w, /_astro/image_lg@672w.10f8ccc8.webp 672w, /_astro/image_lg@907w.d8da83c0.webp 907w, /_astro/image_lg@1024w.44e8bace.webp 1024w"
      sizes="(min-width: 1024px) 1024px, 100vw"
      width="1024"
      height="768"
      type="image/webp"
      media="(min-width: 768px)"
    />
<source
      srcset="/_astro/image_lg@320w.b3aeb44e.jpeg 320w, /_astro/image_lg@672w.15ea8aba.jpeg 672w, /_astro/image_lg@907w.7591f11c.jpeg 907w, /_astro/image_lg@1024w.d1f2ff39.jpeg 1024w"
      sizes="(min-width: 1024px) 1024px, 100vw"
      width="1024"
      height="768"
      type="image/jpeg"
      media="(min-width: 768px)"
    />
<source
      srcset="/_astro/image_sm@320w.6732ecec.avif 320w, /_astro/image_sm@500w.afd14d43.avif 500w"
      sizes="(min-width: 500px) 500px, 100vw"
      width="500"
      height="500"
      type="image/avif"
      
    />
<source
      srcset="/_astro/image_sm@320w.8c12440e.webp 320w, /_astro/image_sm@500w.f371b3af.webp 500w"
      sizes="(min-width: 500px) 500px, 100vw"
      width="500"
      height="500"
      type="image/webp"
      
    />
<img

src="/_astro/image_sm@500w.8661ef7c.jpeg"
alt=""
srcset="/_astro/image_sm@320w.44368211.jpeg 320w, /_astro/image_sm@500w.8661ef7c.jpeg 500w"
sizes="(min-width: 500px) 500px, 100vw"
width="500"
height="500"
loading="lazy"
decoding="async"
class="astro-imagetools-img"
style="display: inline-block; overflow: hidden; vertical-align: middle; ; max-width: 100%; height: auto;"
onload="parentElement.style.setProperty('--z-index', 1); parentElement.style.setProperty('--opacity', 0);"
/>
</picture>
```

すごい（小学生並みの感想）

## 注意すること

### ① 出力した要素内にstyle要素が出力される

`Img`コンポーネントの出力された部分を抜き出して見てみると、`img`要素の上の`style`要素があることが確認できます。

```html:style要素が出力されている
<style >
  .astro-imagetools-img-CD354CD3 {
    object-fit: cover;
    object-position: 50% 50%;
  }

  .astro-imagetools-img-CD354CD3 {
    background-size: cover;
    background-image: url("data:image/jpeg;base64,/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAPABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAYEBwj/xAAiEAACAQMDBQEAAAAAAAAAAAABAgMABAUREiEGB0FRYdH/xAAVAQEBAAAAAAAAAAAAAAAAAAACAf/EABoRAAMAAwEAAAAAAAAAAAAAAAABEQMEEiH/2gAMAwEAAhEDEQA/AGbsZ1OkWIuLPKXiR21uqGBWAGhJYtz+1aq9Q4cnaL2Pd681h+XJxi2EbXCvoNQI96n15Gnuo4yrR3O63WdFQlgDOWIB+6VFtRSAeL20aMtOpy98QeDO5HP2ilx7x5pZJFBIdiaKHQof/9k=");
    background-position: 50% 50%;
  }
</style>
```

ここでは`img`要素で表示する`object-fit`と`object-position`の設定と、画像表示が完了するまでの間表示するプレースホルダー画像の設定が記述されています。

`background-image`に指定されているのは、`placeholder`オプションのデフォルト値である`blurred`によって生成される指定した画像の低解像度のものになります。

しかしこちら、[Living Standard](https://html.spec.whatwg.org/multipage/semantics.html#the-style-element)[^2]的にはアウトな書き方となっております。

[^2]: HTML Living Standard: WHATWGが策定するHTMLの標準仕様のこと

> Contexts in which this element can be used:
> Where metadata content is expected.
> In a noscript element that is a child of a head element.

style要素は、メタデータが書かれるところか、head要素内のnoscript要素内ならOKと決められています。

このAstro ImageToolsで出力されたstyle要素は、バリバリbodyの中に入っているため、このLiving Standardのルールからは逸脱しています。

よって、その『ルール』に準拠するには、style要素はまるごと削除しないといけないということになります。

では、どうやって削除するかというと、**`placeholder`に`none`を指定**します。

これだけで後述の **`objectFit`を指定していても**style要素の中身は空っぽになります。

```javascript:astro-imagetools.config.mjs(全体に適用)
import { defineConfig } from 'astro-imagetools/config';

export default defineConfig({
  placeholder: 'none',
});
```

これでビルドしてみるとこうなります。

```html
    <style ></style><img
    
    src="/_astro/image_lg@1024w.d1f2ff39.jpeg"
    alt="image"
    srcset="/_astro/image_lg@320w.b3aeb44e.jpeg 320w, /_astro/image_lg@672w.15ea8aba.jpeg 672w, /_astro/image_lg@907w.7591f11c.jpeg 907w, /_astro/image_lg@1024w.d1f2ff39.jpeg 1024w"
    sizes="(min-width: 1024px) 1024px, 100vw"
    width="1024"
    height="768"
    loading="lazy"
    decoding="async"
    class="astro-imagetools-img astro-imagetools-img-5217724D"
    style="display: inline-block; overflow: hidden; vertical-align: middle; ; max-width: 100%; height: auto;"
    onload=""
  />
```

style要素残っている。。。。

ということで、完全に消すにはオプションではどうにもならず、npm-scriptsかなんかで消すしかありません。あしからず。

### ② objectFit

objectFitオプションには、CSSの`object-fit`に指定する値と同じものを指定できます。
つまり、`fill` | `contain` | `cover` | `none` | `scale-down`を指定できるということです。

しかしながら、ここで指定した値は`background`に指定したプレースホルダーの`background-size`にも使用されるため、`fill`、`scale-down`、`none`を指定すると本来`background-size`に指定できない値が指定されてしまい、エラーになってしまいます。

### ③ breakpointを1つにしたとき

Astro ImageToolsでは渡された画像に応じてbreakpointsが自動で計算され、横幅に応じた最適な画像を生成し、sizes属性も指定してくれます。

しかし、このbreakpointsを1つに設定すると、Nu HTML Checkerでエラーが吐かれます。
「**`sizes`属性には値が入っているのに、`srcset`にはwidthが指定されていない**」と怒られるのです。

breakpointが1つならもはやsizes属性はいらんやん、ということでconfigを設定します。

```js:astro-imagetools.config.mjs
import { defineConfig } from 'astro-imagetools/config';

export default defineConfig({
  breakpoints: { count: 1 },
  sizes: '',
});
```

これで完璧かと思いきや、これでは`sizes`が空になってしまうため、これはこれでエラーが出ます。

そのためこちらも①と同様、何かしらの方法で`sizes`自体を削除するしかありません。

### ④ インデントがハチャメチャ

これは見てお気づきと思いますが、インデントがめちゃくちゃになっています。
気にならない人は気にならないかもしれませんが、私はこのまま納品するのは気が引けるので、フォーマットを行います。

私は、[Astro HTML Beautifier](https://github.com/bangkitdev/astro-html-beautifier)を使用しました。

```js:astro.config.mjs
import { defineConfig } from 'astro/config';
import { astroImageTools } from 'astro-imagetools';
import htmlBeautifier from 'astro-html-beautifier';

export default defineConfig({
  integrations: [
    astroImageTools,
    htmlBeautifier({
      indent_size: 2,
      indent_char: ' ',
      max_preserve_newlines: 1,
    }),
  ],
});

```

### ここまでを踏まえてビルド

※webpだけを出力するように設定を追加しています。

```js:astro-imagetools.config.mjs
import { defineConfig } from 'astro-imagetools/config';

export default defineConfig({
  format: ['webp'],
  breakpoints: { count: 1 },
  sizes: '',
  placeholder: 'none',
});

```

```html:mainタグの中
<main class="astro-J7PV25F6">
  <img src="/_astro/image_lg@1024w.d1f2ff3" alt="image" srcset="/_astro/image_lg@1024w.d1f2ff39.jpeg" width="1024" height="768" loading="lazy" decoding="async" class="astro-imagetools-img astro-imagetools-img-794EC5E1" style="display: inline-block; overflow: hidden; vertical-align: middle; ; max-width: 100%; height: auto;" onload="" />
  <picture class="astro-imagetools-picture astro-imagetools-picture-1924158B" style="position: relative; display: inline-block; ; max-width: 100%; height: auto;">
    <source srcset="/_astro/image_lg@1024w.44e8bace.webp" width="1024" height="768" type="image/webp" media="(min-width: 768px)" />
    <source srcset="/_astro/image_lg@1024w.d1f2ff39.jpeg" width="1024" height="768" type="image/jpeg" media="(min-width: 768px)" />
    <source srcset="/_astro/image_sm@500w.f371b3af.webp" width="500" height="500" type="image/webp" />
    <img src="/_astro/image_sm@500w.8661ef7c" alt="" srcset="/_astro/image_sm@500w.8661ef7c.jpeg" width="500" height="500" loading="lazy" decoding="async" class="astro-imagetools-img" style="display: inline-block; overflow: hidden; vertical-align: middle; ; max-width: 100%; height: auto;" onload="parentElement.style.setProperty('--z-index', 1); parentElement.style.setProperty('--opacity', 0);" />
  </picture>
</main>
```

## 余談

Astro v2.1から試験的機能として、アセット最適化が利用可能となりました。

この機能はいずれ@astrojs/imageと置き換わるらしいのですが、こちらではやはりpictureタグによる画像の出し分けができないため、今回は話題にあげませんでした。

単純に画像をぺたっと貼りたいだけであれば有用だと思うので、使ってみると良いと思います。

## 最後に

Astro ImageToolsを使ってビルドしたときに、そのままだとクオリティとして不十分だよということをお伝えしました。

使っていて思いましたが、widthとheightとったりしたいだけならもはやViteプラグインを使用した方が良いような気がしました。（身も蓋もない感想）

@astrojs/imageにはそもそもアートディレクションがないし、Astroにおける画像まわりは今後も検討が必要そうです。

みんなアートディレクション使ってないの……？