ウェブ開発は、単一の静的ページで構成された個人サイトの始まりから、今日まで長い道のりを歩んできました。最近では言語、フレームワーク、そしてコンテンツ管理システム(CMS)の選択肢の幅は大きく広がり、あらゆるニッチに対応することができます。

今回取り上げたいのは、JavaScriptフレームワークの新星「Astro」です。

Fred K. Schott氏をはじめとする開発チームによって構築されたAstroは、静的サイトジェネレーターのように動作するオールインワンフレームワークで、開発者の間で瞬く間に人気を獲得しています。

Astroが支持を集めている理由、他のソリューションよりも優れている点などをご紹介し、Astroを使用してMarkdownベースのブログを構築する方法を見ていきます。

Astroとは

Astro
Astro

Astro(Astro.js)は、素早くスムーズに動作するコンテンツ豊富なサイトの構築を想定した、人気の静的サイトジェネレーター。その軽量な性質、直感的な構造、そして習得のしやすさは、経験値を問わずあらゆる開発者向けと言えます。

そのフットプリントの小ささにもかかわらず、Astroにはサイトの柔軟性を飛躍的に高める強力なツールが搭載されており、コンテンツやテーマ管理の手間を削減。また、好きなフレームワークと連動して使用できるのは、すでに使い慣れたフレームワークを複数持つ上級者にとって嬉しい機能です。

Astroには、他のソリューションと差をつける以下のような魅力があります。

  • アイランドアーキテクチャ─ユーザーインターフェース(UI)を、「Astroアイランド」と呼ばれるどのページでも使用できる小さな独立したコンポーネントに抽出。未使用のJavaScriptは軽量なHTMLに置き換えられる。
  • JavaScriptなし(デフォルト)─コード書き写し、本番環境へのJavaScriptの使用を回避して、サイトスピードを重視。
  • SSRも使用可─静的サイトジェネレーターとして構築されたが、現在では静的サイトジェネレーター(SSG)とサーバーサイドレンダリング(SSR)を併用できるフレームワークに。ページごとにどちらを使用するかを選択できる。
  • フレームワークにとらわれない─任意のJavaScriptフレームワークを使用可能(これについてはこの記事の後半で)。

さらにエッジ対応で、いつでもどこでも簡単にデプロイ可能です。

続いて、Astroの構造について掘り下げていきましょう。

Astroの構造

Astroについて紐解く前に、Astroがどのような構造を持つかを理解することが重要です。まずはAstroの中核となるファイル構造を見てみます。

├── dist/
├── src/
│   ├── components/
│   ├── layouts/
│   └── pages/
│       └── index.astro
├── public/
└── package.json

このように、構造自体は非常にシンプルですが、いくつか重要な点があります。

  • プロジェクトの大部分はsrcフォルダに格納される。コンポーネント、レイアウト、ページはサブフォルダに整理可能。さらにフォルダを追加して、プロジェクトを見やすくすることも。
  • publicフォルダには、フォントや画像、robots.txtファイルなど、ビルドプロセスの外部にあるすべてのファイルが格納される。
  • distフォルダには、本番サーバーにデプロイするすべてのコンテンツが格納される。

では、Astroの主な構成要素であるコンポーネント、レイアウト、ページについて見ていきましょう。

コンポーネント

コンポーネントは再利用可能なコードの塊で、WordPressのショートコードのようなものです。デフォルトでは.astroというファイル拡張子を持ちますが、VueReact、Preact、SvelteでビルドされたAstro以外のコンポーネントを使用することもできます。

以下は、単純なコンポーネントの見え方の例です(h2.astroを含むクラス化されたdivタグ)。


<div class="kinsta_component">
    <h2>Hello, Kinsta!</h2>
</div>

このコンポーネントは、以下のようにサイトに組み込みます。

---
import KinstaComponent from ../components/Kinsta.astro
---
<div>
    <KinstaComponent />
</div>

このように、まずはコンポーネントをインポートしてからページに組み込みます。

続いて、コンポーネントをプロパティに追加します。まずは{title}から。

---
const { title = 'Hello' } = Astro.props
---

<div class="kinsta_component">
    <h2>{title}</h2>
</div>

プロパティの実装方法は以下のとおりです。

---
import KinstaComponent from ../components/Kinsta.astro
---

<div>
    
    <KinstaComponent title="Good day"/>

    
    <KinstaComponent />
 </div>

このように、かなりシンプルです。

お気づきかもしれませんが、Astroのコンポーネントの真の強みはグローバル性と再利用性にあります。たった数行のコードを編集するだけで、サイト全体に大幅な変更を加えることができます。

レイアウト

Astroお馴染みのテーマ機能に加えて、レイアウトも再利用可能なコンポーネントですが、レイアウトはラッパーとして使用します。

