Astroでニュースレターサイトを作る
僕のニュースレター用のWebサイトを作っていたとき、ルーティングやアイソモーフィック(同構)は僕が最も重視するポイントではなく、データベースから頻繁にデータを取得したり、ブラウザ側でインタラクションを行ったりする要件もないことに気づいた。そのため、Next.jsのようなSSRフレームワークはオーバースペックなのではないかと考えた。Next.jsも完全な静的エクスポートをサポートしているが、Reactコンポーネントを書く際の認知的負荷を考えると、少し二の足を踏んでしまった。
初めてAstroを聞いたのは同僚からの紹介で、せっかくの機会だから試してみようと思った。AstroはSFC(単一ファイルコンポーネント)を中心とした開発手法を提供しており、非常に手軽かつ高速にWebページを構築できる。アーキテクチャ的にもクラウドのトレンドに合わせてVercelやNetlify、GitHub Pagesなどのサービス向けに調整されており、コードをアップロードするだけで使えるWebページを作成できる。

市場にはすでに多くの静的サイトジェネレーターが存在しており、なぜさらにもう一つ必要なのかと疑問に思う人もいるかもしれない。僕のブログはJekyll、HexoからGatsbyへと進化し、これまで何度もフレームワークの移行を経験してきた。この経験によって、こうしたコンテンツ指向のWebサイトを作る際に自分が何を重視しているのかが、より明確になった。
JekyllとHexoには多くの共通点がある。コンテンツサイトを構築するための基本機能は備えているものの、Markdown構文の拡張や、よりインタラクティブ性の高いMDXの実現は比較的難しく、それぞれ固有のテンプレート構文に適応しなければならなかった。
Gatsbyを使う場合は、自分でGraphQLを書く必要がある。最初は、この柔軟性が書いていて楽しかったが、スキーマを定義していない状態だと、Markdownのfrontmatter属性を追加したいときにかなり面倒なことになった。
次第に僕は、単にシンプルなページを作りたいだけなのに、大量の時間を費やしてGraphQLやReactコンポーネントを書かざるを得ない、という状況によく陥るようになった。
もう一つの選択肢は11ty(Eleventy)で、Astroと非常によく似ているが、最大の違いはテンプレート構文にあると考えている。11tyは多様なテンプレート構文をサポートしているが、長年Reactを書いてきた僕にとって、JSXが最も直感的な方法に思えた。そしてAstroはまさにそれを実現してくれた。以下に、僕が感じたAstroと他のジェネレーターとの明確な違いをいくつか共有したい。
単一ファイルコンポーネント(Single-File Components)
Astroの設計では、すべてのファイルを完全なコンポーネントとして扱うことができる。
つまり、JavaScript(ビルド時のみ実行される)、HTML、スタイルを同一ファイル内に記述でき、クラス名のスコープ分離はフレームワークが処理してくれる。JSXやVueが書ければ、別のテンプレート構文を新たに学ぶことなく、Astroのコンポーネントを書くことができる。
---
const variable = 'Hello';
---
<section>
<p>
{variableA}
</p>
</section>
<style>
p { font-size: 14px; }
</style>
Webページを作成するときに、JavaScript、HTML、CSSを同じファイルに書けるのは非常に快適であり、Astroはデフォルトでこれをサポートしているため追加の設定も不要だ。
コンポーネントの構築方法はVueやSvelteと同じだが、注意すべきなのは、AstroはデフォルトではSSRでもフロントエンドフレームワークでもないため、ページはすべてビルド時にのみ実行され、余分なJavaScriptは出力されない(SSRモードを有効にしない場合)という点だ。
ブラウザ側でJavaScriptを実行してインタラクションを行いたい場合はどうすればいいか?Astroは2つの方法を提供している:
<script>タグ内に記述する。ただしDOMバインディング機能はないため、document.querySelector()のようなコードを書く必要がある- 他のフロントエンドフレームワークを直接インポートして使用する
Astro Island
ページの大部分は静的だが、特定の1〜2ページやページの一部(ブログのコメント欄など)だけでインタラクションを多用したい、という要件が生じることがある。
ブラウザでのインタラクションについて、AstroではReactやVueなど、他のフレームワークのコンポーネントファイルを参照することができ、対応するアダプターさえあれば追加可能だ。この機能は僕にとって非常にユニークであり、静的サイトジェネレーターにさらなる柔軟性をもたらしていると感じる。
<aside>
<ReactOrVueComponent client:load />
</aside>
このコードでは、非Astroコンポーネントに遭遇した際、Astroはコンパイル時に空のDOMを生成し、ブラウザ側でJavaScriptを読み込んでからコンポーネントのDOMノードをマウントする。ここでの client:load は、スクリプトをどのタイミングで読み込むべきかを指定している。このオプションで僕が親切だと感じたのは、client:media や client:visible が用意されている点だ。
特定のデバイスでのみ読み込みたいコンポーネント(例えばスマートフォンの画面でのみ表示したいスワイプメニューなど)や、特定の位置までスクロールしたときに初めて読み込みたいコンポーネントが存在することがある。フレームワークの助けがなければ、これらはすべて自分でJavaScriptを書いて解決しなければならないが、Astroはそれをすべて肩代わりしてくれる。
ここで一つ疑問が生じる。異なるフレームワーク間(例えばAstroとReact)でどのように状態を共有するのかという問題だが、Astroはすでにその解決策を提供している。とはいえ、別のライブラリを導入することになるため、コストがメリットを上回らないか疑問に思う部分もある。
僕は、静的サイトジェネレーターに他のフロントエンドフレームワークのサポートを取り入れることに対して前向きに考えている。おそらくAstro単体で大半のWebサイトのニーズは満たせるし、完全にインタラクティブなWebサイトを作りたいのであれば最初からAstroを選ばないだろう。しかし、フロントエンドフレームワークのサポートが加わることで、一部のニッチな需要にも応えられるようになり、Webサイトを素早く構築しつつ、必要に応じてPreactなどを導入してインタラクションを書くのも非常に手軽になる。
Content Collections
Astroには Content Collections という機能がある。簡単に言えば、静的なMarkdownファイルをクエリするのを支援してくれる機能だ。content/ ディレクトリ配下のファイルはすべてContent Collectionsとして扱われる。
import { getEntry } from "astro:content";
const entry = await getEntry("weekly", "my-first-weekly.md");
const { Content } = await entry.render();
AstroはMarkdownをそのままページとしてレンダリングすることも可能だが、ページのレイアウト調整の柔軟性を保ちたい場合、組み込みの検索関数を利用できる。entry.render() は処理済みの <Content /> コンポーネントを返すため、それを好きな場所に配置するだけで、MarkdownのコンテンツをHTMLとしてレンダリングできる。
型サポート
AstroはTypeScriptをサポートしており、静的ページであってもPropsを宣言することでそのコンポーネントが受け取れるプロパティを定義できるため、使い勝手が非常に良い。
// MyComponent.astro
export interface Prop {
user: string
}
// <MyComponent user="kalan" />
コンポーネントのプロパティだけでなく、Contentの定義も可能だ。例えば記事の定義では、タイトル、概要、画像などのメタデータを定義するために通常frontmatterを使用する:
---
title: 這是標題
summary: 這是大綱
image: https://image.com
---
Astroはスキーマ定義機能(内部実装は zod)を提供しており、content/config.ts 内でスキーマの型を定義しておくだけで、getCollection や getEntry を使用する際に対応する型情報を得ることができる。
import { z, defineCollection } from "astro:content";
const weeklyCollection = defineCollection({
type: "content",
schema: z.object({
published_at: z.date(),
issue_num: z.number(),
title: z.string(),
description: z.string().optional(),
image: z.string().optional(),
}),
/* ... */
});
export const collections = {
weekly: weeklyCollection,
};
// 其他 Astro 元件內呼叫
import { getCollection, getEntry } from "astro:content";
const entry = await getEntry("weekly", 'my-first-weekly.md'); // entry 會解析出 weekly 型別,來自上面的 weeklyCollection
Astroを使ってみた感想
全体として、Astroは非常に快適な開発体験を僕にもたらしてくれた。特別な機能を必要としなければ、変数を埋め込めるHTMLとして書くだけでも全く問題ない。
静的サイトジェネレーターの機能はどれも大同小異であり、前述した機能も、他のツールで本気で実現しようとすれば方法は見つかるだろう。では、Astroの何がそんなに優れているのか?それは、快適な開発者体験(DX)、シンプルさと柔軟性を両立した構文(JSX)、特定のユースケースに対する的確な設計(他のフロントエンドフレームワークのサポートや多様なスクリプト読み込み戦略)、そしてクラウドサービスとの連携のスムーズさにあると僕は考えている。
フロントエンドにおける静的サイトの構築方法は、ここ数年で大きく変化した。Gatsbyの登場以降、開発者はより効率的にWebページを構築する方法を模索し続けてきた。それは単にコンポーネントをサーバーサイドでレンダリングできるようにするだけでなく、JavaScriptの読み込みタイミング、プリフェッチ、Service Workerによるキャッシュ、画像最適化の仕組み、スタイリングの管理、エッジコンピューティング、さらには開発スピードの両立まで、現代の静的サイトジェネレーターが考慮すべき要素は多岐にわたる。もはや単にMarkdownをHTMLに変換してサーバーにアップロードするだけの時代ではないのだ。
関連記事
- Three.js で僕の部屋を表現する 僕が React Three Fiber を使って実際の自分の部屋をブラウザ上に再現し、実世界のオブジェクトを目録に見立て、空間の記憶を通してここ数年の生活と仕事について語った話。
- フロントエンドで画像を扱う際に注意すべきこと Jake Archibaldの記事を起点に、現代のレスポンシブ画像の書き方を整理する。なぜwidth/heightを付ける必要があるのか、CSSのaspect-ratioはいつ使うべきか、AVIFとWebPの選び方、そしてpicture/source/srcsetを使ったモバイル向け画像の切り替えについて。
- CSS field-sizing — たった1行のCSSでフォーム要素を自動リサイズする かつてtextareaの自動高さ調整は、JavaScriptでscrollHeightを監視するしかなかった。しかしCSSのfield-sizing: contentなら、わずか1行で代替でき、textarea、input、selectに対応している。本記事では従来のやり方のペインポイントと、field-sizingの使い方をまとめる。
- リンクの下線をもっと見栄え良くする:text-underline-offset デフォルトでは下線と文字が近すぎて、このスタイルを好まないデザイナーもいるし、僕自身もあまり綺麗ではないと感じていた。