0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AstroをCMS化してブログ記事を効率よく管理する方法

Last updated at Posted at 2025-04-14

ブログ記事が増えてくると、管理が大変になりますよね。Astroの「Content Collections API」を使えば、記事を src/content ディレクトリにまとめて、安全かつ効率的に管理できます。ここではその基本的な手順を紹介します。

Content Collections を使うメリット

  • 型安全性: 記事のフロントマター(タイトル、日付など)の型を定義でき、間違いを防げます。
  • 管理のしやすさ: 記事データを src/pages から分離し、src/content に集約できます。
  • URLの柔軟性: ファイルパスに縛られず、記事ごとに任意のURLを設定できます。
  • 強力なAPI: 記事データの取得や処理が簡単になります。

基本的な手順

1.コレクションの設定 (src/content/config.ts)

まず、どのような記事データ(コレクション)を管理するか定義します。src/content/ ディレクトリ(なければ作成)に config.ts というファイルを作成し、以下のように記述します。

// src/content/config.ts
import { defineCollection, z } from 'astro:content';

// 'blog' という名前のコレクションを定義
const blogCollection = defineCollection({
  type: 'content', // Markdown や MDX ファイルの場合
  schema: z.object({ // フロントマターの型定義 (Zodを使用)
    title: z.string(),
    pubDate: z.coerce.date(), // 文字列を日付型に変換
    description: z.string().optional(), // オプショナルな項目
    tags: z.array(z.string()).optional(), // 文字列の配列 (オプショナル)
    url: z.string().optional(), // ← 追加: カスタムURL用のフィールド (オプショナル)
  }),
});

// 定義したコレクションをエクスポート
export const collections = {
  'blog': blogCollection,
};
  • defineCollectionblog という名前のコレクションを定義します。
  • schema で、各記事のMarkdownファイルに記述するフロントマターの項目(titlepubDate など)とその型を zod を使って指定します。これにより、型が間違っているとエラーで教えてくれます。
  • url: z.string().optional()はoptional() なので、フロントマターに url を書かなくてもエラーにはなりません。

2.記事ファイルの配置 (src/content/blog/)

次に、Markdown記事ファイルを配置する場所を作成します。src/content/ ディレクトリ内に、コレクション名と同じ名前のディレクトリ(この例では blog)を作成します。

your-project/
└── src/
    ├── content/
    │   ├── config.ts      <-- ステップ1で作成
    │   └── blog/          <-- コレクション名のディレクトリを作成
    │       └── my-first-post.md <-- ここに記事Markdownを置く
    │       └── another-post.md
    └── pages/
    └── layouts/

src/content/blog/ ディレクトリ内に、Markdownファイル(例: my-first-post.md)を作成または移動します。この際、ファイルのフロントマターは、ステップ1で定義した schema に従っている必要があります。

---
# src/content/blog/my-first-post.md
title: '私の最初の投稿 (Content Collections版)'
pubDate: 2025-04-14
description: 'Content Collections を使って記事を追加しました。'
tags: ["Astro", "Blog"]
url: my-first-post
---

Content Collections を使うと、記事管理が楽になりますね。

注意: src/content 内のMarkdownファイルでは、以前の方法で使った layout: フロントマターは 使用できません。レイアウトは後述するページコンポーネント側で適用します。

3.記事一覧ページの作成 (src/pages/blog/index.astro)

blog コレクションの記事一覧を表示するページを作成します。src/pages/blog/ ディレクトリ(なければ作成)に index.astro ファイルを作成します。

---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro'; // 例: 基本レイアウト

const posts = await getCollection('blog');
posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());

// URLを決定するヘルパー関数 (例)
function getPostUrl(post) {
  if (post.data.url) {
    // フロントマターにurlがあれば、ルートからのパスを返す
    return `/${post.data.url}/`;
  }
  // なければファイルパスに基づくデフォルトのパスを返す
  // post.slug は 'html/html-custom' のようになる
  return `/blog/${post.slug}/`;
}
---
<BaseLayout title="ブログ一覧">
  <h1>ブログ記事一覧</h1>
  <ul>
    {posts.map((post) => (
      <li>
        {/* getPostUrlヘルパーを使ってリンク先を決定 */}
        <a href={getPostUrl(post)}>{post.data.title}</a>
        <p>公開日: {post.data.pubDate.toLocaleDateString()}</p>
        {post.data.description && <p>{post.data.description}</p>}
      </li>
    ))}
  </ul>
