Astro&microCMSでブログ作り 第1回

Astro

はじめに

Astroというサイト作成用のフレームワークで、ブログを作成してみます!

Astro
Astro builds fast content sites, powerful web applications, dynamic server APIs, and everything in-between.

CMSにはmicroCMSを使用し、APIと連携して機能を作っていきます!

microCMS|APIベースの日本製ヘッドレスCMS
microCMSはAPIベースの日本製のヘッドレスCMSです。もう社内向け編集/管理画面を自作する必要はありません。開発・運用コストを大きく下げることでビジネスを加速させます。

作成する機能

  • 一覧
  • 詳細
  • カテゴリ
  • お問い合わせフォーム

環境構築

  • Node.js v20.11.0
  • astro 4.8.6

プロジェクト名を決めてディレクトリを作成し、移動します。

mkdir AstroApp
cd AstroApp

早速下記コマンドでセットアップしていきます。

npm create astro@latest

プロジェクト名を「.」に指定し、今いるルート配下にソースを展開します。

今回はブログテンプレートを指定してみます。

TypeScriptを使うのでYes

TypeScriptはStrictモードにします。

依存関係はインストールしておきます。

gitも初期化しておきます。

これで設定が完了し、インストールが始まります。

インストールが終わったら、

npm run dev

で起動します。

http://localhost:4321/

を開きます。

ブログページができていればOKです!

テンプレートを見てみる

VSCodeを使用している場合、

Astro - Visual Studio Marketplace
Extension for Visual Studio Code - Language support for Astro

こちらの拡張機能をインストールしておくとファイルが見やすくなります!

テンプレートの基本

astroのテンプレートファイルはこのような構成をとります👇

---
// Component Script (JavaScript)
---
<!-- Component Template (HTML + JS Expressions) -->
Components
An intro to the .astro component syntax.

コード フェンス「—」の区切りの中に、JavaScriptを記述できるようになっています。

Astro コンポーネントについて知っておくべき最も重要なことは、クライアント上でレンダリングされないことです。ビルド時またはサーバー側レンダリング (SSR)を使用してオンデマンドで HTML にレンダリングされます。

コード フェンスの中に記述したJavaScriptはブラウザ側で動作せず、サーバー側で動作します。

プライベートなAPIを呼び出したり(APIキーなどを隠したい)、DBから直接値を引っ張ってくる、とかもできそうですね。

逆にブラウザ側で動作するJavaScriptを書きたい場合はscriptタグで記述します。

Scripts and Event Handling
How to add client-side interactivity to Astro components using native browser JavaScript APIs.

pages

pages配下に作成したファイルで、そのままパス情報が決まります。

.
├── about.astro
├── blog
│   ├── [...slug].astro
│   └── index.astro
├── index.astro
└── rss.xml.js

ここはNext.jsなどのフレームワークと似ていますね!

about.astroが/about

blog/index.astroが/blog

blog/[…slug].astroがblog詳細ページ

にマッチします。

layouts

ページを構成するレイアウトを作成します。

ヘッダーやフッター、メニューなどを共通コンポーネントにまとめる時に使用します。

---
import BaseHead from '../components/BaseHead.astro';
import Footer from '../components/Footer.astro';
const { title } = Astro.props;
---
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <BaseHead title={title}/>
  </head>
  <body>
    <nav>
      <a href="#">Home</a>
      <a href="#">Posts</a>
      <a href="#">Contact</a>
    </nav>
    <h1>{title}</h1>
    <article>
      <slot /> <!-- your content is injected here -->
    </article>
    <Footer />
  </body>
</html>

slot

子要素を差し込む時にはslotを使用します

<slot />

layoutを使う側から、コンポーネントを差し込むことができます。

---
import MySiteLayout from '../layouts/MySiteLayout.astro';
---
<MySiteLayout title="Home Page">
  <p>My page content, wrapped in a layout!</p>
</MySiteLayout>

((Reactでいうchildrenと同じですね!))

components

再利用したいコンポーネントを記述します。

props

コンポーネントに外から変数を渡すことができます。

---
// Usage: <GreetingHeadline greeting="Howdy" name="Partner" />
const { greeting, name } = Astro.props;
---
<h2>{greeting}, {name}!</h2>

Astro.propsとして受け取ることが可能です。

TypeScriptの場合は、interface Propsをつけて型定義します。

---
interface Props {
  name: string;
  greeting?: string;
}

const { greeting = "Hello", name } = Astro.props;
---
<h2>{greeting}, {name}!</h2>

microCMS側の設定

ここからはmicroCMSで記事を作成してみます。

アカウントを作成し、管理画面にログインした状態から開始します!

「一から作成する」を選択

サービス名を入力し、サービスを作成します。

APIを作成の箇所では、ブログを選んで作成します。

サンプルの投稿が作成されました!

試しに記事を投稿してみます。

投稿しました👇

これでmicroCMSの設定は完了です!

AstroとmicroCMSの連携

