コンテンツにスキップ

任意: コンテンツコレクションを作る

以上でAstroの組み込みのファイルベースルーティング (EN)を使ったブログができましたが、これをコンテンツコレクション (EN)を使うように更新していきましょう。ブログ記事のような、似たようなコンテンツのグループを管理するうえで、コンテンツコレクションは非常に強力です。

ここで学ぶことは...

  • ブログ記事のフォルダーをsrc/blog/に移動する
  • ブログ記事のフロントマターを定義するスキーマを作成する
  • getCollection()でブログ記事のコンテンツとメタデータを取得する

学ぶ: ページとコレクション

コンテンツコレクションを使う場合でも、src/pages/フォルダーは「About Me」ページのような、個別のページ用には引き続き使います。ただし、ブログ記事をこの特別なフォルダーの外に移すことで、ブログ一覧の生成や各記事の表示に、より強力でパフォーマンスの高いAPIが使えるようになります。

同時に、各記事に共通する構造を定義する スキーマ (EN) を用意でき、AstroがZod(TypeScript向けのスキーマ宣言・検証ライブラリ)を通じてその構造どおりかどうかを検証してくれることで、コードエディター上でもより的確なガイドや補完を受けられるようになります。スキーマでは、説明や著者などのフロントマターのプロパティを必須にするかどうか、文字列や配列などの各プロパティの型を何にするかを指定できます。結果として、多くの間違いを早い段階で発見でき、問題箇所をはっきり示すエラーメッセージが得られます。

詳しくはガイドのAstroのコンテンツコレクション (EN)を読むか、以下の手順に沿って、基本的なブログをsrc/pages/posts/からsrc/blog/へ移行してみてください。

確認テスト

  1. src/pages/に残すべきなのは、どの種類のページですか?

  2. ブログ記事をコンテンツコレクションに移す利点にならないものはどれですか?

  3. コンテンツコレクションでTypeScriptを使うと. . .

以下の手順では、「初めてのAstroブログ」チュートリアルの最終成果を、ブログ記事用のコンテンツコレクションを追加して拡張する方法を示します。

依存関係をアップグレードする

ターミナルで次のコマンドを実行し、Astroとすべてのインテグレーションをそれぞれ最新にアップグレードします。

   # Astroと公式インテグレーションをまとめてアップグレード
   npx @astrojs/upgrade

記事用のコレクションを作成する

  1. src/blog/という名前の新しいコレクション(フォルダー)を作成します。

  2. 既存のブログ記事(.mdファイル)をすべてsrc/pages/posts/から、この新しいコレクションへ移動します。

  3. postsCollection用のスキーマを定義する (EN)ためにsrc/content.config.tsファイルを作成します。既存のブログチュートリアルのコードに合わせ、記事のフロントマターで使っているプロパティをすべて定義するために、次の内容をファイルに追加します。

    // glob ローダーをインポートする
    import { glob } from "astro/loaders";
    // `astro:content` からユーティリティをインポートする
    import { defineCollection } from "astro:content";
    // Zod をインポートする
    import { z } from "astro/zod";
    // 各コレクションの loader と schema を定義する
    const blog = defineCollection({
        loader: glob({ pattern: '**/[^_]*.md', base: "./src/blog" }),
        schema: z.object({
          title: z.string(),
          pubDate: z.date(),
          description: z.string(),
          author: z.string(),
          image: z.object({
            url: z.string(),
            alt: z.string()
          }),
          tags: z.array(z.string())
        })
    });
    // コレクションを登録するため、collections オブジェクトをエクスポートする
    export const collections = { blog };
  4. Astroにスキーマを認識させるには、開発サーバを終了(Ctrl + C)して再起動し、チュートリアルを続けます。これでastro:contentモジュールが定義されます。

コレクションからページを生成する

  1. src/pages/posts/[...slug].astroという名前のページファイルを作成します。コレクション内に置いたMarkdownやMDXは、Astroのファイルベースルーティングでは自動的にはページになりません。そのため、各ブログ記事を生成するためのページを自分で用意する必要があります。

  2. 次のコードを追加してコレクションをクエリし (EN)、生成する各ページでスラッグと記事本文が使えるようにします。

    ---
    import { getCollection, render } from 'astro:content';
    
    export async function getStaticPaths() {
      const posts = await getCollection('blog');
      return posts.map(post => ({
        params: { slug: post.id }, props: { post },
      }));
    }
    
    const { post } = Astro.props;
    const { Content } = await render(post);
    ---
  3. Markdown用レイアウトのなかで記事の<Content />をレンダリングします。これですべての記事に共通のレイアウトを指定できます。

    ---
    import { getCollection, render } from 'astro:content';
    import MarkdownPostLayout from '../../layouts/MarkdownPostLayout.astro';
    
    export async function getStaticPaths() {
      const posts = await getCollection('blog');
      return posts.map(post => ({
        params: { slug: post.id }, props: { post },
      }));
    }
    
    const { post } = Astro.props;
    const { Content } = await render(post);
    ---
    <MarkdownPostLayout frontmatter={post.data}>
      <Content />
    </MarkdownPostLayout>
  4. 各記事のフロントマターからlayoutの指定を削除します。レンダリング時にレイアウトでラップされるようになったため、このプロパティはもう不要です。

    ---
    layout: ../../layouts/MarkdownPostLayout.astro
    title: '私の最初のブログ記事'
    pubDate: 2022-07-01
    ...
    ---