</BaseLayout>
  • getCollection('blog')blog コレクションの記事データをすべて取得します。
  • 取得した posts は配列なので、map で各記事の情報を取り出し、リスト表示します。
  • フロントマターのデータには post.data.title のようにアクセスします。
  • 各記事へのリンクURLは post.slug (ファイル名から自動生成されるURLフレンドリーなID) を使って /blog/${post.slug}/ のように生成します。
  • レイアウトは通常のAstroコンポーネントとして <BaseLayout> のように適用します。

4.個別記事ページの作成 (src/pages/[...slug].astro)

個別記事を表示するページを src/pages/ ディレクトリの 直下 に作成し、ファイル名を[...slug].astroとします。Rest parameters(...slug)を使うことで、/my-first-post のようなシンプルなURLと/blog/html/html-customのような階層のあるURLの両方をこのファイルで処理できます。

---
// src/pages/[...slug].astro (新規作成、src/pages/blog/[id].astro は削除)
import { getCollection, getEntry } from 'astro:content';
import BaseLayout from '../layouts/BaseLayout.astro'; // レイアウトへのパスが変わることに注意

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map((post) => {
    // params.slug に設定する値を決定
    let slugPath;
    if (post.data.url) {
      // フロントマターのurlがあれば、それをパスとする
      slugPath = post.data.url;
    } else {
      // なければファイルパスに基づくパス (例: 'blog/html/html-custom')
      // post.id は 'blog/html/html-custom.md' のようなので、
      // ディレクトリ部分 + ファイル名(拡張子なし) を使う
      slugPath = post.id.replace(/\.md$/, ''); // .mdを除去
    }

    return {
      // params.slug は Astro.params.slug でアクセスでき、
      // URL の '/.../' の部分の文字列になる
      params: { slug: slugPath },
      props: { post }, // 記事データを渡す
    };
  });
}

const { post } = Astro.props;
const { Content } = await post.render();
---
<BaseLayout title={post.data.title}>
  <article>
    <h1>{post.data.title}</h1>
    <p>公開日: {post.data.pubDate.toLocaleDateString()}</p>
    {post.data.tags && (
      <p>タグ: {post.data.tags.join(', ')}</p>
    )}
    <hr />
    <Content />
  </article>
</BaseLayout>
  • getStaticPaths 関数は必須です。ここで getCollection('blog') を使って全記事を取得し、それぞれの記事に対して params (URLの一部になる) と props (ページコンポーネントに渡すデータ) を含むオブジェクトの配列を返します。
  • ページコンポーネントのスクリプト部分 (--- の中) で Astro.props から post データを受け取ります。
  • post.render() を実行すると、Markdown本文をレンダリングするための { Content } コンポーネントが得られます。
  • HTMLテンプレート部分で <Content /> を使うと、Markdownで書かれた本文が表示されます。
    • ここでもレイアウトは <BaseLayout> のように適用します。

5.開発サーバーで確認

ターミナルで開発サーバーを起動します。

npm run dev
# または pnpm dev, yarn dev

ブラウザで以下のURLにアクセスして確認してみましょう。

  • 記事一覧ページ: http://localhost:4321/blog/
  • 個別記事ページ: http://localhost:4321/my-first-post/ (記事のファイル名が my-first-post.md の場合)

一覧ページが表示され、各記事へのリンクが正しく機能し、個別記事ページでMarkdownの内容とフロントマターの情報が表示されていれば成功です!

まとめ

Content Collections APIを使うと、最初は設定が必要ですが、記事が増えても構造的に管理でき、型の恩恵も受けられるため、本格的なブログ運営には非常に便利です。ぜひ活用してみてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?