本サイトのフレームワークには Astro を使用していますが、2024 年 12 月 3 日に Astro 5.0 がリリースされたので、v4 から v5 へのアップグレードを実施しました。

本記事では、Astro v5 で追加された主要な機能の説明と、アップグレード作業において、特に気をつける点を取り上げていきます。

Astro 公式のプレスリリースは以下です。

ハイライトとしては、以下の 6 項目が挙げられています。

  • Content Layer
  • Server Islands
  • 事前レンダリングの簡素化
  • 型安全な環境変数(astro:env
  • Vite 6
  • 実験的な機能

Vite 6 については割愛しますが、それ以外の機能を簡単に説明します。

今回のバージョンにおける最大の特徴は、この Content Layer API の導入が挙げらます。

これまでは、Content Collections によって、ブログ記事などのコンテンツを型安全に管理することができましたが、Content Layer の導入によって、さらに自由かつ柔軟にデータを扱うことができるようになります。

以下の図が一目瞭然ですが、これまでのようにローカルファイルのデータ(mdmdxjson など)に加えて、外部で管理している CMS や API のデータをローダで取得して、必要に応じて加工して使用することができるようになります。

Content Layer の図。Loader を介してローカルファイルや、CMS、API のデータを取得し、Astro ファイルに読み込んでいる。
Astro 5.0 より引用

例えば、ヘッドレス CMS からデータを取得したり、直接オープンデータ API からデータを取得して、ウェブページを構築するといったことが可能になります。

カスタムローダを経由して、さまざまなサービスのデータと連携することで、可能性はますます広がるので、これから使い道を考えていくのが楽しみな機能です。

Server Island は、ページのパフォーマンスを犠牲にすることなく、動的なコンテンツがレンダリングされる仕組みです。

Server Islands の図。静的なコンテンツと、サーバから取得される動的なコンテンツのアイランドが独立している。
Astro 5.0 より引用

まず、ページ内の静的なコンテンツがレンダリングされ、動的なコンテンツやユーザごとにパーソナライズされたコンテンツは、領域がプレースホルダーで確保されたうえで、準備ができた段階で差し替えられます。

これらのアイランドは独立しているため、ページ内に複数の動的なコンテンツが存在する場合にも、他のコンテンツによる遅延の影響を抑えられるようです。

事前レンダリングの簡素化

見出し「事前レンダリングの簡素化」

Astro 2.0 にてハイブリッドレンダリングが導入されましたが、Astro の開発チームは、機能が追加されるとともに、ケースごとの説明や文書化が重荷になるという問題を抱えていたようです。

また、ユーザが必要以上に気軽に SSR(server-side rendering)を使用することで、パフォーマンスが犠牲になる傾向が見受けられたため、この仕組みが見直されることになりました。

Astro 5.0 からは、デフォルトではすべて静的ページとして事前レンダリングされるようになります。SSR でコンテンツを追加したい場合には、アダプタを追加するだけで対応できるとのことで、全体的な仕組みがシンプルでわかりやすくなりました。

型安全な環境変数(astro:env

見出し「型安全な環境変数(」

環境変数(astro:env)に、型安全なスキーマが定義できるようになりました。

型(stringnumberbooleanenum)に加えて、使用されるのはクライアントかサーバか、公開か秘密か、任意であるか、そしてデフォルト値などを指定できます。

実験的な機能(Experimental)は以下の 3 点です。

  • 画像の切り抜きサポート
  • レスポンシブイメージのレイアウト(srcsetsizes 属性の自動生成)
  • SVG コンポーネント

最初の 2 つはレスポンシブイメージに関するもので、<Image /><Picture /> の 2 つのコンポーネントで使用できます。特に srcsetsizes 属性の自動生成については待望の機能です。

簡単に試した限りでは、srcsetsizes が自動的に出し分けられ、それに合わせてリサイズされた画像が書き出されました。また、fit 属性の記述によって画像がトリミングされ、対応したスタイルが適用されることも確認できました。

まだ実験的な機能なので、実際のプロダクトに使用するには慎重さが求められますが、今後、レスポンシブイメージを指定するわずらわしさを軽減できるかもしれません。

最後の SVG コンポーネントは、読み込んだ SVG ファイルをインライン SVG として出力してくれるもので、シンプルですがこちらも待望の機能です。

本サイトではこれまで、Vite の raw クエリを使用して SVG のコードを読み込んで、set:html ディレクティブを使用して直接 HTML を出力する方法を採用していました1

raw クエリと set:html ディレクティブを使用した方法
---
const { default: menu } = await import('../assets/img/btn-menu.svg?raw');
---

<Fragment set:html={menu} />

これを SVG コンポーネントに置き換えると、以下のようにシンプルな記述になるので、安定版になったタイミングでこちらへの切り替えを検討したいと考えています。

SVG コンポーネント使用した方法
---
// Experimental
const SVGMenu = from '../assets/img/btn-menu.svg';
---

<SVGMenu />

Astro v5 へのアップグレード

見出し「Astro v5 へのアップグレード」

Astro v5 へのアップグレードは以下のガイドに従い進めます。

本サイトにおいては、v3 から v4 へのアップグレードはスムーズに移行できたのですが、今回の v4 から v5 への移行では既存のコードへの影響がいくつかあり、個別の調整が必要でした。

アップグレード後、調整が不十分なまま不用意に公開してしまったため、一時的にスタイルが崩れる、機能が応答しない、RSS のリンク先が 404 Not Found になるといった状況がありました 🙇‍♂️

以下、特に影響のあった変更をいくつか取り上げます。

レガシー: Content Collections API

見出し「レガシー: Content Collections API」

Astro v5 で導入された Content Layer API にあわせて、コレクションの記述が変更になりました。legacy フラグを有効にすることで、サポート期間内であればこれまでの記述でも使用できるようですが2、Content Layer を試す目的に加え、パフォーマンス面の向上も期待できるので新しい記述に変更しました。

ただ、変更箇所が多く、見落としによって記事ページへのリンクが undefined になってしまうといったことがありました。そのため、時間的な余裕があるときに対応したほうがよさそうです。

具体的な進め方は、以下の記事の「Step-by-step instructions to update a collection(コレクションをアップデートする手順)」などを参考にしてください。

変更: <script> タグの位置

見出し「変更: 」

これまで、Astro ファイルに記述した <script> 要素は、出力される HTML の <head> 要素の下に移動していましたが、Astro v5 ではこの移動がなくなり、コンポーネントの直後に出力されるようになりました。

この仕様変更により、連続して出現するコンポーネントの CSS に影響がありました。

例えば、以下のようなコンポーネントがあるとします。<style> には隣接セレクタを使用します。なお、<script> はタグを出力させるためのもので、コードの内容に意味はありません。

src/components/Num.astro
---
const { num } = Astro.props;
---

<div>
  {num}
</div>

<style>
/* 隣接セレクタ */
div + div {
  margin-block-start: 2em;
}
</style>

<script>
const foo = 'bar';
</script>

このコンポーネントを連続で読み込みます。

コンポーネントの読み込み
---
import Num from './components/Num.astro';
---

<!-- コンポーネントを連続で 5 回呼び出し -->
{[Array.from({ length: 5 }).map((_, index) => (
  <Num num={index + 1}>
))]}

このとき、出力される HTML と CSS は以下のようになります(簡略化しています)。

最初に呼び出されたコンポーネントの直後に <script> タグが挿入されることで、CSS の隣接セレクタが期待どおりに適用されなくなります。

コンポーネントの間に <script> 要素が挿入される
<div>1</div>
<script type="module" src="..."></script>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>

<!-- 隣接セレクタが一致しない「2」のマージンが反映されない -->
<style>
div + div {
  margin-block-start: 2em;
}
</style>

このように、隣接セレクタや :nth-child() のように、要素の出現順が前提となる疑似クラスを使用している場合には、特に注意が必要です。

変更: 論理属性以外の HTML 属性の値

見出し「変更: 論理属性以外の HTML 属性の値」

HTML では論理属性(Boolean Attribute)と呼ばれる、存在すれば true、存在しなければ false になる属性があります3

具体的には、checkeddisabled などが挙げられ、値が指定されていなくても属性自体が存在すれば true になるのが特徴です。

論理属性の例
<!-- 属性が存在すれば、値に関わらず `true` になる -->
<label><input type="checkbox" name="check1" checked>1</label>
<label><input type="checkbox" name="check2" checked="">2</label>
<label><input type="checkbox" name="check3" checked="checked">3</label>
<label><input type="checkbox" name="check4" checked="true">4</label>
<label><input type="checkbox" name="check5" checked="false">5</label>

<!-- 属性が存在しなければ `false` になる -->
<label><input type="checkbox" name="check6">6</label>

Astro ファイルで属性の出し分けをする目的で、true もしくは false の指定をすることがありますが、v5 からはこの方法が有効になるのが論理属性に限定されます。

Astro ファイルの例
<!-- 論理属性 -->
<label><<input type="checkbox" name="check1" checked={true}>foo</label>
<label><<input type="checkbox" name="check2" checked={false}>bar</label>

<!-- 非論理属性 -->
<custom-element feat={true}>foo</custom-element>
<custom-element feat={false}>bar</custom-element>

<!-- 非論理属性 -->
<div data-stack={true}>foo</div>
<div data-stack={false}>bar</div>

最初の checked は論理属性ですが、カスタム要素の独自属性である feat や、 data-* 属性は論理属性ではありません。結果を見てみましょう。

生成される HTML の例
<!-- 論理属性 -->
<label><input type="checkbox" name="check1" checked>foo</label>
<label><input type="checkbox" name="check2">bar</label>

<!-- 非論理属性 -->
<custom-element feat="true">foo<custom-element>
<custom-element feat="false">bar<custom-element>

<!-- 非論理属性 -->
<div data-stack="true">foo</div>
<div data-stack="false">bar</div>

このように、論理属性ではない場合、truefalse がそのまま文字列として出力されてしまいます。属性自体の出し分けをしたい場合には、以下のように undefined もしくは null を使用することで実現できます。

undefined を指定して属性自体を出力しない例
<custom-element feat={undefined}>foo</custom-element>
<div data-stack={undefined}>bar</div>

より実用的な例としては、以下のように props に応じて属性を出し分けます。

props に応じて出し分ける例
---
type Props = {
  feat?  : boolean;
  stack? : boolean;
}

const { feat, stack } = Astro.props;
---

<custom-element feat={feat || undefined}>foo</custom-element>
<div data-stack={stack || undefined}>bar</div>

ひととおり対応が完了したら、astro check を実行して、重大なエラーが発生していないかを確認するのがよいでしょう。

npx astro check

この記事では、Astro v5 で追加された主要な機能の説明や、本サイトを v5 にアップグレードすることで必要になった対応を紹介しました。

記事内では具体的な内容は割愛しましたが、Content Collections API の変更は影響範囲が大きく、今回のアップグレードで特に注意を払う作業でした。

また、論理属性以外の HTML 属性の値は、意図せずに false を指定していてもエラーとして表示されないため探しづらく、まだ見落としている可能性もあります。

もし、サイト内での不具合など、お気づきの点がございましたらお知らせください。

脚注

  1. Using SVGs as Astro components and inline CSS | David Warrington

  2. Legacy flags

  3. The 27 Boolean Attributes of HTML | Jens Oliver Meiert