Astroの使いやすいアートディレクションコンポーネントを考える

Astroの画像の扱いについて

v3.0で組み込まれたastro:assetsにはImageコンポーネントがあり、srcディレクトリ内の画像であれば最適化して表示してくれる仕組みがあります。

最適化する場合、画像はそのままコンポーネントにパスを指定するのではなく、importして指定する必要があります。

公式ドキュメント - 画像

---
// astroコンポーネント
import { Image } from 'astro:assets';
import thumbnail from '../images/thumbnail.jpg'; // 相対パスで指定してimport
---
// Imageコンポーネントの出力
<Image src={thumbnail} alt="サムネイル" width="800" height="600"/>
<!-- 出力結果 -->
<img
  src="./_astro/thumbnail.hash.webp"
  width="800"
  height="600"
  decoding="async"
  loading="lazy"
  alt="サムネイル"
/>

出力結果のパスは相対リンクで出力されていますが、astro-relative-linksをインテグレーションで読み込んでいたためです。Astro公式では相対リンクはサポートされていないので、デフォルトでは絶対パスになります。

デフォルトではwebpに自動的に最適化して<img>タグで出力を返してくれます。またこの<Image>コンポーネントは多数のフォーマットに対応していて、より圧縮率の高いavif形式でも出力することができます。

// Imageコンポーネントの出力
<Image src={thumbnail} format="avif" alt="サムネイル" width="800" height="600"/>
<!-- 出力結果 -->
<img
  src="./_astro/thumbnail.hash.avif"
  width="800"
  height="600"
  decoding="async"
  loading="lazy"
  alt="サムネイル"
/>

jpgやpngなどの拡張子は任意で指定しない限り出力されません。とは言っても今はほぼ全てのモダンブラウザでwebpが対応しているため、あまり気にならない部分かなと思います。ただavifはEdgeなどが未対応のブラウザがあるので、Imageコンポーネントで指定するのは避けたほうがいいです。

レスポンシブイメージにも対応させたい

この<Image>コンポーネントは出力を<img>タグとしてしか出力できないため、<picture>タグで画像を切り分けるアートディレクションには対応しておらず、使いづらさがあります。


Astro v3.3でPictureコンポーネントが追加されました

ただ2023年12月このブログを書いている時点では画像のフォーマットによる切り分けるしかなく、media属性によりレスポンシブイメージやsrcset属性での解像度の切分けはできないようです。

ただ今後のアップデートで機能が追加される可能性はあるので公式リファレンスも見ておくと良いかもしれません。

また組込みのコンポーネントは先ほどの例のように1枚ずつ画像をimportする必要があることも記述が多くなってしまうため、避けたいところです。そこで公式のコンポーネントではないですが、Astro用のフレームワークとしてアートディレクションに対応したAstro-Imagetoolsというものを見つけたので、ちょっと使ってみたいと思います。

インストール

まずは下記のコマンドでインストールしてみます。

npm install astro-imagetools

インストールができたらastro.configファイルのインテグレーションに追加します。

import { astroImageTools } from "astro-imagetools"; // astro-imagetoolsのimport

export default {
  integrations: [astroImageTools], // インテグレーションに追加
};

これでAstro-Imagetoolsに組み込まれてるPictureコンポーネントを使えるようになります。

Astro-ImagetoolsのPictureコンポーネントを使ってみる

まず公式ドキュメントに従いimportで<Picture>コンポーネントを読み込み、<Picture>タグに必要な属性を書いていきます。

---
import { Picture } from "astro-imagetools/components";
---

<Picture
  src="/src/images/thumbnail.jpg"
  alt="サムネイル"
  artDirectives={[
    {
      src: "/src/images/thumbnail-sp.jpg",
      media: "(max-width: 767px)",
    },
  ]}
/>

artDirectivesは配列形式でmediaとsrc属性で読み込みたい画像とメディアクエリを記入していきます。画像に対してimportを挟まずに指定できるのはいいですね。上記のものを出力した結果が以下の内容です。

<!-- 出力結果 -->
<style>
.astro-imagetools-picture-hash {
	--opacity: 1;
	--z-index: 0;
}

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

.astro-imagetools-picture-hash::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-hash img {
	object-fit: cover;
	object-position: 50% 50%;
}

