SPAへ向かうAstro

MPA(Multi-page App)なAstroが、Next.jsやGatsubyのようなSPA(Single-page App)へ向かおうとしています。

MPA なフレームワークの代表的な存在である Astro。

ただ、SPA なサイトに慣れてしまったためか、個人的には MPA のパタパタした挙動がどうも落ち着きません。これは、ページやサイト側の処理だけが原因ではなく、ブラウザの挙動がページの移動を強調するようになった影響も大きい気がします(セキュリティ対策?)。

そのため、Astro でサイトを作る場合には挙動を SPA っぽくしたくなり、その対策をいろいろと考えるのですが、React Server Components が登場してきた現状だと Next.js で SPA でいいや… と、Astro の出番が少なくなっていました。

ところが、ブロックテーマな WordPress のフロントエンドとして使うフレームワークを検討していたところ、Gatsby.js ではブロックテーマのスタイルと GatsbyImage の相性の悪さが問題となり、Next.js では App Router & 静的生成を前提にすると画像の扱いが厳しいことが見えてきたもので、どちらも候補から外れることに。

最後に残ったのが Astro だったのですが、MPA が好みではなく…ということで、手軽に SPA っぽくできないのだろうか?と調べてみました。やはりチャレンジしている方がいるもので、気になるものをメモ代わりに残しておこうと思います。

Turbo Drive

まずは、こちらのブログで紹介されている Turbo Drive の Turbo を使った方法です。

Persistent Islands in Astro with Turbo Drive
https://www.maxiferreira.com/blog/astro-turbo-persistent-islands/

このブログによれば、Turbo Drive は Rails チームが 2012 年に開発した Turbolinks というライブラリが改名されたもので、Hotwire エコシステムの構成要素とされています。

Rails を触ったことがないもので、Hotwire エコシステムや Turbo Drive に関してはさっぱりなのですが、これを Astro に組み込むことで SPA な挙動になります。

Turbo がブラウザ上で動いている間は、リンクやフォームの処理が Turbo を通して行われることになります。その結果、クライアントサイドでナビゲーションが処理される形になります。

たとえば、ページを移動して次のページをレンダリングする際には、現在の<body>を新しいページのものに置き換え、<head>の内容は新しいページのものとマージします。

こちらのリポジトリにあるプロジェクトを参考にして、自分のプロジェクトに組み込んでみると、SPA らしい挙動が確認できます。

Astro SPA Starter Kit: Basics
https://github.com/paragchirde/Astro-SPA

デベロッパーツールの Network タブで確認してみれば、サーバーへデータを取得しに行かず、Turbo が仕事をしているのを確認できます。

👇Turbo 導入前

Turboがない場合、リンク先にアクセスすると、ページを表示するのに必要なファイルがすべて読み込み直されています。

👇Turbo 導入後

Turboがある場合、最初のページアクセス時にTurboのスクリプトが読み込まれます。リンク先にアクセスすると、Turboを通して、ページを表示するのに追加で必要な分だけが取得されています。

👇Turbo の導入手順

Astro のプロジェクトに Turbo を導入するのは簡単です。まずは Turbo をインストールします。

Terminal window
npm install @hotwired/turbo

そして、上のサンプルのプロジェクトでは src/turbo-router.js を以下の内容で作成しています。

import * as Turbo from '@hotwired/turbo'
Turbo.start()

これを以下のような形で layout.astro で読み込みます。これだけで SPA な挙動になります。

// ~省略~
<title>{title}</title>
<script>
import "../turbo-router.js";
</script>

要素に data-turbo-permanent 属性と id を与えることで、その要素の永続化が可能になります。Astro のアイランド・アーキテクチャとは相性が良く、アイランドの永続化が可能になります。

また、ページを移動する際に処理を挟み込んだり、キャッシュをコントロールする機能が用意されています。このあたりを使うことで、最近注目されている View Transitions API も簡単に組み込めます。

👇View Transitions API の組み込み

turbo-router.js を以下のように修正すれば、View Transitions API を使ってページ間がクロスフェードで切り替わるようになります。

import * as Turbo from '@hotwired/turbo'
Turbo.start()
addEventListener("turbo:before-render", (event) => {
if (document.startViewTransition) {
event.preventDefault()
document.startViewTransition(() => {
event.detail.resume()
})
}
})

さらに、Smooth and simple transitions with the View Transitions API などを参考に CSS を設定すれば、要素間のトランジションも簡単に実現できます。

今回は、必要な CSS をアイキャッチ画像に適用して仕上げてみました。view-transition-name プロパティで同じ識別子を持つ画像にトランジションがかかるため、記事のスラッグを指定しています。

<Custompicture
src={src}
alt={alt}
style={{ viewTransitionName: `${slug}`, contain: 'paint' }}
/>

生成コードは次のようになります。記事ごとに、記事一覧ページと記事ページのアイキャッチ画像の view-transition-name が同じ値になっているのがポイントです。

記事4のアイキャッチ画像には、記事一覧と記事ページのどちらに表示された場合にも「view-transition-name:owl04」が適用されています。

これで、View Transitions API に対応したブラウザ(Chrome、Edge、Opera)ではアイキャッチ画像が残り、拡大縮小してくれます。

なお、その際には Turbo のプレビュー機能をオフにする必要があります。そのため、layout.astro を通して各ページに以下の <meta> を追加しています。

<meta name="turbo-cache-control" content="no-preview">

こちらを参考にさせていただきました
猫でもわかるHotwire入門 Turbo編

スタイルの当て方などでちょっと悩むところはありましたが、そのあたりは Astro の柔軟性で解決できます。個人的には、Zero JavaScript にはこだわりがないもので、常用してもいいかなと思っています。

他のアプローチとして、HTMXなども出てきます
https://htmx.org/

SPA Router

こちらは、公式の Astro の機能として検討されているものです。

もともと、Persistent Islands として提案されたものが、Stage2 へと進んで Client-side Routing になり、その成果物として登場してきたのが、こちらです。

Astroの開発におけるStageに関しては、こちらを参照してください
https://github.com/withastro/roadmap

この機能を使うためには、プロジェクトの Astro を以下のコマンドで SPA Router に対応した Astro 入れ替えます。

Terminal window
npm install astro@next--spa

そして、astro.config.mjs に以下のように設定を追加して、SPA Router をオプトインします。

export default defineConfig({
experimental: {
router: 'spa'
}
})

これだけで、SPA な挙動になります(現時点では、クロスフェードでページが切り替わる感じの表示になっています)。挙動のコントロールは <a> にデータ属性を追加することで行います。

<a data-router-ignore> <!-- SPAのナビゲーションをスキップして、従来のページロードを行う。 -->
<a data-router-noscroll> <!-- ナビゲーション時にページ上部へスクロールしない -->
<a data-router-keepfocus> <!-- ナビゲーション時にフォーカスをリセットしない -->

まだまだこれからといった感じではありますが、View Transitions API や Navigation API などが明確に意識されていたりと楽しみな機能です。


どちらも、Astro の MPA な部分はそのままに、部分的に SPA 化できるというのは魅力です。特に Astro Island が強く意識されているのも興味深いところでしょう。

SPA Router がどうなるかも含めて、Astro はまだまだ大きく変化していきそうです。

(⬇️ Astro の基本的な使い方はこちらの書籍にまとめています)