以下の例をご覧ください。

---
// src/layouts/Base.astro
const { pageTitle = 'Hello world' } = Astro.props
---

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width">
    <title>{pageTitle}</title>
</head>
<body>
    <main>
        <slot />
    </main>
</body>
</html>

<slot />タグに注目します。Astroのこの<slot />要素は、HTMLタグやコンテンツのプレースホルダーとして機能します。

実際に使ってみましょう。

以下のコードでは、<slot />タグが目的のコードに置き換えられており、すべてBase.astroレイアウトでラップされています。

---
import Base from '../layouts/Base.astro';
---

<Base title="Hello world">
    <div>
        <p>これは例文です</p>
    </div>
</Base>

<slot />タグは、以下のHTMLに置き換えられていることがわかります。

<div>
    <p>これは例文です</p> </div>

このように、コンポーネントと同じようにレイアウトを使用すれば、サイト全体でコードの塊を再利用でき、グローバルなコンテンツやデザインの更新作業が効率化できます。

ページ

ページは特別なコンポーネントで、ルーティングやデータ読み込み、テンプレート化の役割を担います。

Astroは、動的ルーティングではなく、ファイルベースの静的ルーティングを使用してページを生成します。静的ルーティングでは、帯域幅の消費量が少なくなり、コンポーネントを手動でインポートする手間が省けます。

定義されたルートの例は以下のとおり。

src/pages/index.astro => yourdomain.com
src/pages/test.astro => domain.com/test
src/pages/test/subpage => domain.com/test/subpage

例えばトップページは次のようにレンダリングされます。


<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width">
    <title>Hello World</title>
</head>
<body>
    <h1>Hello, Kinsta</h1>
</body>
</html>

レイアウトの使用方法は先でご説明したとおりです。これをグローバルにアクセス可能なものに変換してみます。

---
import Base from '../layouts/Base.astro';
---

<Base>
    <h1>Hello, Kinsta</h1>
</Base>

これでかなりすっきりしました。

Astroのルーティングに関しては、後ほど詳しくご紹介します。続いては、サイトの構築とカスタマイズについてです。

Astroのカスタマイズと拡張

Markdownコレクション、ルーティング、画像処理、Reactとの統合で、静的サイトを構築、パーソナライズすることができます。

Markdownコレクション

バージョン2.0からは、それ以前よりも大幅に改善されたMarkdownコンテンツの管理方法が導入されています。これによって、すべてのフロントマターが含まれ、適切なタイプの関連付けが行われているかを確認することができます。

最近のバージョン2.5では、JSONとYAMLファイルもコレクションとして管理できるようになりました。

それでは、使用例を見ていきましょう。

まず、すべてのMarkdown記事をsrc/content/collection_nameフォルダに格納します。この例ではブログコレクションを作成するため、フォルダ名は「src/content/blog」とします。

次に、src/content/config.tsファイルで必要なフロントマターフィールドを定義。この例では次の項目を使用します。

  • title─文字列
  • tags─配列
  • publishDate─時刻
  • image─文字列(任意)

これをまとめると次のようになります。

import { z, defineCollection } from 'astro:content';

const blogCollection = defineCollection({ 
    schema: z.object({
        title: z.string(),
        tags: z.array(z.string()),
        image: z.string().optional(),
        publishDate: z.date(),
    }),
});

export const collections = {
    'blog': blogCollection,
};

article-about-astro.mdのMarkdownファイルの内容は以下のとおり。

---
title: Astroに関する記事
tags: [tag1, tag3]
publishDate: 2023年3月1日
---
## Tamen risit

Lorem *markdownum flumina*, laceraret quodcumque Pachyne, **alter** enim
cadavera choro.

このMarkdownファイルには特別なコードはありませんが、入力ミスをしたときに本領を発揮します。

例えば、publishDateと入力しようとして、誤ってpublishDataと入力してしまった場合。Astroはこのようなミスに対して、次のようなエラーを返します。

blog → article-about-astro.md frontmatter does not match collection schema.
  "publishDate" is required.
article-about-astro.mdのフロントマターがコレクションスキーマと一致しません。「publishDate」が必要です。(和訳)

この気の利いた機能のおかげで、フロントマターの誤りを瞬時に見つけることができます。

最後にデータを表示するページが必要です。src/page/blog/[slug].astroに以下のコードでファイルを作成します。

---
import Base from '../../layouts/Base.astro';
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
    const blogEntries = await getCollection('blog');
    return blogEntries.map(entry => ({
        params: { slug: entry.slug }, props: { entry },
  }));
}
const { entry } = Astro.props;
const { Content } = await entry.render();
---
<Base>
    <h1>{entry.data.title} </h1>
    <Content />
</Base>