.astro-imagetools-picture-hash::after {
	background-size: cover;
	background-image: url("data:image/jpeg;base64,#######Base64文字列#######");
	background-position: 50% 50%;
}

@media (max-width: 767px) {
	.astro-imagetools-picture-hash img {
		object-fit: cover;
		object-position: 50% 50%;
	}

	.astro-imagetools-picture-hash::after {
		background-size: cover;
		background-image: url("data:image/jpeg;base64,#######Base64文字列#######");
		background-position: 50% 50%;
	}
}
</style>
<picture class="astro-imagetools-picture astro-imagetools-picture-hash" style="position: relative; display: inline-block; ; max-width: 100%; height: auto;">
	<source srcset="../_astro/thumbnail-sp@320w.hash.avif 320w, ../_astro/thumbnail-sp@607w.hash.avif 607w, ../_astro/thumbnail-sp@750w.hash.avif 750w" sizes="(min-width: 750px) 750px, 100vw" width="750" height="1300" type="image/avif" media="(max-width: 767px)" />
	<source srcset="../_astro/thumbnail-sp@320w.hash.webp 320w, ../_astro/thumbnail-sp@607w.hash.webp 607w, ../_astro/thumbnail-sp@750w.hash.webp 750w" sizes="(min-width: 750px) 750px, 100vw" width="750" height="1300" type="image/webp" media="(max-width: 767px)" />
	<source srcset="../_astro/thumbnail-sp@320w.hash.jpeg 320w, ../_astro/thumbnail-sp@607w.hash.jpeg 607w, ../_astro/thumbnail-sp@750w.hash.jpeg 750w" sizes="(min-width: 750px) 750px, 100vw" width="750" height="1300" type="image/jpeg" media="(max-width: 767px)" />
	<source srcset="../_astro/thumbnail@320w.hash.avif 320w, ../_astro/thumbnail@693w.hash.avif 693w, ../_astro/thumbnail@992w.hash.avif 992w, ../_astro/thumbnail@1216w.hash.avif 1216w, ../_astro/thumbnail@1365w.hash.avif 1365w, ../_astro/thumbnail@1440w.hash.avif 1440w" sizes="(min-width: 1440px) 1440px, 100vw" width="1440" height="1240" type="image/avif" />
	<source srcset="../_astro/thumbnail@320w.hash.webp 320w, ../_astro/thumbnail@693w.hash.webp 693w, ../_astro/thumbnail@992w.hash.webp 992w, ../_astro/thumbnail@1216w.hash.webp 1216w, ../_astro/thumbnail@1365w.hash.webp 1365w, ../_astro/thumbnail@1440w.hash.webp 1440w" sizes="(min-width: 1440px) 1440px, 100vw" width="1440" height="1240" type="image/webp" />
	<img src="../_astro/thumbnail@1440w.hash.jpeg" alt="サムネイル" srcset="../_astro/thumbnail@320w.hash.jpeg 320w, ../_astro/thumbnail@693w.hash.jpeg 693w, ../_astro/thumbnail@992w.hash.jpeg 992w, ../_astro/thumbnail@1216w.hash.jpeg 1216w, ../_astro/thumbnail@1365w.hash.jpeg 1365w, ../_astro/thumbnail@1440w.hash.jpeg 1440w" sizes="(min-width: 1440px) 1440px, 100vw" width="1440" height="1240" 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>

<picture>タグ以外にもクラス名やスタイルが出力されています。

デフォルトで設定されるplaceholder

Astro Imagetoolsの<Picture>コンポーネントはplaceholderが用意されていて、読み込みが終わるまでの間、低解像度画像を出力する「blurred」がデフォルトで表示される仕様になっています。読み込み時点で余白が生まれないのでUX的にも良さそうです。

公式デモ

ちょっと個人的に気になったのは、ただ<style>タグが<picture>タグの直前に挿入されていること、そして<img>のスタイルが子孫要素で指定されていることです。

<style>タグが<body>の中に出てきてしまうのはのWordPressなどのCMSでプラグインや出力されるものなら仕方ないなとも思えるのですが、Astroのような静的生成を主とするものでは何だかいただけないなと思いました。

