Astro でブログ再構築したので概要を紹介

Astro でブログ再構築したので概要を紹介

岩佐 孝浩
岩佐 孝浩
13 min read
Astro Tailwind CSS

最近少しトレンドになっている Astro を利用してブログを再構築したので、 Astro の概要について紹介します。

Astro 概要

コンテンツ重視型 SSG (Static Site Generator) です。 公式サイトでは、以下のように紹介されています。

Astro is an all-in-one web framework for building fast, content-focused websites.

類似のツールとして、 HugoJekyll 等が有名ですが、以下が Astro の最大の特徴だと感じました。 特に二つ目の Zero JS, by default は、今後他のフレームワークでも採用が進むかもしれません。 詳細は公式ページをご参照ください。

  • Astro Islands: ページ内のエリアを Island として分離し、それぞれ HTML, React, Vue 等を利用可能
  • Zero JS, by default: ビルドで可能な限り HTML に変換し、JavaScript を生成しないことで高速化

Play with Astro

公式から提供されているブログテンプレートを利用したセットアップを紹介します。 公式チュートリアルも提供されているので、そちらもご参考ください。

セットアップ

npm create astro@latest を実行してください。 後はウィザードに従って入力すると、セットアップ完了です。

npm run dev または astro dev で、開発用サーバーを起動できます。 デフォルトは、 http://localhost:4321/ でリクエストをリスンしています。

npm create astro@latest

 astro   Launch sequence initiated.

   dir   Where should we create your new project?
         ./

  tmpl   How would you like to start your new project?
         Use blog template
 ██████  Template copying...

  deps   Install dependencies?
         Yes
 ██████  Installing dependencies with npm...

    ts   Do you plan to write TypeScript?
         Yes

   use   How strict should TypeScript be?
         Strict
 ██████  TypeScript customizing...

   git   Initialize a new git repository?
         Yes
 ██████  Git initializing...

  next   Liftoff confirmed. Explore your project!
         Run npm run dev to start the dev server. CTRL+C to stop.
         Add frameworks like react or tailwind using astro add.

         Stuck? Join us at https://astro.build/chat

╭─────╮  Houston:
│ ◠ ◡ ◠  Good luck out there, astronaut! 🚀
╰─────╯

ディレクトリ構成

インストール直後のディレクトリ構成です。 フロントエンド開発の経験があれば、ディレクトリ構成を見ると、それぞれの役割が分かると思います。 詳細は公式ページをご参照ください。

tree --dirsfirst -I dist -I node_modules

.
├── public
│   ├── fonts
│   │   ├── atkinson-bold.woff
│   │   └── atkinson-regular.woff
│   ├── blog-placeholder-1.jpg
│   ├── blog-placeholder-2.jpg
│   ├── blog-placeholder-3.jpg
│   ├── blog-placeholder-4.jpg
│   ├── blog-placeholder-5.jpg
│   ├── blog-placeholder-about.jpg
│   └── favicon.svg
├── src
│   ├── components
│   │   ├── BaseHead.astro
│   │   ├── Footer.astro
│   │   ├── FormattedDate.astro
│   │   ├── Header.astro
│   │   └── HeaderLink.astro
│   ├── content
│   │   ├── blog
│   │   │   ├── first-post.md
│   │   │   ├── markdown-style-guide.md
│   │   │   ├── second-post.md
│   │   │   ├── third-post.md
│   │   │   └── using-mdx.mdx
│   │   └── config.ts
│   ├── layouts
│   │   └── BlogPost.astro
│   ├── pages
│   │   ├── blog
│   │   │   ├── [...slug].astro
│   │   │   └── index.astro
│   │   ├── about.astro
│   │   ├── index.astro
│   │   └── rss.xml.js
│   ├── styles
│   │   └── global.css
│   ├── consts.ts
│   └── env.d.ts
├── README.md
├── astro.config.mjs
├── package-lock.json
├── package.json
└── tsconfig.json

Content Collections

概要

ブログ記事等のコンテンツは、コレクションとして管理します。 詳細は公式ページをご参照ください。

src/content/ の下に任意のディレクトリを作成し、そのディレクトリの中に Markdown ファイルを配置します。 MDX も利用可能です。 以下は、 blog/first-post.md の一部です。

---
title: 'First post'
description: 'Lorem ipsum dolor sit amet'
pubDate: 'Jul 08 2022'
heroImage: '/blog-placeholder-3.jpg'
---

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
...

コレクション定義