getStaticPathsによって、ブログコレクションの各記事のすべての静的ページが生成されます。

残るはすべての記事一覧のみです。

---
import Base from '../../layouts/Base.astro';

import { getCollection } from 'astro:content';
const blogEntries = await getCollection('blog');
---
<Base>
<ul>
    {blogEntries.map(item => <li> <strong><a href={'/blog/' + item.slug}>{item.data.title}</a></strong></li>)}
</ul>
</Base>

このとおり、コレクションを使用することで、驚くほど簡単に実行できます。

次に、データ型コレクションを作成してみます。src/content/config.tsファイルを再度開き、新たにデータコレクションを追加します。

import { z, defineCollection, referenece } from 'astro:content';

const blogCollection = defineCollection({ 
	type: 'content',
    schema: z.object({
        title: z.string(),
        tags: z.array(z.string()),
        image: z.string().optional(),
        publishDate: z.date(),
	    author: reference('authors')
    }),
});

const authorsCollection = defineCollection({ 
	type: 'data',
    schema: z.object({
        fullName: z.string(),
        country: z.string()
    }),
});


export const collections = {
    'blog': blogCollection,
'authors': authorsCollection,
};

コレクションの作成とは別に、blogCollection著者情報も追加しています。

著者情報を追加するには、content/authors.jsonにmaciek-palmowski.jsonファイルを作成します。

{
    "fullName": "Maciek Palmowski",
    "country": "ポーランド"
}

最後に、getEntryを使用してこのデータをPostに取り込みます。

---
import Base from '../../layouts/Base.astro';
import { getCollection, getEntry } from 'astro:content';
export async function getStaticPaths() {
  const blogEntries = await getCollection('blog');
  return blogEntries.map(entry => ({
    params: { slug: entry.slug }, props: { entry },
  }));
}
const { entry } = Astro.props;
const author = await getEntry(entry.data.author);
const { Content } = await entry.render();
---
<Base>
<h1>{entry.data.title}</h1>
<h2>Author: {author.data.fullName}</h2>
<Content />
</Base>

ルーティング

Astroには、2つの異なるルーティングモードがあり、静的(ファイルベース)ルーティングについては、先ほどの「ページ」セクションで触れました。

ここでは、もう1つの動的ルーティングについて。

動的ルートパラメータを使うと、ページファイルに指示を出し、同じ構造を持つ複数のページを自動生成することができます。これは、特定のタイプのページが多数ある場合に便利です(著者の経歴、ユーザープロフィール、ドキュメント記事など)。

著者の経歴ページを作成する例を見てみます。

Astroデフォルトの静的出力モードでは、これらのページはビルド時に生成されます。つまり、対応するファイルを取得する著者一覧を事前に用意しておかなければなりません。対する動的モードでは、一致するルートへのリクエストに応じてページが生成されます。

ファイル名として変数を渡したい場合は、ブラケット(大括弧)で囲みます。

pages/blog/[slug].astro -> blog/test, blog/about-me 

src/page/blog/[slug]ファイルのコードを用いて、もう少し掘り下げてみます。

---
import Base from '../../layouts/Base.astro';
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
    const blogEntries = await getCollection('blog');
    return blogEntries.map(entry => ({
        params: { slug: entry.slug }, props: { entry },
  }));
}
const { entry } = Astro.props;
const { Content } = await entry.render();
---
<Base>
    <h1>{entry.data.title}</h1>
    <Content />
</Base>

getStaticPathsルートは、すべての静的ページを生成します。このルートは次の2つのオブジェクトを返します。

  • params:URLの括弧を埋めるために使用される
  • props:ページに渡すすべての値

ページ生成は以上で完了です。

画像処理

最新の画像ファイル形式、適切なリサイズ方法、遅延読み込みなくして、サイトのパフォーマンス改善は語れません。

Astroはこの点も考慮されており、@astrojs/imageパッケージで上記のすべてをものの数分で導入できます。

パッケージをインストールすると、ImageおよびPictureの2つのコンポーネントを使用できるようになります。

Imageコンポーネントは、最適化された<img />タグの作成に使用します。例を見てみましょう。

---
import { Image } from '@astrojs/image/components';
import heroImage from '../assets/hero.png';
---

<Image src={heroImage} format="avif" alt="説明文" />
<Image src={heroImage} width={300} alt="説明文" />
<Image src={heroImage} width={300} height={600} alt="説明文" />

同様に、Pictureコンポーネントは、最適化された<picture/>コンポーネントを作成します。

---
import { Picture } from '@astrojs/image/components';
import hero from '../assets/hero.png';
---
<Picture src={hero} widths={[200, 400, 800]} sizes="(max-width: 800px) 100vw, 800px" alt="説明文" />