microcms-sdkをインストールします

GitHub - microcmsio/microcms-js-sdk: microCMS JavaScript SDK.
microCMS JavaScript SDK. Contribute to microcmsio/microcms-js-sdk development by creating an account on GitHub.
npm install microcms-js-sdk

src配下に、libsディレクトリを作成。その配下にclient.tsを作成します。

mkdir -p src/libs && touch src/libs/client.ts

client.ts

import type { MicroCMSQueries } from "microcms-js-sdk";
import { createClient } from "microcms-js-sdk";

const client = createClient({
    serviceDomain: import.meta.env.MICROCMS_SERVICE_DOMAIN,
    apiKey: import.meta.env.MICROCMS_API_KEY,
});

export type Blog = {
    id: string;
    createdAt: string;
    updatedAt: string;
    publishedAt: string;
    revisedAt: string;
    title: string;
    content: string;
    eyecatch: {
        url: string,
        height: number,
        width: number
    };
    category: {
        id: string;
        name: string;
    };
};
export type BlogResponse = {
    totalCount: number;
    offset: number;
    limit: number;
    contents: Blog[];
};

export const getBlogs = async (queries?: MicroCMSQueries) => {
    return await client.get<BlogResponse>({ endpoint: "blogs", queries });
};
export const getBlogDetail = async (
    contentId: string,
    queries?: MicroCMSQueries
) => {
    return await client.getListDetail<Blog>({
        endpoint: "blogs",
        contentId,
        queries,
    });
};

次に環境変数を設定します。

touch .env

.env

MICROCMS_SERVICE_DOMAIN=xxxxx.microcms.ioのxxxxxの部分
MICROCMS_API_KEY=APIキーを入れる

(PUBLIC_をプレフィックスにつけると、ブラウザ側から閲覧可能。秘匿にしたいので、このままでOK)

念の為、.envがgitには追加されていないことも確認してください。

blog/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";
import { getCollection } from "astro:content";
import FormattedDate from "../../components/FormattedDate.astro";
import { getBlogs } from "../../libs/client";
// 一覧を取得
const blogs = await getBlogs();
console.log(blogs);

const posts = (await getCollection("blog")).sort(
  (a, b) => a.data.pubDate.valueOf() - b.data.pubDate.valueOf()
);
---

blogのindexページをリロードすると、ターミナルにCMSの投稿内容が表示されます。

一覧ページ作成

ここからは一覧をCMSの内容に置き換えていきましょう。

↑デフォルトではこのようになっています。

blog/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";
import FormattedDate from "../../components/FormattedDate.astro";
import { getBlogs } from "../../libs/client";
// 一覧を取得
const blogs = await getBlogs();
---

<!doctype html>
<html lang="en">
  <head>
    <BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
    <style>
   .....略......
    </style>
  </head>
  <body>
    <Header />
    <main>
      <section>
        <ul>
          {
            blogs.contents.map((blog) => (
              <li>
                <a href={`/blog/${blog.id}/`}>
                  <img
                    width={720}
                    height={360}
                    src={blog.eyecatch.url}
                    alt=""
                  />
                  <h4 class="title">{blog.title}</h4>
                  <p class="date">
                    <FormattedDate date={new Date(blog.publishedAt)} />
                  </p>
                </a>
              </li>
            ))
          }
        </ul>
      </section>
    </main>
    <Footer />
  </body>
</html>

CMSの内容が一覧に表示できました!

詳細ページ

次に詳細です。

詳細ページのパスは、/blog/投稿のid

としましょう。

動的ルーティングについて、詳しくはこちら

今回はSSG(静的生成)モードで実装していきましょう。

まずlayouts/BlogPost.astroのPropsを変更します。

type Props = {
  title: string;
  description: string;
  pubDate: Date;
  updatedDate?: Date;
  heroImage: string;
};

[…slug].astroを編集します。

---
import BlogPost from "../../layouts/BlogPost.astro";
import { getBlogDetail, getBlogs } from "../../libs/client";

export async function getStaticPaths() {
  const response = await getBlogs({ fields: ["id"] });
  return response.contents.map((content) => ({
    params: { slug: content.id },
  }));
}

const { slug } = Astro.params;
const blog = await getBlogDetail(slug as string);
---

<BlogPost
  title={blog.title}
  description={"description"}
  pubDate={new Date(blog.publishedAt)}
  updatedDate={new Date(blog.updatedAt)}
  heroImage={blog.eyecatch.url}>{blog.content}</BlogPost
>

このように更新し、一覧から詳細に遷移します。

このように、詳細が表示されるようになりました!

パスも

「http://localhost:4321/blog/投稿id」

のような形式になっているのが確認できると思います!

ただ、HTMLのタグがそのまま埋め込まれているので修正しましょう!

BlogPostのslotに入れる箇所を差し替えます。

<BlogPost
  title={blog.title}
  description={"description"}
  pubDate={new Date(blog.publishedAt)}
  updatedDate={new Date(blog.updatedAt)}
  heroImage={blog.eyecatch.url}