子孫要素のスタイルについても詳細度がスタイルに影響しそうなのも困ります。

そのほかにもAstro-Imagetoolsのいけてないポイントがかなりあり、実装で使うのは難しいという結論に至りました。

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

個人的にはシンプルに実装したかったので、この辺をまるっと削除する方法を調べたんですが、公式ドキュメントでは見当たりませんでした。

一応placeholderをnoneにすればstyleの中身自体は出力されなくなるようです。

Astro Imagetoolsは高度な分、思想が強いのか個人的にちょっと使いづらいです。

Pictureの出力は自前で実装することにしました。

Astroには「astro:assets」の中にgetImage関数があり、これでカスタムコンポーネントを作成できるということでこれを使ってコンポーネントを作成してみました。

一応以下の条件を満たすように作ったつもりです。

  • 画像はimportを挟まずコンポーネント側だけで指定
  • media属性でレスポンシブイメージを実装できる
  • 複数の画像フォーマットと解像度の出力ができる

npmにパッケージで用意したので、興味がある方はインストールしてみてください。

https://www.npmjs.com/package/astro-simple-art-direction

npm install astro-simple-art-direction

使い方

インストールしたら、そのままコンポーネントを読み込んで使ってください。

以下はその例です。

import { Picture } from 'astro-simple-art-direction';

<Picture
  src={{
    file:"my-image.jpg",
    width:1000, 
    height: 800
  }} 
  artDirectives={[
    {
      media:"(max-width: 767px)",
      file:"my-image-sp.jpg",
      width:400, 
      height: 400
    }
  ]}
  alt="My image"
/>

出力結果

画像は「jpg」「png」デフォルトのフォーマットに加えて、avifとwebpを出力します。また解像度は自動で1xと2xまで出力されます。

<!-- Output Results -->
<picture>
  <source media="(max-width: 767px)" width="400" height="400" srcset="./_astro/my-image-sp.hash.avif 1x,./_astro/my-image-sp.hash.avif 2x" sizes="(max-width: 400px) 100vw, 400px" type="image/avif">
  <source media="(max-width: 767px)" width="400" height="400" srcset="./_astro/my-image-sp.hash.webp 1x,./_astro/my-image-sp.hash.webp 2x" sizes="(max-width: 400px) 100vw, 400px" type="image/webp">
  <source media="(max-width: 767px)" width="400" height="400" srcset="./_astro/my-image-sp.hash.jpg 1x,./_astro/my-image-sp.hash.jpg 2x" sizes="(max-width: 400px) 100vw, 400px">
  <source srcset="./_astro/my-image.hash.avif 1x,./_astro/my-image.hash.avif 2x" sizes="(max-width: 1000px) 100vw, 1000px" type="image/avif">
  <source srcset="./_astro/my-image.hash.webp 1x,./_astro/my-image.hash.webp 2x" sizes="(max-width: 1000px) 100vw, 1000px" type="image/webp">
  <img width="1000" height="800" src="./_astro/my-image.hash.jpg" srcset="./_astro/my-image.hash.jpg 1x,./_astro/my-image.hash.jpg 2x" sizes="(max-width: 1000px) 100vw, 1000px" loading="lazy" decoding="auto" alt="My image">
</picture>

src

このコンポーネントはsrcを設定するだけで使えるようにしています。このsrcはオブジェクト形式で指定します。srcオブジェクトのサイズを基準に複数の解像度の生成基準にするため、幅と高さの指定は必須にしています。

src={{
  file:"my-image.jpg", // パス不要でsrc/imagesディレクトリ内にあるファイルを参照
  width: 1000, // 画像の幅はNumber型で指定
  height: 800 // 画像の高さはNumber型で指定
}}

環境変数を使ってデフォルトの参照元「src/images」を変更することもできますが、ここでは割愛します。

artDirectives

artDirectivesはレスポンシブイメージのためのもので、先ほどのsrcオブジェクトにmedia属性を加えたオブジェクトを配列形式で指定します。media属性は配列順で出力されます。以下は使用例です。

  artDirectives={[
    {
      media:"(max-width: 1200px)",
      file:"my-image-medium.jpg",
      width:600, 
      height: 400
    },
    {
      media:"(max-width: 767px)",
      file:"my-image-small.jpg",
      width:400, 
      height: 400
    }
  ]}