SSGとSSRの比較

デフォルトでは、Astroは静的サイトジェネレーター(SSG)として動作し、コンテンツはすべて静的なHTMLページに変換されます。

これは多くの観点(特にスピード)において適したアプローチになりますが、より動的なアプローチが好ましい場合もあります。例えば、各ユーザーのプロフィールページを個別に作成したい場合や、サイトに何千もの記事がある場合、毎回すべてを再レンダリングするのにはかなり時間がかかります。

Astroは、サーバーサイドレンダリング(SSR)のフレームワークとして、あるいはSSGとSSRのハイブリッドモードで使用することもできます。

SSRを有効にするには、以下のコードをastro.config.mjsに追加します。

import { defineConfig } from 'astro/config';

export default defineConfig({
    output: 'server'
});

これが一般的な方法です。

SSGとSSRを併用すると、デフォルトでexport const prerender = trueを追加したページを排除し、すべてが動的に生成されます。

Astro 2.5では、静的レンダリングをデフォルトに設定し、動的ルートを手動で選択するという手法もあります。

これによって、例えば、動的なログインページとプロフィールページを持つ静的サイトを構築することができます。

詳しくは公式ドキュメントをご覧ください。

他のJavaScriptフレームワークとの統合

Astroを使用するもう1つの大きなメリットは、好きなフレームワークを連携できること。React、Preact、Svelte、Vue、Solid、Alpineなどを統合することができます(詳しくは公式ドキュメント参照)。

例えば、Reactを使用するなら、まずはnpmで以下を実行します。

npx astro add react

これでReactが統合され、Reactコンポーネントを作成できるようになります。以下は、src/components/ReactCounter.tsxのカウンターコンポーネントを作成する例です。

import { useState } from 'react';

/** Reactで書かれたカウンター */
export function Counter({ children }) {
    const [count, setCount] = useState(0);
    const add = () => setCount((i) => i + 1);
    const subtract = () => setCount((i) => i - 1);

    return (
        <>
            <div className="counter">
                <button onClick={subtract}>-</button>
                <pre>{count}</pre>
                <button onClick={add}>+</button>
                </div>
            <div className="counter-message">{children}</div>
        </>
    );
}

最後に、次のコードでカウンターをページに配置します。

---
import * as react from '../components/ReactCounter';
---
<main>
    <react.Counter client:visible />
</main>

これで、Reactコンポーネントがサイトにシームレスに統合されます。

KinstaでAstroサイトをデプロイする方法

Astroサイトを構築したら、次のステップはウェブ上での公開。Kinstaでは、迅速かつ簡単に静的サイトをホスティングできます。

まずは、サイトのファイル用にGitHubリポジトリを作成します。ファイルの用意がなければ、KinstaのAstroテンプレートを複製することができます。

リポジトリを準備したら、以下の手順に従って、静的サイトをKinstaにデプロイします。

    1. ログインまたはアカウントを作成して、MyKinstaを開く
    2. GitサービスでKinstaを認証する
    3. 左サイドバーの「静的サイト」を選択して「サイトを追加」をクリックする
    4. デプロイしたいリポジトリとブランチを選択する
    5. サイトに一意の名前を割り当てる
    6. 以下の形式でビルド設定を追加する
      • ビルドコマンドnpm run build
      • ノードのバージョン18.16.0
      • 公開ディレクトリdist
    7. 最後に「サイトを作成」をクリックする

以上で、Astroフレームワークで構築された静的サイトのデプロイが完了です。

本番のAstroホームページ
本番のAstroホームページ

本番URLやその他のデプロイ情報については、「デプロイ」画面で確認できます。

静的サイトホスティングの代替として、より柔軟性に優れたKinstaのアプリケーションホスティングで静的サイトをデプロイすることも可能です。Dockerfileを使用しカスタマイズしたデプロイメント、リアルタイムおよび過去のデータを網羅した包括的な分析など、便利な機能が多数揃っています。

まとめ

Astroでは、明瞭な構造、シンプルな構文、グローバルなコンポーネントによって、アプリケーションの構築と実行が非常に簡単です。軽量な性質と静的および動的ルーティングを活用することで、サイトの応答性を劇的に高めることができます。

コンテンツが豊富で、読み込みが速く、モジュールをいかした機能を持ち、静的生成と動的生成の両方を用いたサイトを作成するなら、Astroを使用しない手はありません。

構築した静的サイトは、Kinstaの静的サイトホスティングで無料で運営可能です。

Astroを使用したことはありますか?以下のコメント欄でAstroに関するご意見をお聞かせください。

Maciek Palmowski

Maciek is a web developer working at Kinsta as a Development Advocate Analyst. After hours, he spends most of his time coding, trying to find interesting news for his newsletters, or drinking coffee.