>
  <div set:html={blog.content} />
</BlogPost>

set:htmlで、中身のHTMLをエスケープせずに埋め込むことができます。

Template Directives Reference

divではなくFragmentを使うこともできます。

<Fragment set:html={cmsContent}>

set:htmlon a を使用して<Fragment>、不要なラッパー要素の追加を避けることもできます。これは、CMS から HTML をフェッチする場合に特に便利です。

埋め込みのために、新たなdiv要素を作成する必要がなくなります!

これは便利ですね。

詳細の内容が、問題なく表示されています🎉

カテゴリ

まずはカテゴリの表示から作成します。

layout/BlogPost.astroを編集します。

---
import BaseHead from "../components/BaseHead.astro";
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import FormattedDate from "../components/FormattedDate.astro";

type Props = {
  title: string;
  description: string;
  pubDate: Date;
  updatedDate?: Date;
  heroImage: string;
  category: {
    name: string;
    id: string;
  };
};

const { title, description, pubDate, updatedDate, heroImage, category } =
  Astro.props;
---

カテゴリをHTMLに追加します。

	<a
		href={`/category/${category.id}`}
		style={{
		textDecoration: "none",
		color: "inherit",
		display: "flex",
		justifyContent: "center",
		alignItems: "center",
		margin: "2em auto",
		width: "160px",
		fontWeight: "bold",
		fontSize: "14px",
		padding: "0.5em",
		backgroundColor: "rgb(var(--gray-light))",
		borderRadius: "12px",
		}}
	>
		{category.name}
	</a>
	<h1>{title}</h1>

[…slog.astro]からカテゴリを渡すように変更します。

---

<BlogPost
  title={blog.title}
  description={"description"}
  pubDate={new Date(blog.publishedAt)}
  updatedDate={new Date(blog.updatedAt)}
  heroImage={blog.eyecatch.url}
  category={blog.category}
>
  <Fragment set:html={blog.content} />
</BlogPost>

日付とタイトルの間にカテゴリが追加されました!

次にカテゴリごとの一覧を作りましょう。

libs/client.tsに、カテゴリを取得する関数を追加します。

export type Category = {
    id: string;
    name: string;
};

export type CategoryResponse = {
    totalCount: number;
    offset: number;
    limit: number;
    contents: Category[];
};

export const getCategories = async (queries?: MicroCMSQueries) => {
    return await client.get<CategoryResponse>({ endpoint: "categories", queries });
};

また、Blogの型定義でカテゴリを使用している箇所も、統一しておくと良いでしょう。

export type Blog = {
    id: string;
    createdAt: string;
    updatedAt: string;
    publishedAt: string;
    revisedAt: string;
    title: string;
    content: string;
    eyecatch: {
        url: string,
        height: number,
        width: number
    };
    category: Category
};

詳細画面では、

href={`/category/${category.id}`}

と設定したので、このパスに合うようにページを作成します。

src/pages/category/[id].astro

---
import BaseHead from "../../components/BaseHead.astro";
import Footer from "../../components/Footer.astro";
import FormattedDate from "../../components/FormattedDate.astro";
import Header from "../../components/Header.astro";
import { SITE_DESCRIPTION, SITE_TITLE } from "../../consts";
import { getBlogs, getCategories } from "../../libs/client";

export async function getStaticPaths() {
  const response = await getCategories({ fields: ["id"] });
  return response.contents.map((content) => ({
    params: { id: content.id },
  }));
}

const { id } = Astro.params;
const blogs = await getBlogs({
  filters: `category[equals]${id}`,
});
---

<!-- ここから下はトップページと全く同じです。 -->
<!doctype html>
<html lang="en">
  <head>
    <BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
    <style>
      ...略...
    </style>
  </head>
  <body>
    <Header />
    <main>
      <section>
        <ul>
          {
            blogs.contents.map((blog) => (
              <li>
                <a href={`/blog/${blog.id}/`}>
                  <img
                    width={720}
                    height={360}
                    src={blog.eyecatch.url}
                    alt=""
                  />
                  <h4 class="title">{blog.title}</h4>
                  <p class="date">
                    <FormattedDate date={new Date(blog.publishedAt)} />
                  </p>
                </a>
              </li>
            ))
          }
        </ul>
      </section>
    </main>
    <Footer />
  </body>
</html>

getBlogsで

filters: `category[equals]${id}`

のようにカテゴリで絞り込んでいます。

HTMLのところは一覧と全く同じです。

これで詳細ページのカテゴリをクリックすると

「http://localhost:4321/category/カテゴリid」

に遷移し、カテゴリごとの一覧が表示されるはずです。

まとめ

これでブログの表示に必要な機能を作成できました!

Astroを触ってみた感想ですが、他の静的生成フレームワークにはない、かゆいところに手が届く印象です!

次回はこのブログにお問い合わせフォームを実装していきます!