はじめに
Astroの画像インテグレーションは、公式Docsで記載されている@astrojs/imageか、Astro ImageToolsが有力な選択肢です。
その2つから私が選択したのは、Astro ImageToolsだったのですが、これをプロダクション用にそのまま使おうとすると少し手直しが必要だと感じたので、それをまとめたいと思います。
シンタックスハイライトがAstroに対応していないので、コードブロック内に下線が出てしまいますがご了承ください。
Astro ImageToolsを導入した理由: アートディレクション対応がしたい
私がデザインを頂いてコーディングする上では、よくPCとSPで画像の解像度・比率が異なることがよくあります。
PCだと横長画像だけど、SPだと縦長画像になる、ような感じです。
アートディレクション1に対応させる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
というプロパティが存在しており、まさに上記のようなHTMLを出力することができます。
---
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トレースをしたりなど、多彩な機能を備えています。
導入
では、導入手順を確認します。
yarn add astro-imagetools
追加できたら、Astroの設定ファイルにインテグレーションを登録します。
設定ファイルはデフォルトであれば、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を使用して簡単な画像変換と最適化を行うことができます。
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されています。
コンポーネントはいろいろとカスタマイズが可能で、使う度に設定することも可能ですし、グローバルに設定することも可能です。
---
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されています。
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の画像を表示します。
<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',
},
]}
/>
出力されるファイル一覧は以下の通りです。
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になりました。
<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
要素があることが確認できます。
<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 Standard2的にはアウトな書き方となっております。
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要素の中身は空っぽになります。
import { defineConfig } from 'astro-imagetools/config';
export default defineConfig({
placeholder: 'none',
});
これでビルドしてみるとこうなります。
<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を設定します。
import { defineConfig } from 'astro-imagetools/config';
export default defineConfig({
breakpoints: { count: 1 },
sizes: '',
});
これで完璧かと思いきや、これではsizes
が空になってしまうため、これはこれでエラーが出ます。
そのためこちらも①と同様、何かしらの方法でsizes
自体を削除するしかありません。
④ インデントがハチャメチャ
これは見てお気づきと思いますが、インデントがめちゃくちゃになっています。
気にならない人は気にならないかもしれませんが、私はこのまま納品するのは気が引けるので、フォーマットを行います。
私は、Astro HTML Beautifierを使用しました。
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だけを出力するように設定を追加しています。
import { defineConfig } from 'astro-imagetools/config';
export default defineConfig({
format: ['webp'],
breakpoints: { count: 1 },
sizes: '',
placeholder: 'none',
});
<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における画像まわりは今後も検討が必要そうです。
みんなアートディレクション使ってないの……?