今回は私がブログを構築・運用するにあたって行き着いた構成、「Astro × Strapi」について、そしてそれをどうやって構築し、最後に自動デプロイ環境まで持っていくのかを詳しくお話ししたいと思います。
前提条件 本記事では Strapi v5 と Astro v5 を使用している環境を前提としています。Strapi v4系とはAPIのレスポンス形式(データ内の
documentIdの有無など)が異なるため、過去のバージョンをお使いの方は適宜読み替えてください。
なぜ Astro × Strapi なの?
まず最初に、「なぜ数ある技術の中から Astro と Strapi を選んだの?」というところから。
Astro の圧倒的なパフォーマンス Astro は、コンテンツ駆動のウェブサイトに特化したフレームワークです。「アイランドアーキテクチャ」という仕組みのおかげで、必要な場所にだけJavaScriptを読み込むため、ページの表示速度がものすごく速いんです。サクサク動くブログは、読者にとってストレスフリーですよね。 加えて、SEO対策的にも表示速度が速いほど評価が良くなると一般的に言われています。
Strapi の柔軟なコンテンツ管理(Headless CMS) 記事を書くたびにコードを触るのは面倒……。そこで活躍するのが Strapi です。管理画面が直感的で美しく、API をサクッと自動生成してくれます。WordPressのような複雑さはなく、フロントエンド(Astro)とバックエンド(Strapi)を完全に分離できるのが魅力ですね。
ローカルMarkdownとCMSのシームレスな統合 今回紹介する構成のミソは、「Astro の Collection 機能で管理するローカルのMarkdown記事」と「Strapi から API で取得する記事」を統合して、ひとつのブログとして表示できる点です。
バックエンド:Strapi のセットアップ
まずは、記事のデータ元となる Strapi を立ち上げましょう。今回は Docker などを活用して構築する前提で進めますが、ローカルでの基本的な立ち上げ方は以下の通りです。
1. プロジェクトの作成
ターミナルを開いて、以下のコマンドを実行します。
npx create-strapi-app@latest my-project --quickstart
これだけでCMSのベースが出来上がります。
2. コレクション型の作成
Strapi の管理画面(http://localhost:1337)にアクセスして、「Article」というコレクション型を作成します。
必要なフィールドは以下を参考にしてみてください:
Title(Text - Short)Excerpt(Text - Long)Content(Rich text)CoverImage(Media)PublishedAt(Date)
このブログの構成も載せておきますね。

注意ポイント: APIからアクセスできるように、
Settings > Roles & Permissions > PublicからArticleのfindとfindOneの権限にチェックを入れるのを忘れないでくださいね。これを忘れるとAstro側からデータをまったく取得できません。

フロントエンド:Astro のセットアップ(SSRモードの活用)
次に本命の Astro 側の設定です。ここで Strapi からデータを引っ張ってきて、美しく表示させます。
重要:SSG(静的サイト生成)かSSR(サーバーサイドレンダリング)か AstroはデフォルトでSSGですが、本番環境で「 Strapi で記事を公開したら直ぐにブログに反映させたい!」という運用をするために、今回は Node.js アダプターを使った SSR(動的レンダリング)構成 を採用します。これで Strapi 側で記事を更新するたびに Astro を再ビルドし直す手間が省けるんです! ※もしビルド時のみ通信する完全なSSGで構築したい場合、後述のアダプターは不要です。
1. プロジェクトの初期化
npm create astro@latest
初期設定画面では、空のプロジェクト(Empty)か、ブログテンプレートを選ぶとスムーズです。
2. SSR用のパッケージのインストールと設定
SSRを有効にするため、Node.js用のアダプターをインストールします。実際の私の package.json の抜粋はこちらです。
{
"dependencies": {
"@astrojs/node": "^9.5.4",
"astro": "^5.17.1",
"marked": "^12.0.1",
"unstorage": "^1.17.4"
}
}
Astroの設定ファイル(astro.config.mjs)で output: 'server' を指定し、アダプターを登録します。
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server', // ← ここでSSRモードを宣言!
adapter: node({
mode: 'standalone'
})
});
3. 動的ルーティング(詳細ページ)の実装
一番見せたいのがこの部分。読者のみなさんが一番気になるであろう「クリックしたあとの詳細ページはどうやって作ってるの?」という疑問にお答えします。
Astroの動的ルーティングを利用して src/pages/blog/[slug].astro を作成します。今回は SSR利用のため getStaticPaths は使用しません。 アクセスが来るたびに slug を元にデータを取得しにいくロジックになります。
---
// src/pages/blog/[slug].astro
import Layout from '../../layouts/Layout.astro';
import { getCollection } from 'astro:content';
import { marked } from 'marked';
const STRAPI_URL = import.meta.env.STRAPI_URL || "http://localhost:1337";
const { slug } = Astro.params;
let post = null;
let htmlContent = '';
let ContentComponent = null;
// ============================================================
// SSRモード: ページリクエスト時に都度データを取得します
// ============================================================
// 1. まずローカルのMarkdown記事(Content Collections)を探す
const mdPosts = await getCollection('blog');
const mdPost = mdPosts.find(p => p.slug === slug);
if (mdPost) {
const { Content } = await mdPost.render();
ContentComponent = Content;
post = { ...mdPost.data, title: mdPost.data.title, source: 'markdown' };
}
// 2. もしMarkdownになければ、Strapi API から記事を探す
if (!post) {
try {
const res = await fetch(`${STRAPI_URL}/api/articles/${slug}?populate=*`);
if (res.ok) {
const json = await res.json();
const article = json.data;
if (article) {
post = {
title: article.Title || article.title,
// ローカルストレージ運用の場合はURLの結合が必要ですが、外部ストレージを使っている場合はそのまま使えます
heroImage: article.CoverImage?.url ? `${STRAPI_URL}${article.CoverImage.url}` : null,
pubDate: new Date(article.PublishedAt || article.publishedAt),
source: 'strapi'
};
// MarkdownやリッチテキストをHTMLへ変換
const rawContent = article.Content || article.content || '';
if (typeof rawContent === 'string') {
htmlContent = marked.parse(rawContent);
}
}
}
} catch (e) {
console.error("Strapi fetch error:", e);
}
}
// 3. どちらにも無ければ404ページへ
if (!post) {
return Astro.redirect('/404');
}
---
<Layout title={post.title}>
<main class="blog-detail-container">
<article class="blog-post">
{post.heroImage && (
<img src={post.heroImage} alt={post.title} class="hero-image" />
)}
<h1 class="post-title">{post.title}</h1>
<div class="meta-info">
<time>{post.pubDate.toLocaleDateString()}</time>
</div>
<div class="post-content">
<!-- Markdownの場合はコンポーネントとして、Strapiの場合はHTMLとして流し込む -->
{ContentComponent ? <ContentComponent /> : <Fragment set:html={htmlContent} />}
</div>
</article>
</main>
</Layout>
これで、ローカルのMarkdownファイルもStrapiのAPIデータも、同じ詳細ページレイアウトで動的(SSR)にレンダリングされる仕組みができました!
仕上げ:GitHub Webhookを使った自動デプロイ(CI/CD)
ここまででブログのSSR表示は完璧です!記事データはアクセス毎にStrapiからひっぱってくるので、Strapi上で記事を書けば即座に本番のブログに反映されます。(静的生成のようにStrapi側の記事更新に合わせてAstroを再ビルドさせるWebhookは不要です)
では、どういう時に「自動デプロイ」が必要か?それは、**「ローカルのMarkdown記事を追加した時」や「デザインのコード(CSSやAstroファイル)を修正した時」**です。
今回は、Ubuntuなどの自前サーバーで、Dockerを利用している実践的なケースでの自動デプロイフック(GitHub連携)を紹介します。
1. サーバー側にデプロイ用のWebhook受け口を作る
サーバー上で、GitHubからのPush通知を受けとってDockerコンテナを再構築(再ビルド)する簡単なNode.jsスクリプト(deploy-server.jsなど)を立てておきます。
// deploy-server.js
const express = require('express');
const { exec } = require('child_process');
const app = express();
app.use(express.json());
// GitHubからのWebhookリクエストを受け取るエンドポイント
app.post('/webhook/github', (req, res) => {
// mainブランチへのPushイベントか確認
if (req.body.ref === 'refs/heads/main') {
console.log("GitHubのmainブランチへのPushを検知しました。コードをPullして再ビルドします...");
// docker-compose を使って立ち上げ直すコマンドの例
// ※プロジェクトの構成によって適宜変更してください
const cmd = 'cd /path/to/project && git pull origin main && docker compose build frontend && docker compose up -d frontend';
exec(cmd, (err, stdout, stderr) => {
if (err) {
console.error("デプロイエラー:", err);
return;
}
console.log("デプロイ完了!:", stdout);
});
}
// タイムアウトを防ぐためすぐに 200 OK を返す
res.status(200).send("Webhook Received");
});
app.listen(9000, () => console.log('Deploy server running on port 9000'));
2. GitHubのRepository設定からWebhookを追加する
GitHubのあなたのリポジトリを開き、Settings > Webhooks から「Add webhook」をクリックします。
- Payload URL: サーバーのエンドポイントURL(例:
http://サーバーIP:9000/webhook/github) - Content type:
application/jsonを選択 - Which events would you like to trigger this webhook?:
Just the push event.のままでOK。
これで、ローカルでMarkdown記事を書いたりデザインを直して git push をするだけで、サーバーが勝手に最新のコードを引っ張ってきてAstro入りのコンテナを再デプロイしてくれます!
ワンポイント: GitHub Webhookには「Secret」を設定する機能があります。 リクエストヘッダーの x-hub-signature-256 を検証するコードを追加するのもありですね。
最後に
Astro の軽快さと Strapi の便利なコンテンツ管理、そしてGitHub Webhookを使った自動デプロイの組み合わせ。SSR構成にすることで「記事の更新はStrapiからリアルタイムで」「コードやMarkdownの更新はPushするだけ」という最高にスマートなブログサイトが運用できちゃいます。
あとは出先でも記事を書いて公開(Push)ボタンを押すだけ。裏側ではサーバーがビルドを走らせてくれます。
それでは、また次の記事でお会いしましょう♡