<!-- Output Results -->
<source media="(max-width: 1200px)" width="600" height="400" srcset="./_astro/my-image-medium.hash.avif 1x,./_astro/my-image-medium.hash.avif 2x" sizes="(max-width: 600px) 100vw, 600px" type="image/avif">
<source media="(max-width: 1200px)" width="600" height="400" srcset="./_astro/my-image-medium.hash.webp 1x,./_astro/my-image-medium.hash.webp 2x" sizes="(max-width: 600px) 100vw, 600px" type="image/webp">
<source media="(max-width: 1200px)" width="600" height="400" srcset="./_astro/my-image-medium.hash.jpg 1x,./_astro/my-image-medium.hash.jpg 2x" sizes="(max-width: 600px) 100vw, 600px">
<source media="(max-width: 767px)" width="400" height="400" srcset="./_astro/my-image-small.hash.avif 1x,./_astro/my-image-small.hash.avif 2x" sizes="(max-width: 400px) 100vw, 400px" type="image/avif">
<source media="(max-width: 767px)" width="400" height="400" srcset="./_astro/my-image-small.hash.webp 1x,./_astro/my-image-small.hash.webp 2x" sizes="(max-width: 400px) 100vw, 400px" type="image/webp">
<source media="(max-width: 767px)" width="400" height="400" srcset="./_astro/my-image-small.hash.jpg 1x,./_astro/my-image-small.hash.jpg 2x" sizes="(max-width: 400px) 100vw, 400px">

alt, class, style, loading, decoding

その他、imgタグで使われる属性も追加したり変更が可能です。

背景用のコンポーネント

その他、背景コンポーネントも用意しました。スタイルはタグで出力せずにastroコンポーネントのスタイルで出力しているため、ビルド時に他のアセットと一緒に統合されるので使いやすいかと思います。astro-imagetoolsにあるようなplaceholderは対応していません。

BackgroundPicture

import { BackgroundPicture } from 'astro-simple-art-direction';

<BackgroundPicture
  TagName="section" 
  image={{ 
    src: {
      file:"my-image.jpg",
      width: 1000,
      height: 800
    }
  }}
/>
  <h1>astro-simple-art-direction</h1>
</BackgroundPicture>
<!-- Output Results -->
<section data-astro-hash class="bgp">
  <figure aria-hidden="true" data-astro-hash style="--imageWidth: 100%;--imageHeight: 100%;--attachment: cover;">
    <picture>
      <source srcset="./_astro/my-image.hash.avif 1x,./_astro/my-image.hash.avif 2x" sizes="(max-width: 500px) 100vw, 500px" type="image/avif">
      <source srcset="./_astro/my-image.hash.webp 1x,./_astro/my-image.hash.webp 2x" sizes="(max-width: 500px) 100vw, 500px" type="image/webp"> <img width="500" height="2000" src="./_astro/my-image.hash.jpg" srcset="./_astro/my-image.hash.jpg 1x,./_astro/my-image.hash.jpg 2x" sizes="(max-width: 500px) 100vw, 500px" loading="lazy" decoding="auto" alt="">
    </picture>
  </figure>
  <div class="bgp-inner" data-astro-hash style="--imageWidth: 100%;--imageHeight: 100%;--attachment: cover;">
    <h1>astro-simple-art-direction</h1>
  </div>
</section>

BackgroundImage

import { BackgroundImage } from 'astro-simple-art-direction';

<BackgroundImage TagName="section" image={ {src:{file:"my-image.jpg", width:500, height:2000}} }>
  <h1>astro-simple-art-direction</h1>
</BackgroundImage>
<!-- Output Results -->
<section style="background-image: url(./_astro/my-image.hash.jpg);">
  <h1>astro-simple-art-direction</h1>
</section>

最後に

Astroで静的サイトを作成するときは画像の出力については当面このコンポーネントを使っていこうと思います。ただ今はアップデートの変化が激しいため、また組込みのコンポーネントが汎用性があるものになればそちらに切り替えるかしれません。

出力コードの解説は気が向いたらまた別の機会に書こうと思います。