Astro + Markdownブログで最初に設定しておくべきこと

AstroでMarkdownブログを作るなら、記事を書き始める前に最低限の設定だけ済ませておくと後が楽です。

調べた感じ、以下のような構成にすると扱いやすいらしい。

  • Markdown記事の置き場所
  • frontmatterの型
  • 記事一覧と詳細ページのルート
  • draft記事を公開しない仕組み
  • canonical URLとsitemap

デザインや細かいコンポーネントは後でも直せます。逆に、URLやfrontmatterの持ち方は後から変えると地味に面倒です。

Markdown記事はcontent collectionで管理する

AstroでMarkdown記事を扱う場合、公式ドキュメントでは Content Collections を使う方法が案内されています。

https://docs.astro.build/en/guides/content-collections/

このブログでは、まず src/content/posts に記事を置く形にしています。

src/
├── content/
│   └── posts/
│       ├── first-post.md
│       └── draft-template.md
└── content.config.ts

記事ファイルは普通のMarkdownでよくて、先頭にfrontmatterを書きます。

---
title: "記事タイトル"
description: "検索結果やOGPで使う説明文"
pubDate: 2026-06-27
draft: false
tags: ["Astro", "Markdown"]
---

本文を書きます。

この時点で titledescription は毎回書く前提にします。検索結果、記事一覧、OGPあたりで使い回すことが多いので、後から足すより最初に決めた方が楽です。

frontmatterの型を決める

Content Collections では、src/content.config.ts でfrontmatterのスキーマを定義できます。

https://docs.astro.build/en/guides/content-collections/#defining-the-collection-schema

設定例はこんな感じです。

import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';

const posts = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/posts' }),
  schema: z.object({
    title: z.string(),
    description: z.string().optional(),
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    draft: z.boolean().default(false),
    tags: z.array(z.string()).default([]),
    canonicalUrl: z.string().url().optional(),
  }),
});

export const collections = { posts };

ここでやっているのは、記事ごとの情報を「なんとなく」ではなく、コード側で扱いやすい形に固定することです。

特に draft は最初から入れておくと便利です。下書き記事を作っても、一覧や詳細ページで除外できます。

記事一覧ではdraftを除外する

記事一覧では getCollection() を使ってMarkdown記事を取得します。AstroのContent Collectionsでは、このAPIでcollection内の記事を読めます。

https://docs.astro.build/en/reference/modules/astro-content/#getcollection

---
import { getCollection } from 'astro:content';
import { getPostUrl } from '../lib/posts';

const posts = (await getCollection('posts', ({ data }) => !data.draft))
  .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
---

<ul>
  {posts.map((post) => (
    <li>
      <a href={getPostUrl(post)}>{post.data.title}</a>
    </li>
  ))}
</ul>

ポイントは !data.draft で公開対象だけに絞ることです。

Markdownファイル自体はリポジトリに置いておきたいけれど、まだ公開はしたくない。そういう状態は普通にあるので、ここは最初から入れておきます。

Markdownファイル名からURLを作る

URLの作り方も、早めに決めておきたいところです。Astroは src/pages のファイル構成をルーティングに使うので、このあたりは公式のRoutingも合わせて見ておきます。

https://docs.astro.build/en/guides/routing/

たとえば src/content/posts/astro-markdown-blog-settings.md を、次のURLで公開する形にします。

/posts/astro-markdown-blog-settings/

ヘルパーを1つ置いておくと、一覧ページでも詳細ページでも同じルールを使えます。URL生成をあちこちに直書きしない、くらいの狙いです。

import type { CollectionEntry } from 'astro:content';

export function getPostSlug(post: CollectionEntry<'posts'>) {
  return post.id.replace(/\.mdx?$/, '');
}

export function getPostUrl(post: CollectionEntry<'posts'>) {
  return `/posts/${getPostSlug(post)}/`;
}

URLは後から変えるとリダイレクトや内部リンクの修正が必要になります。最初はシンプルに、ファイル名をslugとして使うくらいで十分です。

記事詳細ページを作る

記事詳細ページは src/pages/posts/[...slug].astro に置きます。Astroの動的ルーティングでは、getStaticPaths() で生成するパスを返す形になります。

https://docs.astro.build/en/guides/routing/#dynamic-routes

---
import { getCollection, render } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getPostSlug } from '../../lib/posts';

export async function getStaticPaths() {
  const posts = await getCollection('posts', ({ data }) => !data.draft);

  return posts.map((post) => ({
    params: { slug: getPostSlug(post) },
    props: { post },
  }));
}

const { post } = Astro.props;
const { Content } = await render(post);
---

<BaseLayout title={`${post.data.title} | Makonote`} description={post.data.description} ogType="article">
  <article class="prose">
    <h1>{post.data.title}</h1>
    <Content />
  </article>
</BaseLayout>

getStaticPaths() で公開する記事のURLを作り、render(post) でMarkdown本文をコンポーネントとして描画します。

ここでも draft は除外しておきます。記事一覧だけ除外して詳細ページが生成される、という状態は避けたいです。

canonicalとsitemapを入れる

検索流入を考えるなら、sitesitemap は早めに設定しておきます。Astro公式のSitemap integrationでも、サイトURLを設定してsitemapを生成する流れになっています。

https://docs.astro.build/en/guides/integrations-guide/sitemap/

astro.config.mjs:

import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';

export default defineConfig({
  site: 'https://example.com',
  output: 'static',
  integrations: [sitemap()],
});

site はcanonical URLやsitemap生成で使います。Cloudflare Pagesに載せる場合でも、最終的に使う独自ドメインを入れておく方が自然です。

レイアウト側では、各ページのcanonicalを作っておきます。

---
const { title, description, ogType = 'website' } = Astro.props;
const canonicalUrl = new URL(Astro.url.pathname, Astro.site).href;
---

<head>
  <title>{title}</title>
  <meta name="description" content={description} />
  <link rel="canonical" href={canonicalUrl} />
  <meta property="og:title" content={title} />
  <meta property="og:description" content={description} />
  <meta property="og:url" content={canonicalUrl} />
  <meta property="og:type" content={ogType} />
</head>

このへんは後回しにしがちですが、記事数が増えてから直すより最初に入れておく方が楽です。

Cloudflare Pagesならstatic出力でよい

Markdown中心のブログなら、Astroの output: 'static' で十分です。Astroの設定項目はConfiguration Referenceにまとまっています。

https://docs.astro.build/en/reference/configuration-reference/

export default defineConfig({
  site: 'https://example.com',
  output: 'static',
  integrations: [sitemap()],
});

Cloudflare Pages側は、だいたい次の設定になります。

Build command: npm run build
Build output directory: dist
Root directory: 空欄

環境変数は、外部APIや秘密情報を使わない静的ブログなら必須ではありません。

最初に決めておくと楽なこと

Astro + Markdownのブログで、最初に決めておく項目はこのあたり。

  • 記事URLは /posts/{slug}/ にする
  • Markdownファイル名をslugにする
  • frontmatterに titledescriptionpubDatedrafttags を持たせる
  • draft: true は一覧にも詳細にも出さない
  • site、canonical、sitemapを最初から入れる

逆に、デザインや記事カードの見た目は後からでもなんとかなります。

URL、frontmatter、公開判定。この3つだけは記事が増えるほど変更コストが上がるので、先に決めておきます。

参考