前回は「👈 Astro x microcms JAMSTACKブログ構築 - 環境設定」
「SRC」で開発を行なう
ポイントとなる覚書
第1階層について
📦src
┣ 📂 components //UI関連使いまわし
┣ 📂 icons //svgアイコン等
┣ 📂 layouts //Header,Footer,Layoutなどレイアウト類
┣ 📂 pages //home,archive,post,static-page
┣ 📂 scss //共通cssや使い回し
┗ 📜 content.config.ts //microCMSのデータを読み込むための、Content Loader「content.config.ts」ここにサイト内共通の情報やメソッドを用意する
import { defineCollection, z } from "astro:content";//「コンテンツコレクション(Content Collections)」 という機能を使い、ブログ記事やマークダウン(Markdown/MDX)ファイルを安全かつ便利に管理・型定義(TypeScript化)するために必須となるモジュール
import { createClient } from "microcms-js-sdk";//microCMS JavaScript SDKをインストール
// サイト共通設定 - 様々な箇所で呼び出す
export const siteConfig = {
siteName: "ブログの名前",
siteUrl: "https://------.com",
defaultDescription:"デスクリプション",
subcopy: "サブコピー",
defaultImage: "/assets/og.png",
locale: "ja_JP",
};
//型宣言
export type PageMeta = {
title: string;
description?: string;
ogType?: "website" | "article";
ogImage?: string;
canonicalUrl?: string;
};
//microCMSとの接続
export const client = createClient({
serviceDomain: import.meta.env.MICROCMS_SERVICE_DOMAIN,
apiKey: import.meta.env.MICROCMS_API_KEY,
});
// microCMSのコンテンツローダー
const microCMSLoader = (endpoint: string) => {
return async () => {
try {
console.log(`microCMSから${endpoint}データを取得中...`);
const response = await client.getAllContents({
endpoint,
});
console.log(`${response.length}件の${endpoint}を取得しました`);
return response;
} catch (error) {
console.error(`microCMSからの${endpoint}取得に失敗:`, error);
return [];
}
};
};
// 共通のフィールド
const microCMSDateFields = {
createdAt: z.string(),
updatedAt: z.string(),
publishedAt: z.string(),
revisedAt: z.string(),
};
export function filterBlogsByIds<T extends { id: string }>(
blogs: T[],
ids: readonly string[],
limit = 5,
) {
return blogs.filter((blog) => ids.includes(blog.id)).slice(0, limit);
}
// コレクションの定義
// 基本的にはテンプレートを使用。新しいものを追加するなどはGemini等で聞きながら構築を行なった。
const blog = defineCollection({
loader: microCMSLoader("blog"),
schema: z
.object({
title: z.string(),
content: z.string(),
insert_toc: z.boolean().optional(),
eyecatch: z
.object({
url: z.string(),
height: z.number().optional(),
width: z.number().optional(),
})
.optional()
.nullable(),
categories: z
.array(
z
.object({
id: z.string(),
name: z.string(),
})
.passthrough(),
)
.optional()
.nullable(),
tags: z
.array(
z
.object({
id: z.string(),
name: z.string(),
})
.passthrough(),
)
.optional()
.nullable(),
...microCMSDateFields,
})
.passthrough(),
});
/**
* カテゴリーコレクションの定義
* カテゴリー一覧ページで使用するために追加
*/
const categories = defineCollection({
loader: microCMSLoader("categories"),
schema: z
.object({
name: z.string(), // カテゴリー名
...microCMSDateFields,
})
.passthrough(),
});
const tags = defineCollection({
loader: microCMSLoader("tags"),
schema: z
.object({
name: z.string(), // カテゴリー名
...microCMSDateFields,
})
.passthrough(),
});
// コレクションのエクスポート
export const collections = {
blog,
categories,
tags,
};
// 型定義
export type Blog = z.infer<typeof blog.schema>;全ページ設定が反映する「Layout.astro」
Meta出力をSEOを意識した構造
---
import "@scss/_index.scss";//全体スタイルを構築
import { siteConfig, type PageMeta } from "../content.config";//サイト基本設定
import Header from "@layouts/Header.astro";//ヘッダーモジュール
import Footer from "@layouts/Footer.astro";//フッターモジュール
〜〜〜〜〜〜〜〜
//型宣言
interface Props extends PageMeta {
title: string;
description?: string;
ogType?: "website" | "article";
ogImage?: string;
canonicalUrl?: string;
bodyClassName?: string;
}
//Props設定
const {
title,
description = siteConfig.defaultDescription,
ogType = "website",
ogImage = siteConfig.defaultImage,
canonicalUrl,
bodyClassName = "",
} = Astro.props as Props;
//ページタイトル
const fullTitle = `${title} | ${siteConfig.siteName}`;
//URL設定
const pageUrl = new URL(
Astro.url.pathname,
`${siteConfig.siteUrl}${Astro.url.pathname}`,
);
〜〜〜〜〜〜〜〜
---
<!doctype html>
<html lang="ja" data-open-menu="false" data-header-show="true">
<head prefix={`og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# ${ogType}: http://ogp.me/ns/${ogType}#`}>
<script is:inline data-astro-rerun>//「Astro」において、ページ遷移(View Transitions)後もスクリプトを毎回再実行させ、かつコードをバンドルせずそのままHTMLに埋め込むための記述
/*タグマネージャーのコード*/
</script>
<title>{fullTitle}</title>//ページタイトル
<!-- OGP タグ -->
<meta property="og:type" content={ogType} />
<meta property="og:site_name" content={siteConfig.siteName} />
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(ogImage, siteConfig.siteUrl).toString()} />
<meta property="og:url" content={pageUrl} />
<meta property="og:locale" content={siteConfig.locale} />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={fullTitle} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={new URL(ogImage, siteConfig.siteUrl).toString()}/>
<link rel="canonical" href={pageUrl} />
<meta property="og:image" data-pagefind-meta="image[content]" content={pageUrl} />
<ClientRouter />
</head>トップページ記事の呼び出し
---
import { getCollection } from "astro:content";
import Layout from "@layouts/Layout.astro";
import ArticleCard from "@components/blog/ArticleCard.astro";
import { Image } from "astro:assets";
// 全ブログ記事を取得
const allBlogs = await getCollection("blog");
// ブログをソート(新しい順)
const sortedBlogs = allBlogs.sort((a, b) => {
return (
new Date(b.data.publishedAt).getTime() -
new Date(a.data.publishedAt).getTime()
);
});
const ITEMS_PER_PAGE = 12; // 1ページあたりの記事数
const currentPage = 1;
const currentPageBlogs = sortedBlogs;
const totalPages = Math.ceil(currentPageBlogs.length / ITEMS_PER_PAGE);
// 現在のページのデータをスライス
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const endIndex = startIndex + ITEMS_PER_PAGE;
const pagedBlogs = currentPageBlogs.slice(startIndex, endIndex);
〜〜〜〜〜〜〜〜〜〜〜〜〜〜
---
<Layout title="ホーム" ogType="website" bodyClassName="p-home">//メタを設定
<main>
<div class="c-container">
{
//件数文の記事を出力
pagedBlogs.length > 0 &&
pagedBlogs.map((blog) => (
//ArticleCardcコンポーネントへ
<ArticleCard
id={blog.id}
title={blog.data.title}
publishedAt={blog.data.publishedAt}
imageUrl={blog.data.eyecatch?.url || "/assets/no-image.webp"}
categories={blog.data.categories}
tags={blog.data.tags}
articleClass="page"
/>
))
}
</div>
</main>
</Layout>
<script>
//このページのみのjsを直接記述
import { Splide } from "@splidejs/splide";
import { AutoScroll } from "@splidejs/splide-extension-auto-scroll";
import { Intersection } from "@splidejs/splide-extension-intersection";
const initFunc = () => {
~~~~~~~~~~~~~~~~
}
//「DOMContentLoaded」ではなく「ClientRouter」を使用している場合「astro:page-load」で行うとこのらしい
document.addEventListener("astro:page-load", initFunc);//Astroのページ遷移完了時
~~~~~~~~~~~~~~~~
<style lang="scss">//「lang="scss"」で「scss」が使える
//このページのみのcssを直接記述
~~~~~~~~~~~~~~~~
</style>記事サムネイルを表示するコンポーネント
---
import { Image } from "astro:assets";
//型宣言
interface Props {
id: string;
title: string;
publishedAt: string;
imageUrl: string;
categories?: { id: string; name: string; [key: string]: any }[] | null;
tags?: { id: string; name: string; [key: string]: any }[] | null;
articleClass?: string;
}
//Props
const {
id,
title,
publishedAt,
imageUrl,
categories,
tags,
articleClass,
} = Astro.props;
//日付の調整
const date = new Date(publishedAt);
// 年と月を取得して「YYYY.MM」の形式にする
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0"); // 月は0から始まるため+1
const year_month = `${year}.${month}`; // 結果: "2026.05"
// 日を取得して「DD」の形式にする
const day = String(date.getDate()).padStart(2, "0"); // 結果: "08"
// 出力結果: "2026年5月8日"
const formattedDate = `${year}年${month}月${date.getDate()}日`;
---
<article
class=`article article__${articleClass}`
data-category={categories && categories[0].id}
>
<div class="article__inner">
<span class="article__figure-wrapper">
<figure class="article__figure">
//Astro Imageオブジェクトを利用
<Image
width={1200}
height={630}
src={imageUrl}
alt={title}
loading="lazy"
decoding="async"
/>
</figure></span
>
<hgroup class="article__hgroup">
//投稿日を出力
<time
class="article__hgroup__time"
datetime={formattedDate}
aria-label={formattedDate}
>
{year_month}
<span>{day}</span>
</time>
<div class="article__hgroup__meta">
//カテゴリーを出力
{
categories && (
<ul class="article__hgroup__category">
{categories.map((category) => (
<li>
<CategoryButton id={category.id} name={category.name} />
</li>
))}
</ul>
)
}
//記事タイトル&リンク設定
<a class="article__hgroup__title" href={`/${id}/`}
><em><span>{title}</span></em></a
>
//タグを出力
{
tags && (
<ul class="article__hgroup__tag">
{tags.map((tag) => (
<li>
<TagButton id={tag.id} name={`#${tag.name}`} />
</li>
))}
</ul>
)
}
</div>
</hgroup>
</div>
</article>
<style lang="scss">
~~~~~~~~~~~
</style>
次回は、難しと感じた箇所とこだわりをまとめたいと思います。