賢明な皆様はお気づきかもしれませんが、このブログは先日フルリニューアルされました。リリースは確か2023年10月20日だったと思います。
デザインが変わっているのは実は副次的なもので、メインは Nuxt Content から Astro への移行でした。フレームワークから変わってしまうため旧スタイルを移設するのに非常に手間がかかります。ならばせっかくなのでデザインも刷新してしまおう!というのがおおまかな経緯です。
今日のテーマは、 Astro の紹介と Astro 上でのブログの構築方法についてです。
Astro とは
まずは Astro を簡単に紹介します。
Astroは、コンテンツにフォーカスした高速なWebサイトを構築するためのオールインワンWebフレームワークです。 https://docs.astro.build/ja/getting-started/
Astro はコンテンツに焦点を絞った2021年生まれの若いWebフレームワークです。最も得意とするのは静的サイトの構築なので「静的サイトジェネレーター」と紹介される事も多いですが、アプリケーションの開発環境としても有用です。
ハイパフォーマンス
Astro はポリシーとして Zero JS, by default
を掲げており、基本としてクライアントサイドでJavaScriptを実行させない作りになっています。そのため極めてパフォーマンスの高い静的サイトの構築が可能になっているのです。
公式ドキュメントでは、 Astroで遅いWebサイトを構築することは不可能 とまで書かれています。すごい自信だ!
とっても簡単
Astro は難解なプログラミングを必要としません。HTML/CSS と簡単な JavaScript がわかればサイトを作り上げる事ができます。そしてファイルベースの分かりやすいルーティングと、シンプルな構成の .astro
ファイルや Markdown/MDX によるコンテンツ編集で、少ない学習コストで誰でもWebサイト編集に参加することができます。
.astro
ファイルの構成は Vue のSFC(単一ファイルコンポーネント)にとても良く似ています。
---
// JavaScript
---
<html lang="en">
<head>
<title>My Homepage</title>
</head>
<body>
<div class="Container">
<h1>Welcome to my website!</h1>
</div>
</body>
</html>
<style>
.Container {
width: 50vw;
padding: 1rem;
margin: 0 auto;
}
</style>
さらに素晴らしいのは、難解なプログラミングを駆使する事でより高機能なWebアプリケーションをも開発できる事です。ハードルは低く、それでいて可能性の振り幅が非常に広いのが特徴です。
多彩なインテグレーション
Astroは自身のコンポーネントだけでなく、React や Vue、 SolidJS など様々なフレームワークをインテグレーションとして利用する事ができます。これらをフレームワークコンポーネントと呼びます。
前述の通りAstroコンポーネントはクライアント側でJavaScriptを実行させる事ができないので、例えばクリックなどのユーザーアクションに紐づけて処理を行ったり、非同期通信などをさせたい場合は、このフレームワークコンポーネントを活用して実装します。このように動的な機能を自身で持たないからこそ、スリムで高速なWebサイトが実現できるのでしょう。
フレームワークは複数利用することができるので、React と Vue を悪魔合体させたキメラアプリケーションなんかも作れちゃうのがAstroの面白いところです。
ブログサイトを構築してみよう
それでは実際に Astro を使ってブログサイトを構築してみましょう。静的でシンプルなWebサイトと違ってブログサイトのセットアップは少々骨がありますので、咀嚼しながら前進しましょう。
Astro セットアップ
まずは Astro をセットアップしましょう。コマンド一発で簡単に導入できます。
$ npm create astro@latest
いくつか質問されますので、適当に答えましょう。以下は回答の一例です。1,2番目の質問を除いて、ほぼデフォルトで良いでしょう。
Question | Answer |
---|---|
Where should we create your new project? | ./astro-sandbox |
How would you like to start your new project? | Empty |
Install dependencies? | Yes |
Do you plan to write TypeScript? | Yes |
How strict should TypeScript be? | Strict (recommended) |
Initialize a new git repository? | Yes |
How would you like to start your new project?
の質問で Use blog template
を選択すると一瞬でブログサイトの構築は完了してしまいますので、ここは構築のステップを理解するために Empty
を選びます。
インストールが終わったら開発サーバーを立ち上げてみましょう。
$ cd astro-sandbox
$ npm run dev
http://localhost:4321 でAstroが立ち上がります。ブラウザで確認してみましょう。
コンテンツコレクションのセットアップ
ブログは Astro@2.0.0 で追加された Content Collections という機能で実装します。これはその名の通りコンテンツのコレクション(例えばブログ記事やニュースコンテンツのような)を管理する為の機能です。
管理するドキュメントは md/mdxで記述し、 src/content
ディレクトリに配置していきます。こんなイメージです。
src/
└── content/
└── blog/
├── 001.md
├── 002.md
└── 003.md
今回は blog
という名前のコレクションを作ります。
ここに保存する Markdown ファイルに記事コンテンツを書いていくのですが、そのヘッダ部分には、タイトルや投稿日時等のドキュメントに関するメタ情報を記載しておきます。
---
title: ポラーノの広場
date: 2023-10-30 00:00
description: あのイーハトーヴォのすきとおった風、夏でも底に冷たさをもつ青いそら
tags:
- 宮沢賢治
- モリーオ市
- イーハトーヴォ
---
そのころわたくしは、モリーオ市の博物局に勤めて居りました。
十八等官でしたから役所のなかでも、ずうっと下の方でしたし俸給ほうきゅうもほんのわずかでしたが、受持ちが標本の採集や整理で生れ付き好きなことでしたから、わたくしは毎日ずいぶん愉快にはたらきました。殊にそのころ、モリーオ市では競馬場を植物園に拵こしらえ直すというので、その景色のいいまわりにアカシヤを植え込んだ広い地面が、切符売場や信号所の建物のついたまま、わたくしどもの役所の方へまわって来たものですから、わたくしはすぐ宿直という名前で月賦で買った小さな蓄音器と二十枚ばかりのレコードをもって、その番小屋にひとり住むことになりました。わたくしはそこの馬を置く場所に板で小さなしきいをつけて一疋の山羊を飼いました。毎朝その乳をしぼってつめたいパンをひたしてたべ、それから黒い革のかばんへすこしの書類や雑誌を入れ、靴もきれいにみがき、並木のポプラの影法師を大股にわたって市の役所へ出て行くのでした。
このヘッダー部分はフロントマター( front-matter
)と呼ばれ、yaml で記述します。フロントマターの項目は自由に定義する事ができます。
記事を一つ書いて保存をしたら、一度ビルドをしましょう。Astro のビルドプロセスの中で astro:content
の型定義をやってくれます。
$ npm run build
ビルドをすると .astro/types.d.ts
というファイルが生成され、 astro:content
の機能が認識されるようになります。うまくワークしない場合もあるので、その時はエディタの再読み込みなどをしてみましょう。
コレクションの型定義をする
これは任意なのですが、やっておくとフロントマターの型検証などをしてくれて型安全に制作を進めることが出来ます。 先程作った content
ディレクトリに config.ts
を追加し、その中で zod
を使用したコレクションの型定義を行います。
src/
└── content/
├── blog/
│ ├── 001.md
│ ├── 002.md
│ └── 003.md
└── config.ts
import { defineCollection, z } from "astro:content";
export const collections = {
blog: defineCollection({
type: "content",
schema: z.object({
title: z.string(), // title は文字列
date: z.string(), // date は文字列
description: z.string(), // description は文字列
tags: z.array(z.string()), // tags は文字列で構成された配列( Array<string> )
})
})
};
これが基本形です。 schema
の中でフロントマターの各フィールドの型を定義しています。型の表現には zod
ライブラリを使用しているので、書き方に迷ったらそちらのドキュメントを参照すると良いでしょう。
colinhacks/zod: TypeScript-first schema validation with static type inference
type
の値は基本的に content
でOKです。JSON や yaml でデータを扱う際には data
を指定する事ができます。
記事詳細画面をつくる
さて、いよいよ記事詳細画面を作ります。まずは pages
ディレクトリの中にページファイルを設置します。
src/
└── pages/
└── blog/
└── [...slug].astro
[...slug]
にはブログ記事のスラグ(デフォルトではファイル名)が展開され、ルーティングされます。この場合、例えば記事を my-first-post.md
という名前で保存したならば、 /blog/my-first-post
というパスでアクセスできるようになります。
で、アクセスできるようにするためのスクリプトを [...slug].astro
に記述するわけです。
---
import type { GetStaticPathsResult } from "astro";
import { getCollection, type CollectionEntry } from "astro:content";
// エントリーごとにパスを生成するために getStaticPaths を使う
export async function getStaticPaths ():Promise<GetStaticPathsResult> {
// getCollection() で blog コレクションのエントリーをすべて取得
const entries = await getCollection("blog");
return entries.map((entry) => {
return {
params: { slug: entry.slug }, // params に slug を返す事でパスを生成する
props: { entry } // ページ描画のためのデータを props にわたす
}
});
}
// Props の型を定義しておく
interface Props {
entry: CollectionEntry<"blog">
}
const { entry } = Astro.props;
const { Content } = await entry.render();
---
<h1>{entry.data.title}</h1>
<Content />
ポイントはこのあたり。
- そのまま
entry
を参照しようとするとプロパティ 'entry' は型 'Props | undefined' に存在しません。
と怒られるので、コンポーネントPropsの型を定義してあげる必要があります。コンポーネントPropsについてはこちら。 - コレクションエントリーには
render()
というメソッドが生えていて、これが返す<Content />
でエントリーの本文を描画する事ができます。render()
は Promise を返すのでawait
を忘れないようにしましょう。 - フロントマターの情報は
entry.data
に格納されています。
インデックスページをつくる
次は、記事一覧を表示する目次ページを作ります。こちらは詳細画面に比べると非常に簡単です。インデックスページは /blog
のURLでアクセスしたいので、 /src/pages/blog/index.astro
を追加して書いていきましょう。
---
import { getCollection } from "astro:content";
const entries = await getCollection("blog");
---
<ul>
{entries.map(entry => (
<li>
<a href={`/blog/${entry.slug}`}>
{entry.data.title}
</a>
</li>
))}
</ul>
記事詳細画面の getStaticPaths
と同じ要領で getCollection()
を使ってコレクションの記事リストを取得し、それを .map()
で回すだけです。
.astro
ファイルの構成が Vue の単一ファイルコンポーネントに似ているという話をしましたが、テンプレートの書き方はほぼ完全に React です。React メンは .map
で返す要素に key
を渡さないところに違和感を覚えるかもしれませんが、 .astro
ワールドは完全に静的なHTMLを出力するためハイドレーションがないので key
が必要ないのだという理解をしています。
これで詳細ページと目次ページが出来たので、ブログとしてミニマムの機能は備えられたと思います。
検索ページも、つくる?(フレームワークコンポーネントの利用)
他にあると嬉しいのは検索機能ですが、Astroだけでは実現が厳しいのでフレームワークコンポーネントの力を借ります。すでに長いエントリになってしまっているのでさわりだけ。
まず、React を使えるようにします。 astro add
コマンドから簡単に追加できます。
$ npm run astro add react
そしてReactコンポーネントで検索フォームを簡易的に実装します。検索フォームに文字列を入れてサブミットすると検索結果がリストで表示される仕様です。
src/components/SearchForm.tsx
として作成しました。
import { getCollection, type CollectionEntry } from "astro:content";
import { useCallback, useState } from "react";
export function SearchForm (): JSX.Element {
const [query, setQuery] = useState<string>("");
const [result, setResult] = useState<CollectionEntry<"blog">[]>([]);
const search = useCallback(async () => {
const entries = await getCollection("blog");
setResult(
entries.filter(entry => {
return [
entry.body,
entry.data.title,
...entry.data.tags
].join(" ").includes(query)
})
);
}, [query]);
return (
<>
<form
onSubmit={e => {
e.preventDefault();
search();
}}
>
<input
type="search"
value={query}
onChange={event => {
setQuery(event.target.value)
}}
/>
</form>
<ul>
{result.map(entry => (
<li key={entry.id}>
<a href={`/blog/${entry.slug}`}>
<div>{entry.data.title}</div>
<div>{entry.data.description}</div>
</a>
</li>
))}
</ul>
</>
)
}
特段変わったことはしていないごく普通のReactコンポーネントです。ひとつ注目すべきなのは astro:content
の getCollection()
がReactコンポーネント内でも利用できるという点で、これのおかげでクライアントサイドでコレクションのデータを参照してアレコレできるわけです。
このReactコンポーネントをAstroワールドに配置します。検索ページのファイルは src/pages/search.astro
に作成しました。 /search
のURLでアクセスできます。
---
import { SearchForm } from "../components/SearchForm.tsx"
---
<h1>検索</h1>
<SearchForm client:load />
import
してテンプレートに設置するだけなので難しい事はありません。いえ、 ReactコンポーネントをAstroテンプレートにそのまま設置できるのはとってもすごい事なのですが、それを意識させずに簡単にできちゃうところがAstroの素晴らしさだと思います。
注意したいのは client:load
ディレクティブです。Astroテンプレートではコンポーネントはすべて静的な要素として出力されるのですが、 client
ディレクティブを付与することでハイドレーションを機能させ、クライアントサイドJSが働くようにしてくれるのです。
client
ディレクティブには load
の他にも idle
visible
media
などがあり、クライアントJSがどのタイミングでブラウザに送信されるかを指定します。より詳しくはドキュメントにて。
まとめ
今現在このブログサイトは Astro で構築されています。
Astro は静的コンテンツが主軸となるWebサイトにはもちろん最適解となり得ますし、フレームワークコンポーネントとの連携で動的なアプリケーションも開発でき、技術的なスケーリングが可能です。そのうえ学習コストが安い。
今後チャンスがあったらぜひ採用してみたいフレームワークです。