はじめに
Astroというサイト作成用のフレームワークで、ブログを作成してみます!
CMSにはmicroCMSを使用し、APIと連携して機能を作っていきます!
作成する機能
- 一覧
- 詳細
- カテゴリ
- お問い合わせフォーム
環境構築
- 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
で起動します。
を開きます。
ブログページができていればOKです!
テンプレートを見てみる
テンプレートの基本
astroのテンプレートファイルはこのような構成をとります👇
---
// Component Script (JavaScript)
---
<!-- Component Template (HTML + JS Expressions) -->
コード フェンス「—」の区切りの中に、JavaScriptを記述できるようになっています。
コード フェンスの中に記述したJavaScriptはブラウザ側で動作せず、サーバー側で動作します。
プライベートなAPIを呼び出したり(APIキーなどを隠したい)、DBから直接値を引っ張ってくる、とかもできそうですね。
逆にブラウザ側で動作するJavaScriptを書きたい場合はscriptタグで記述します。
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をインストールします
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をエスケープせずに埋め込むことができます。
divではなくFragmentを使うこともできます。
<Fragment set:html={cmsContent}>
set:html
on 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を触ってみた感想ですが、他の静的生成フレームワークにはない、かゆいところに手が届く印象です!
次回はこのブログにお問い合わせフォームを実装していきます!