AstroでMarkdown記事サイトを作る最小構成|静的HTMLとContent Collections
AstroでMarkdown記事を管理し、Content Collectionsの型検査、動的ルートの静的生成、canonical、sitemap、生成HTMLの検証までを最小構成で解説します。
この記事では、AstroでMarkdownから静的HTML(あらかじめ作っておくページ)を生成し、frontmatter(記事の先頭に書く設定欄)の不足を公開前に見つける最小構成を作ります。手を動かすのが自分でも、AIに任せるのでも、進め方は同じです。1段ずつ作り、そのたびに生成物で合否を確かめます。なお、この記事のコードはAstro 5系のプロジェクトで確認しています。公式ドキュメントは最新版に合わせて更新され続けるため、手元に入っている版でビルドが通るかを最後の物差しにしてください。
完成形と、静的構成が向く条件
先に完成形を決めます。目標は「Markdownファイルを1つ足すと、ビルド(=サイト全体をまとめて生成する作業)で記事のHTMLとsitemapが自動で作られるサイト」です。
Astroのoutput設定はstaticかserverの2択で、既定値はstaticです(公式のConfiguration Referenceに記載)。staticでは各ページを公開前にまとめて生成します。全員へ同じ本文を配る記事サイトには、この既定がそのまま合います。会員ごとに本文を変えるなど、アクセスのたびに中身が変わるサイトなら、必要な部分だけ別の構成を検討します。
最小構成の判断表
| 判断項目 | 静的構成で進めやすい条件 | 別の構成も検討する条件 |
|---|---|---|
| 本文の更新 | 更新時に再ビルドできる | 秒単位・分単位の更新が必要 |
| 読者ごとの差 | 全員へ同じ本文を配信する | ログイン状態や契約ごとに本文が変わる |
| 記事の入力 | Markdownをファイルで管理する | CMSの下書きを即時プレビューしたい |
| URL生成 | ビルド時に公開URLを列挙できる | リクエスト内容でURLや本文が決まる |
| 最初の検証 | schema、生成HTML、sitemapを確認する | 認証、セッション、外部APIも同時に検証する |
この表の左側に収まるなら、まず静的構成で公開までの道を1本完成させます。記事作りを半自動化する流れを先に決めておくと、原稿の生成と公開の判定を分けられます。
最小構成のファイルと作る順番
空の状態から始めるなら、公式が案内するコマンドnpm create astro@latestでプロジェクトを作ります(公式のInstallガイド)。ウィザードで依存関係の導入を飛ばした場合は、あとからnpm installを実行します。
最小構成で触るファイルは5つだけです。
| 作る順 | ファイル | 役割 |
|---|---|---|
| 1 | astro.config.mjs | 公開URL(site)と出力方式、sitemapの設定 |
| 2 | src/content.config.ts | 記事コレクションの定義とfrontmatterの型検査 |
| 3 | src/content/articles/記事名.md | 記事本体(frontmatter+本文) |
| 4 | src/pages/kiji/[...id].astro | 記事IDからHTMLページを生成する動的ルート |
| 5 | 共通レイアウト | title、description、canonicalタグの出力 |
AIに作らせるときは、一度に全部を頼まないでください。「このファイルだけを変更して、npm run buildが通ることを確認して」と、対象ファイルと合格条件を1段ずつ渡します。1段ずつなら、失敗したときにどこが原因かすぐ分かります。
Content Collectionsで記事の形を固定する
Content Collectionsは、同じ構造の記事群をまとめて扱う仕組みです。ビルド時コレクションはsrc/content.config.tsで定義し、ローカルのMarkdownをglob()で読み込みます(公式のContent collectionsガイド)。
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
const articles = defineCollection({
loader: glob({
base: './src/content/articles',
pattern: '**/*.md',
}),
schema: z.object({
title: z.string(),
description: z.string(),
category: z.string(),
pubDate: z.coerce.date(),
draft: z.boolean().default(false),
}),
});
export const collections = { articles };
baseは基準フォルダ、patternは対象ファイルの指定です。glob()では、ファイル名から記事のid(=URLのもとになる名前)が自動で作られます。だからファイル名は、最初からURLに使える形(半角英数字とハイフン)にそろえます。
記事側のMarkdownは、schemaと同じ名前の項目をfrontmatterに書きます。最小の1記事はこれだけです。
---
title: '記事の題名'
description: '検索結果に出る説明文'
category: 'jidoka'
pubDate: 2026-07-23
draft: true
---
## 最初の見出し
本文をここに書きます。
schemaは、titleの欠落や日付として読めないpubDateを、公開前のビルドで検出します。カテゴリが固定ならz.enum(['guide', 'news'])のように候補を絞れます。ここで1つ注意があります。draftはこの例で設けた独自項目で、Astroが自動で下書きを隠してくれるわけではありません。公開記事を取得する処理で、自分で除外します(次の節で書きます)。
エラーが出たときは、Content Collectionsのエラーを切り分ける手順とfrontmatterエラーの直し方が続きになります。
記事IDから静的HTMLのURLを生成する
コレクションへ記事を置くだけでは、記事ページは生成されません。静的モードでは、すべてのURLをビルド時に決める必要があり、動的ルート(=記事ごとに変わるURL)にはgetStaticPaths()という関数が必須です(公式のエラーリファレンス)。
src/pages/kiji/[...id].astroを作り、生成する記事の一覧を返します。
---
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const articles = await getCollection('articles', ({ data }) => !data.draft);
return articles.map((article) => ({
params: { id: article.id },
props: { article },
}));
}
const { article } = Astro.props;
const { Content } = await render(article);
---
<h1>{article.data.title}</h1>
<Content />
流れは1本道です。getCollection()の2つ目の引数でdraftの記事を除外し、残った記事のidをparamsとして返し、render()で得た<Content />が本文になります。frontmatterの値はarticle.dataから読めます(公式のMarkdownガイド)。
ファイル名の[...id]は、任意の深さのパスに対応するrest parameter(残りのパスをまとめて受け取る指定)です。記事URLが常に1階層なら[id].astroで足ります。「将来使うかもしれない」という理由だけで複数階層を許す設計に広げないでください。ページが404になったら、コレクション名、下書きの除外条件、paramsのキー名とファイル名の一致、出力先の順に確認します。Claude Codeで最初に見るフォルダも、配置の切り分けに役立ちます。
canonicalとsitemapを設定し、生成物で検品する
検索エンジンへURLの一覧を伝えるsitemapと、正規URL(=このページの正式な住所)を示すcanonicalを用意します。astro.config.mjsのsiteには最終的な公開URLを設定します。Astroはこの値を、sitemapとcanonical URLの生成に使います(公式のConfiguration Reference)。
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://example.com',
output: 'static',
integrations: [sitemap()],
});
output: 'static'は既定値なので書かなくても静的になりますが、意図を残すために書いておくと読み手(人もAIも)が迷いません。
公式の@astrojs/sitemapは、静的生成されたルートをたどってsitemapを作ります。getStaticPaths()で作った動的ルートも含まれますが、SSR(アクセス時にサーバーで作る方式)のルートは対象外です。設定後のビルドではsitemap-index.xmlとsitemap-0.xmlが出力されます。ただし出力ファイル名は版によって変わる可能性があるため、実際の生成物を見て確かめてください。
canonicalタグは、sitemapとは別に自分で出します。HTML標準では、<link rel="canonical">のhrefが「この文書の優先URL」を示すと定められています(HTML Standard)。共通レイアウトでnew URL(Astro.url.pathname, Astro.site)を作り、hrefへ渡します。注意点が1つあります。Googleの公式ドキュメントは、canonical指定を「規則ではなくヒント」として扱うと明言しています。付ければ必ずそのURLが採用される、とは考えないでください。
sitemapに載せるURLは完全な絶対URL(https://から始まる形)にします。Googleのsitemap仕様では、2026-07-26時点で1ファイルの上限が非圧縮50MBまたは50,000 URLと案内されています。数値は変わることがあるので、大規模になったら最新の公式案内で確認してください。今回の最小構成なら気にする規模ではありません。
設定を終えたらnpm run buildを実行します。成果物は既定でdist/フォルダに入り、npm run previewでビルド時点のサイトをローカル確認できます(公式のDevelop and build)。合否は次の表で決めます。
ビルド後の検品チェック表
| 確認する場所 | 合格条件 |
|---|---|
dist/kiji/記事ID/index.html | ファイルが存在し、title・description・本文がHTML内にある |
同じHTML内の<link rel="canonical"> | hrefが公開URLと一致している |
dist/のsitemapファイル | 生成されていて、記事の絶対URLが載っている |
| sitemap全体 | draft: trueの記事のURLが1つも無い |
npm run previewの表示 | 記事が開けて、内部リンクが踏める |
ブラウザで見るだけでなく、HTMLファイルの中身を文字列検索してください。本文が最初からHTMLに含まれているかを機械的に判定できます。リンク切れの検査は別の仕組みに分けると管理しやすいので、Astroの記事リンクを自動検査する方法を組み合わせます。URL方針の詳細はcanonical URLの書き方、詰まったときはrobots.txtとsitemapのトラブル確認が続きになります。
よくある質問
AstroではMarkdownファイルを置くだけで記事URLができますか
できません。Content Collectionsへ読み込んだだけでは、ページは生成されません。静的構成では動的ルートのファイルを用意し、getStaticPaths()から各記事のparamsとpropsを返して、ビルド対象を列挙する必要があります。
draft: trueなら自動的に公開対象から外れますか
外れません。この記事のdraftは自分で決めた独自項目です。getCollection()の絞り込みで明示的に除外します。さらに、生成後のsitemapとHTMLにも下書きが混ざっていないか確認します。二重に見るのは、除外の書き忘れが公開事故に直結するからです。
[id].astroと[...id].astroはどちらを選びますか
記事URLが常に1階層なら[id].astroで足ります。カテゴリ名などを含む複数階層のIDも扱うときだけ、任意の深さに一致する[...id].astroを使います。迷ったら狭い方([id])から始めてください。
Content Collectionsのschemaで本文の品質も検査できますか
できません。schemaが検査するのは、frontmatterの項目名、型、必須・任意の条件です。本文の誤字、事実関係、リンク切れ、読みやすさは別の検査が必要です。Codexへの依頼テンプレートのように、変更対象と合格条件を固定して確認漏れを減らします。
sitemapを入れればcanonicalタグも自動でHTMLに入りますか
入りません。別々の仕組みです。siteの設定は両方のURL生成の基準になりますが、sitemapインテグレーションの仕事はsitemapファイルの生成だけです。各ページの<link rel="canonical">は共通レイアウトなどで自分で出力し、生成後のHTMLで確認します。
まとめ
最小構成の価値は、運用を「記事の形をschemaで固定する、記事IDから静的ページを列挙する、生成HTMLとsitemapを検査する」の3段階へいつでも戻せることです。読み終えたら、次の3つを順に進めてください。
npm create astro@latestで空のプロジェクトを作り、この記事の表の順にファイルを1つずつ足す- 記事を1本だけ入れて
npm run buildを実行し、検品チェック表の全項目を生成物で確認する - AIへ頼むときは「対象ファイル・期待する生成物・ビルドの合格条件」を1セットにして、1段ずつ渡す
公式一次情報
確認日: 2026-07-26
- Install Astro | Docs: 新規プロジェクトを作る公式コマンド
npm create astro@latestと、依存関係を後から入れる場合の手順。 - Content collections | Docs:
src/content.config.tsでの定義、glob()のbaseとpattern、ファイル名からのid生成、schema検証、filterによる下書き除外。 - Markdown in Astro | Docs: 取得した記事のfrontmatterは
data、本文はbodyから読め、render()が描画用の本文を返すこと。 - Routing | Docs: 静的な動的ルートと
getStaticPaths()、任意の深さに一致するrest parameter[...path]の仕様。 - getStaticPaths() function required for dynamic routes | Docs: 静的モードでは全ルートをビルド時に決める必要があり、動的ルートに
getStaticPaths()が必須であること。 - Configuration Reference | Docs:
outputの既定値がstaticであることと、siteがsitemapとcanonical URLの生成に使われること。 - @astrojs/sitemap | Docs: 公式sitemapインテグレーションが静的ルートから生成し、SSRの動的ルートは対象外であること。出力例は
sitemap-index.xmlとsitemap-0.xml。 - Develop and build | Docs:
npm run buildが成果物を既定のdist/へ生成し、npm run previewでビルド時点のサイトを確認できること。 - HTML Standard:
canonicalリンク型のhrefが現在の文書の優先URLを示すという定義。 - What is URL Canonicalization | Google Search Central:
rel="canonical"などの指定は規則ではなくヒントとして扱われること。 - Build and Submit a Sitemap | Google Search Central: sitemapには絶対URLを使うことと、1ファイルの上限(非圧縮50MBまたは50,000 URL)。
続き(結論と実データ)はnoteに置いています
ここでは手順のところまで書きました。実際に出た数字、うまくいかなかった条件、そのまま使える設定ファイルは、 note の記事にまとめてあります。