src/content/config.ts の中で、 Content のメタデータ (frontmatter) や型を Zod を利用して定義します。 コレクションを定義しなくても利用可能ですが、公式ページで以下のように言及されているとおり、メリットをスポイルするのでコレクションを定義することを強くお勧めします。

The src/content/config.ts file is optional. However, choosing not to define your collections will disable some of their best features like frontmatter schema validation or automatic TypeScript typings.

以下は、セットアップ直後の config.ts です。

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

const blog = defineCollection({
  // Type-check frontmatter using a schema
  schema: z.object({
    title: z.string(),
    description: z.string(),
    // Transform string to Date object
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    heroImage: z.string().optional(),
  }),
});

export const collections = { blog };
キー必須
titlestringY
descriptionstringY
pubDateDateY
updatedDateDateN
heroImagestringN

コレクション利用

getCollectiongetEntry 等の関数を利用してコレクションにアクセスできます。

Frontmatter は、 data の中に格納されています。 例えば、以下のようなコードでアクセスできます。

---
import { getEntry } from 'astro:content';

const blogPost = await getEntry('blog', 'welcome');
---

<h1>{blogPost.data.title}</h1>
<p>{blogPost.data.description}</p>

ページ

ルーティング

ファイルベースです。 pages の下に置いたファイルがそのままルーティング設定になります。 セットアップ直後の例だと、以下のようにアクセスできます。

ファイルURL
pages/index.astrohttps://<YOUR_DOMAIN>/
pages/about.astrohttps://<YOUR_DOMAIN>/about/
pages/blog/index.astrohttps://<YOUR_DOMAIN>/blog/

ダイナミックルーティングも可能です。 [...NAME] をディレクトリ名やファイル名として利用します。 セットアップ直後の例だと、 blog/[...slug].astro がダイナミックルーティングを利用しています。

[...slug] の部分は、ファイルの中で getStaticPaths 関数の戻り値で定義します。 blog/[...slug].astro の例だと、8行目で slug: post.slug を設定しています。

---
import { type CollectionEntry, getCollection } from 'astro:content';
import BlogPost from '../../layouts/BlogPost.astro';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map((post) => ({
    params: { slug: post.slug },
    props: post,
  }));
}
type Props = CollectionEntry<'blog'>;

const post = Astro.props;
const { Content } = await post.render();
---

<BlogPost {...post.data}>
  <Content />
</BlogPost>

ページ構成

ページ、レイアウト、コンポーネントは、一般的なフロントエンドと同様の構成です。 もちろん、ページの中で直接コンポーネントを配置するという使い方もできます。

Astro コンポーネント

React JSX/TSX を利用したことがあれば、違和感なく扱えると思います。 以下は、 pages/index.astro の一部です。 詳細は公式ページをご参照ください。

---
import BaseHead from "../components/BaseHead.astro";
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import { SITE_TITLE, SITE_DESCRIPTION } from "../consts";

// This will cause an error.
// const main = document.querySelector('main');
---

<!doctype html>
<html lang="en">
  <head>
    <BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
  </head>
  <body>
    <Header title={SITE_TITLE} />
    <main>
      <h1>🧑‍🚀 Hello, Astronaut!</h1>
      <!-- ... -->
    </main>
    <Footer />

    <script>
      // Use document, windoww, etc. here.
      const main = document.querySelector('main');
    </script>
  </body>
</html>

重要なポイントとして、 --- の中は Node.js で処理されるという点です。 これは、冒頭に書いた「ビルドで可能な限り HTML に変換し、JavaScript を生成しないことで高速化」と関係があります。 したがって、 document, window 等を利用すると以下のようなエラーになります。利用したい場合、<script> タグの中に記述してください。

 error   document is not defined
  Hint:
    Browser APIs are not available on the server.

    Move your code to a <script> tag outside of the frontmatter, so the code runs on the client.

    See https://docs.astro.build/en/guides/troubleshooting/#document-or-window-is-not-defined for more information.

レンダー

Content というコンポーネントを利用します。

CollectionEntry から Content を取得できます。 以下は、 pages/blog/[...slug].astro の一部です。 15行目でレンダーした内容を取得し、19行目で Content を任意の場所に配置しています。

---
import { type CollectionEntry, getCollection } from 'astro:content';
import BlogPost from '../../layouts/BlogPost.astro';

export async function getStaticPaths() {
	const posts = await getCollection('blog');
	return posts.map((post) => ({
		params: { slug: post.slug },
		props: post,
	}));
}
type Props = CollectionEntry<'blog'>;