import.meta.glob()getCollection()に置き換える

  1. チュートリアルのブログページ(src/pages/blog.astro)のようにブログ記事の一覧がある箇所では、Markdownファイルからコンテンツとメタデータを取得する方法として、import.meta.glob()getCollection() (EN)に置き換える必要があります。

    ---
    import { getCollection } from "astro:content";
    import BaseLayout from "../layouts/BaseLayout.astro";
    import BlogPost from "../components/BlogPost.astro";
    
    const pageTitle = "私のAstro学習ブログ";
    const allPosts = Object.values(import.meta.glob("../pages/posts/*.md", { eager: true }));
    const allPosts = await getCollection("blog");
    ---
  2. postに対して返されるデータの参照も更新します。フロントマターの値は、各オブジェクトのdataプロパティにあります。また、コレクションを使う場合、各postオブジェクトが持つのは完全なURLではなく、ページのslugです。

    ---
    import { getCollection } from "astro:content";
    import BaseLayout from "../layouts/BaseLayout.astro";
    import BlogPost from "../components/BlogPost.astro";
    
    const pageTitle = "私のAstro学習ブログ";
    const allPosts = await getCollection("blog");
    ---
    <BaseLayout pageTitle={pageTitle}>
      <p>ここには、私がAstroを学んでいく旅の様子を投稿します。</p>
      <ul>
        {
          allPosts.map((post) => (
            <BlogPost url={post.url} title={post.frontmatter.title} />)}
            <BlogPost url={`/posts/${post.id}/`} title={post.data.title} />
          ))
        }
      </ul>
    </BaseLayout> 
  3. チュートリアルのブログプロジェクトでは、src/pages/tags/[tag].astroでタグごとのページを動的に生成し、src/pages/tags/index.astroでタグ一覧を表示しています。

    次の2つのファイルにも、上と同じ変更を適用します。

    • import.meta.glob()の代わりにgetCollection("blog")ですべてのブログ記事のデータを取得する
      • frontmatterの代わりにdataですべてのフロントマターの値にアクセスする
      • 記事のslug/posts/パスに足してページのURLを作る

    個別のタグページを生成するページは、次のようになります。

      ---
      import { getCollection } from "astro:content";
      import BaseLayout from "../../layouts/BaseLayout.astro";
      import BlogPost from "../../components/BlogPost.astro";
    
      export async function getStaticPaths() {
        const allPosts = await getCollection("blog");
        const uniqueTags = [...new Set(allPosts.map((post) => post.data.tags).flat())];
    
        return uniqueTags.map((tag) => {
          const filteredPosts = allPosts.filter((post) =>
            post.data.tags.includes(tag)
          );
          return {
            params: { tag },
            props: { posts: filteredPosts },
          };
        });
      }
      
      const { tag } = Astro.params;
      const { posts } = Astro.props;
      ---
    
      <BaseLayout pageTitle={tag}>
        <p>{tag}のタグが付いた記事</p>
        <ul>
          { posts.map((post) => <BlogPost url={`/posts/${post.id}/`} title={post.data.title} />) }
        </ul>
      </BaseLayout>

    やってみよう - タグインデックスページのクエリを更新する

    src/pages/tags/index.astroで、ブログ記事に使われているタグを取得するためにgetCollectionをインポートして使います。上記と同じ手順に従ってください。

    コードを表示

        ---
        import { getCollection } from "astro:content";
        import BaseLayout from "../../layouts/BaseLayout.astro";     
        const allPosts = await getCollection("blog");
        const tags = [...new Set(allPosts.map((post) => post.data.tags).flat())];
        const pageTitle = "タグインデックス";
        ---
        <!-- ... -->

スキーマに合わせてフロントマターを更新する

必要に応じて、レイアウトなどプロジェクト全体で使用しているフロントマターの値のうち、コレクションのスキーマと一致していないものを更新します。

ブログチュートリアルの例では、pubDateは文字列でした。一方、記事のフロントマターの型を定義するスキーマに従うと、pubDateDateオブジェクトになります。これを利用して、Dateオブジェクトで使えるメソッドで日付を整形できます。

ブログ記事のレイアウトで日付を表示するには、toLocaleDateString()メソッドで文字列に変換します。

<!-- ... -->
<BaseLayout pageTitle={frontmatter.title}>
    <p>{frontmatter.pubDate.toLocaleDateString()}</p>
    <p><em>{frontmatter.description}</em></p>
    <p>著者: {frontmatter.author}</p>
    <img src={frontmatter.image.url} width="300" alt={frontmatter.image.alt} />
<!-- ... -->

RSSの関数を更新する

チュートリアルのブログプロジェクトにはRSSフィードが含まれています。この関数でもgetCollection()を使ってブログ記事の情報を返す必要があります。返されたdataオブジェクトからRSSの各項目を生成します。

 import rss from '@astrojs/rss';
 import { pagesGlobToRssItems } from '@astrojs/rss';
 import { getCollection } from 'astro:content';

 export async function GET(context) {
   const posts = await getCollection("blog");
   return rss({
     title: 'Astro学習者 | ブログ',
     description: 'Astroを学ぶ旅',
     site: context.site,
     items: await pagesGlobToRssItems(import.meta.glob('./**/*.md')),
     items: posts.map((post) => ({
       title: post.data.title,
       pubDate: post.data.pubDate,
       description: post.data.description,
       link: `/posts/${post.id}/`,
     })),
     customData: `<language>ja-jp</language>`,
   })
 }

コンテンツコレクションを使ったブログチュートリアルの完全な例は、チュートリアルリポジトリのコンテンツコレクションブランチを参照してください。

チェックリスト

貢献する コミュニティ スポンサー