AstroとmicroCMSでブログを作成してみた
はじめに
AstroとmicroCMSでブログを作成しました。(※このブログのことです。)
記事ではブログを作成する流れを解説していきます。
技術スタックと機能は以下のとおりです。
技術スタック
- CMS:microCMS
- フレームワーク:Astro
- デプロイ先:Cloudflare
機能
- タグ一覧
- 目次
- シンタックスハイライト
- ダークモード
構成
microCMSのデータ構成
blogs:titleに記事タイトル、bodyにリッチエディタの本文、tagsにタグへの複数コンテンツ参照を持たせます。
{
"contents": [
{
"id": "ekgrhcgdn",
"createdAt": "2026-07-30T07:04:11.041Z",
"updatedAt": "2026-07-30T07:04:11.041Z",
"publishedAt": "2026-07-30T07:04:11.041Z",
"revisedAt": "2026-07-30T07:04:11.041Z",
"title": "Astro × microCMS でブログを作る",
"body": "<p>Astroの静的サイト生成と、microCMSのようなヘッドレスCMSを組み合わ~~",
"tags": [
{
"id": "uy6bm2xa6",
"createdAt": "2024-11-14T00:48:21.273Z",
"updatedAt": "2025-08-29T08:02:59.725Z",
"publishedAt": "2024-11-14T00:48:21.273Z",
"revisedAt": "2025-08-29T08:02:59.725Z",
"title": "JavaScript",
"slug": "javascript"
}
]
}
],
"totalCount": 1,
"offset": 0,
"limit": 10
}
tags:titleに表示名、slugにURLで使う文字列を持たせます。
{
"contents": [
{
"id": "hj5k20l8f",
"createdAt": "2024-11-14T00:47:32.908Z",
"updatedAt": "2025-03-25T06:24:11.761Z",
"publishedAt": "2024-11-14T00:47:32.908Z",
"revisedAt": "2025-03-25T06:24:11.761Z",
"title": "Accessibility",
"slug": "a11y"
}
],
"totalCount": 1,
"offset": 0,
"limit": 10
}
Astroのファイル構成
src/
├── content.config.ts # microCMSの取得処理とスキーマ
├── components/
│ ├── BaseHead.astro # メタ情報、共通CSS、表示設定の初期化
│ ├── ArticlesList.astro # 記事一覧
│ ├── TagsList.astro # タグナビゲーション
│ ├── TableOfContents.astro # 目次
│ └── ThemeToggle.astro # 配色の切替
├── layouts/
│ └── BlogPost.astro # 記事の共通レイアウト
├── lib/
│ └── microcms.ts # CMS本文のHTML加工
├── pages/
│ ├── index.astro # トップページ
│ ├── about.astro # ブログ・著者の紹介
│ ├── blog/[...slug].astro # 記事詳細
│ ├── tags/[slug].astro # タグ別記事一覧
│ └── rss.xml.js # RSS
├── scripts/ # 目次メニューや画像拡大の操作
└── styles/global.css # 共通スタイルとテーマ変数
ページの構成
ページの役割は次のように分かれています。
- /:公開日時の新しい順に記事を並べ、使われているタグを表示
- /tags/{slug}/:指定したタグを持つ記事を表示
- /blog/{id}/:記事本文、公開日、タグ、目次、関連記事を表示
- /about/:リポジトリ内に記述したブログと著者の紹介を表示
- /rss.xml:記事情報からRSSを生成
トップページとタグページは、どちらもTagsListとArticlesListを使います。
記事詳細はBlogPostレイアウトに本文を渡し、関連記事の表示にもArticlesListを再利用しています。
実装
記事一覧の取得
microCMSへのアクセスをまとめているのが、src/content.config.tsのmicroCMSLoaderです。
const microCMSLoader = (endpoint: string) => {
return async () => {
const contents = await client.getAllContents({ endpoint });
return contents;
};
};
エンドポイント名を受け取り、その全件取得を行う非同期関数を返します。
実際の実装には取得開始・件数のログと、失敗したエンドポイントが分かるエラー処理も加えています。
このloaderを、データの形を表すschemaと一緒にコレクションへ登録します。
const blogs = defineCollection({
loader: microCMSLoader('blogs'),
schema: z.object({
title: z.string(),
body: z.string(),
tags: z.array(tagSchema).optional().default([]),
...microCMSDateFields,
}),
});
const tags = defineCollection({
loader: microCMSLoader('tags'),
schema: tagSchema,
});
export const collections = { blogs, tags };
loaderが取り込んだデータは、AstroのContentCollectionsを通して利用します。
schemaを定義することで、データの検証とページ側で使う型を揃えられます。
仕組みの詳細はAstroのContentCollectionsガイドで確認できます。
トップページではgetCollection('blogs')で記事を取り出し、公開日時の降順に並べます。
const posts = (await getCollection('blogs')).sort(
(a, b) =>
new Date(b.data.publishedAt).valueOf() -
new Date(a.data.publishedAt).valueOf(),
);
const usedTagSlugs = new Set(
posts.flatMap((post) => post.data.tags.map((tag) => tag.slug)),
);
const tags = (await getCollection('tags'))
.filter((tag) => usedTagSlugs.has(tag.data.slug))
.sort((a, b) => a.data.title.localeCompare(b.data.title, 'ja'));
タグ一覧は、記事で使っているslugをSetに集めてから絞り込んでいます。
これにより、記事に使っていないタグはナビゲーションに出ません。
残ったタグは、日本語の表示名で並べ替えます。
取得した記事はArticlesListに渡します。
一覧のリンクは/blog/${post.id}/で、タイトル・公開年・タグが一つの行にまとまっています。
現在の実装にはページネーションはなく、取得した記事を全件表示します。
ここでのgetCollection()は、ページから取り込み済みのコレクションを読む処理です。
タグ別記事一覧の取得
タグページはsrc/pages/tags/[slug].astroで生成します。
静的サイトで動的ルートを使う場合、getStaticPaths()で生成対象のURLを列挙します。
この仕組みはAstroのルーティングガイドに説明されています。
このリポジトリでは、トップページと同じく使用中のタグだけを対象にしています。
allTagsにそのタグ一覧、postsに全記事が入った状態で、次のように生成対象を返します。
return allTags.map((tag) => ({
params: { slug: tag.data.slug },
props: {
tag,
tags: allTags,
posts: posts
.filter((post) =>
post.data.tags.some((t) => t.slug === tag.data.slug),
)
.sort(
(a, b) =>
new Date(b.data.publishedAt).valueOf() -
new Date(a.data.publishedAt).valueOf(),
),
},
}));
params.slugがURLの[slug]に対応し、propsが各ページの表示データになります。
記事のタグ配列に同じslugが一つでもあれば、そのタグの記事として採用します。
タグの判定や並べ替えはビルド時に行われます。
TagsListにはcurrentSlugも渡し、現在のタグにcurrentクラスとaria-current="page"を付けます。
直接URLを開いた時点で、選択状態を含んだHTMLが表示される構成です。
記事本文の取得
記事詳細のsrc/pages/blog/[...slug].astroも、getStaticPaths()で全記事のページを生成します。
export async function getStaticPaths() {
const posts = await getCollection('blogs');
return posts.map((post) => ({
params: { slug: post.id },
props: post,
}));
}
type Props = CollectionEntry<'blogs'>;
const post = Astro.props;
ファイル名は[...slug].astroですが、渡している値はpost.idです。
本文のbodyはHTML文字列です。 表示する前に、src/lib/microcms.tsの関数で加工しています。
const { html: htmlWithIds, headings } = withHeadingIds(post.data.body);
const html = withOrderedListIds(await highlightCodeBlocks(htmlWithIds));
const description = plainTextExcerpt(post.data.body);
withHeadingIds()は本文中のh2とh3を抽出し、見出しの階層・ID・テキストを目次用データにまとめます。
既存のIDは残し、IDのない見出しにはheading-1のような連番を付けます。
続いてコードブロックをShikiで変換し、withOrderedListIds()で順序付きリストの項目にref-1のようなIDを付けます。
リスト項目も、ページ内リンクの参照先として使えるようになります。
plainTextExcerpt()は元の本文からHTMLタグを取り除き、空白をまとめて、標準では120文字までの説明文を作ります。
この説明文は記事のメタ情報に使い、RSSでも同じ関数を利用しています。
加工後のHTMLは、BlogPostのスロットに渡します。
<div set:html={html} />set:htmlに渡すことで、本文をHTMLとして描画します。
この処理にはサニタイズは含まれておらず、現在の実装は管理されたCMSの本文を出力する前提です。
BlogPost.astroは本文に加えて、タイトル、公開日、タグ、目次、スクロール中に使うContentsメニューを配置します。
本文の基本スタイルにはgithub-markdown-cssを利用し、見出しやリスト、コードブロックの見た目を追加のCSSで調整しています。
記事末尾の関連記事は、現在の記事と共通タグを持つものを新しい順に最大5件選びます。
候補が2件以上なら「RelatedArticles」として表示し、それ未満なら、現在の記事を除く最新記事を最大5件「LatestArticles」として表示します。
シンタックスハイライトを導入
microCMSの本文はHTMLなので、AstroのMarkdown処理を通りません。
そのため、astro.config.mjsにMarkdown用のShiki設定を書くだけでは、CMS本文中のコードには適用されません。
そこでhighlightCodeBlocks()が本文中の<pre><code>...</code></pre>を探し、ShikiのcodeToHtml()に渡しています。
import { bundledLanguages, codeToHtml } from 'shiki';
import astroConfig from '../../astro.config.mjs';
const shikiThemes = astroConfig.markdown!.shikiConfig!.themes!;
テーマ設定はAstroの設定ファイルから読み込みます。
Markdown用とCMS本文用で同じ設定を参照するため、変更箇所を一つにまとめられます。
現在は両方の配色モードにgithub-dark-high-contrastを指定しています。
markdown: {
shikiConfig: {
themes: {
light: 'github-dark-high-contrast',
dark: 'github-dark-high-contrast',
},
},
},
変換時はpreまたはcodeの属性にあるlanguage-xxxから言語を判定します。
bundledLanguagesにない値や言語指定のないコードはplaintextとし、ハイライト処理が失敗した場合もplaintextで再処理します。
コード内の<や>などは、Shikiに渡す前にデコードします。
ダークモードを導入
配色の初期化は、全ページで使うBaseHead.astroにまとめています。
保存済みの設定を優先し、保存値がなければOSの設定を参照します。
function resolve() {
const stored = localStorage.getItem('color-scheme');
if (stored === 'light' || stored === 'dark') return stored;
return matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
決定した値は、htmlのdata-themeとstyle.colorScheme、color-schemeのメタ情報へ反映します。
head内のインラインスクリプトで早い段階に適用し、初期表示の配色が切り替わるちらつきを抑える狙いです。
共通CSSは、配色を変数として定義しています。
--color-ink: light-dark(#0f1219, #e6edf3);
--color-page: light-dark(#ffffff, #0d1117);
--color-border: light-dark(#000000, #e6edf3);
背景・文字・罫線をこれらの変数で指定することで、同じレイアウトのまま配色を切り替えます。
本文に使うgithub-markdown-cssには、ライト用とダーク用のCSSをそれぞれ用意しています。
設定を適用する関数は、対応するlink要素のmediaとdisabledも更新します。
サイト全体だけでなく、記事本文の配色も手動選択に合わせるためです。
ThemeToggle.astroは、クリック時に選択をlocalStorageへ保存し、ボタンのアイコン・説明・ツールチップを更新します。
手動設定がない場合は、OS側の配色変更にも追従します。
デプロイ
CloudflarePagesへの公開
ここからは、このリポジトリをCloudflarePagesへ公開する場合の設定です。
リポジトリ内にはCloudflareのプロジェクト設定やWebhookの登録情報がないため、以下は導入手順として説明します。
現在の構成は静的ページを生成するもので、Cloudflare用のサーバーアダプターは導入していません。
ビルド成果物のdistを公開する方針です。
CloudflarePagesでGitリポジトリを接続し、ビルドコマンドをnpmrunbuild、出力ディレクトリをdistに設定します。
Git連携では、対象ブランチへのpushによる再ビルドも利用できます。
CloudflareのAstro向けガイドに設定例があります。
このリポジトリでは、ビルド環境に次の準備も必要です。
package.jsonの要件を満たすNode.jsを使用する。- MICROCMS_SERVICE_DOMAINとMICROCMS_API_KEYをビルド用の環境変数として登録する。
astro.config.mjsのsite:'https://example.com'を、実際の公開URLに変更する。
siteはcanonicalURL、RSS、サイトマップなどに使われます。
公開前には、一覧・記事詳細・タグページに加え、RSSやサイトマップのURLも確認します。
Webhookで記事更新を反映する
静的ページに含まれる記事は、ビルド時点の内容です。
CloudflarePagesの対象プロジェクトで、Settings>Builds>Adddeployhookからフック名とビルド対象ブランチを設定し、URLを発行します。
このURLにHTTPPOSTを送るとデプロイが始まります。
CloudflareのDeployHooksドキュメントに手順が掲載されています。
次に、microCMSの各APIの「API設定→Webhook」から「CloudflarePages」を選び、発行したURLを登録します。
公開・更新だけでなく、公開終了や削除など、サイトに反映したい変更に合わせて通知タイミングを選びます。
設定できるイベントはmicroCMSのWebhookドキュメントで確認できます。
おわりに
無事AstroとmicroCMSを使用してブログを作成することができました。
さらに検索機能を追加したり、swupなどのインテグレーションを試すことを予定しています。