const post = Astro.props;
const { Content } = await post.render();
---

<BlogPost {...post.data}>
	<Content />
</BlogPost>

直接 Markdown を import する場合、以下のように利用できます。

---
import {Content as PromoBanner} from '../components/promoBanner.md';
---

<h2>Today's promo</h2>
<PromoBanner />

ビルド

npm run build または astro build を実行すると、 dist/ ディレクトリにビルドアーティファクトが出力されます。 以下は、セットアップ直後にビルドした場合のアーティファクトです。 ご覧のとおり、JavaScript が生成されていません。

tree dist/ --dirsfirst

dist/
├── about
│   └── index.html
├── blog
│   ├── first-post
│   │   └── index.html
│   ├── markdown-style-guide
│   │   └── index.html
│   ├── second-post
│   │   └── index.html
│   ├── third-post
│   │   └── index.html
│   ├── using-mdx
│   │   └── index.html
│   └── index.html
├── fonts
│   ├── atkinson-bold.woff
│   └── atkinson-regular.woff
├── blog-placeholder-1.jpg
├── blog-placeholder-2.jpg
├── blog-placeholder-3.jpg
├── blog-placeholder-4.jpg
├── blog-placeholder-5.jpg
├── blog-placeholder-about.jpg
├── favicon.svg
├── index.html
├── rss.xml
├── sitemap-0.xml
└── sitemap-index.xml

その他のトピック

Image コンポーネント

Astro 公式から、 Image というコンポーネントが提供されています。 src/ の下に配置された画像を Image コンポーネントで利用すると、ビルドのタイミングで最適化 (webp に変換) してくれます。 public/ の下に配置された画像は最適化されないため、 src/ に置くことが推奨されています。

We recommend that local images are kept in src/ when possible so that Astro can transform, optimize and bundle them.

Tailwind CSS

Astro は、 Tailwind CSS をサポートしています。 npx astro add tailwind で Integration をインストールしてください。

npx astro add tailwind

✔ Resolving packages...

  Astro will run the following command:
  If you skip this step, you can always run it yourself later

 ╭────────────────────────────────────────────────────╮
 │ npm install @astrojs/tailwind tailwindcss@^3.0.24  │
 ╰────────────────────────────────────────────────────╯

✔ Continue? … yes
✔ Installing dependencies...

  Astro will generate a minimal ./tailwind.config.mjs file.

✔ Continue? … yes

  Astro will make the following changes to your config file:

 ╭ astro.config.mjs ───────────────────────────────╮
 │ import { defineConfig } from 'astro/config';    │
 │ import mdx from '@astrojs/mdx';                 │
 │ import sitemap from '@astrojs/sitemap';         │
 │                                                 │
 │ import tailwind from "@astrojs/tailwind";       │
 │                                                 │
 │ // https://astro.build/config                   │
 │ export default defineConfig({                   │
 │   site: 'https://example.com',                  │
 │   integrations: [mdx(), sitemap(), tailwind()]  │
 │ });                                             │
 ╰─────────────────────────────────────────────────╯

✔ Continue? … yes

   success  Added the following integration to your project:
  - @astrojs/tailwind

astro add tailwind は、以下を実行しています。

  1. npm install @astrojs/tailwind [email protected]
  2. tailwind.config.mjs (Tailwind CSS 設定ファイル) 生成
  3. astro.config.mjsintegrationstailwind() を追加

Autoprefixer も一緒にインストールされます。

あとは、以下のように class に Tailwind で提供されているクラスを書くだけです。

<body>
  <Header />
  <main class="ml-2">
    ...
  </main>
  <Footer />
</body>

Integrations

Integration という仕組みで機能を拡張できます。 上記の Tailwind CSS サポートも Integration の一つです。 詳細は公式ページをご参照ください。

IDE サポート

IDE提供元安定性
VS CodeAstro安定している
JetBrainsJetBrains2023年11月時点ではやや不安定でした

まとめ

Astro IslandsZero JS, by default という考え方は、今後の SSG の大きなトレンドになるかもしれません。

この投稿が、お役に立てば幸いです。

岩佐 孝浩

岩佐 孝浩

Software Developer at KAKEHASHI Inc.
AWS を活用したクラウドネイティブ・アプリケーションの要件定義・設計・開発に従事。 株式会社カケハシで、処方箋データ収集の新たな基盤の構築に携わっています。 Japan AWS Top Engineers 2020-2023