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 Content Loader APIを使って外部データをコンテンツコレクションとして扱えるように

0
Posted at

Astro 5.0以降でアップデートされたContent collectionsを使用して、ローカルのmdファイルに加え、microCMSのデータも同じようにコンテンツとして扱えるようにしました

定義ファイルの作成

  • コンテンツコレクションを定義するためには、src/content.config.tsファイルに設定を記述していく必要があります
  • 以下の例では、glob()ローダーを使用して、ローカルに配置されているmdファイルをコンテンツコレクションとして読み込んでいます
src/content.config.ts
import { defineCollection } from "astro:content";
import { glob } from "astro/loaders";
import { z } from "astro/zod";

// スキーマを定義
const blogSchema = z.object({
  title: z.string(),
  description: z.string().optional(),
  date: z.coerce.date(),
  updatedDate: z.coerce.date().optional(),
  tags: z.array(z.string()).default([]),
  draft: z.boolean().default(false),
});

// ローカルのmdファイルを読み込む
const blog = defineCollection({
  loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
  schema: blogSchema,
});

microCMS用のローダーを作成する

  • 外部のデータを読み込む場合は、独自のカスタムローダーを作成する必要があります。
  • Astroでは、独自ローダー用のローダーオブジェクトが用意されているので、そちらを使ってローダーを作成していきます。
  • 基本的にローダーは、エンドポイントなどの設定情報を受け取り、ローダーオブジェクトを返す関数をエクスポートします。
  • ローダーオブジェクトは、nameプロパティとload()メソッドを含んでいる必要があります。
microcms-loader.ts
import type { Loader } from "astro/loaders";
import { createClient } from "microcms-js-sdk";
import type { MicroCMSBlog } from './type'

export function microcmsLoader(options: {
  endpoint: string;
}): Loader {
  return {
    name: "microcms-blog-loader",
    async load({ store, logger }) {
      const serviceDomain = import.meta.env.MICROCMS_SERVICE_DOMAIN;
      const apiKey = import.meta.env.MICROCMS_API_KEY;

      if (!serviceDomain || !apiKey) {
        logger.warn(
          "MICROCMS_SERVICE_DOMAIN or MICROCMS_API_KEY is not set. Skipping microCMS blog loading.",
        );
        return;
      }

      const client = createClient({ serviceDomain, apiKey });

      let response: MicroCMSBlog[];
      try {
        response = await client.getAllContents<MicroCMSBlog>({
          endpoint: options.endpoint,
        });
      } catch (e) {
        logger.warn(
          `Failed to fetch from microCMS endpoint "${options.endpoint}". Skipping. (${e instanceof Error ? e.message : e})`,
        );
        return;
      }

      store.clear();

      for (const item of response) {
        store.set({
          id: item.id,
          data: {
            title: item.title,
            body: item.body,
            date: new Date(item.publishedAt || item.createdAt),
            updatedDate: item.updatedAt
              ? new Date(item.updatedAt)
              : undefined,
            tags: item.tags?.map((t) => t.name) ?? [],
            eyecatch: item.eyecatch,
          },
        });
      }

      logger.info(
        `Loaded ${response.length} blog posts from microCMS`,
      );
    },
  };
}
  • load()関数では、コンテンツがどのように取得、解析、検証、更新されるかを定義していきます。
  • この関数は、ビルド時に実行され、さまざまな方法でデータ処理をカスタマイズし、データストアとやり取りできるコンテキストオブジェクトを受け取ります。
  • 今回の例で使用しているコンテキストオブジェクトに含まれるプロパティは以下になります
    • store:実際のデータを保存するデータベース。これを使用して、microCMSのデータをエントリとして保存する
    • logger:コンソールにメッセージをログ出力するために使用できるロガー

定義ファイルでローダーを読み込む

作成したカスタムローダーを定義ファイルで使用します

src/content.config.ts
import { microcmsLoader } from "./lib/microcms-loader";

// microCMS ブログ記事
const blogCms = defineCollection({
  loader: microcmsLoader({ endpoint: "blogs" }),
  schema: blogSchema
});

ブログページでコレクションを取得する

今回の場合はmdファイル、microCMSデータどちらとも同じブログページで使用したいので、どちらも読み込んで結合する処理をしていきます

ブログ一覧ページ

src/pages/blog/[...page].astro
---
import type { GetStaticPaths } from "astro";
import Layout from "../../layouts/Layout.astro";
import { getCollection } from "astro:content";

export const getStaticPaths = (async ({ paginate }) => {
  const localPosts = (await getCollection("blog", ({ data }) => !data.draft))
    .map((post) => ({
      slug: post.id,
      source: "local" as const,
      data: post.data,
    }));

  const cmsPosts = (await getCollection("blogCms"))
    .map((post) => ({
      slug: post.id,
      source: "cms" as const,
      data: post.data,
    }));

  const posts = [...localPosts, ...cmsPosts]
    .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());

  return paginate(posts, { pageSize: 12 });
}) satisfies GetStaticPaths;

const { page } = Astro.props;
const posts = page.data;
---

<Layou>
  <h1>Blog</h1>

  <div>
    {posts.map((post) => {
      return (
        <a href={`/blog/${post.slug}`}>
          <div>
            <h2>
              {post.data.title}
            </h2>
            <div>
              {post.data.tags.length > 0 && (
                <div>
                  {post.data.tags.map((tag) => (
                    <span>
                      {tag}
                    </span>
                  ))}
                </div>
              )}
            </div>
          </div>
        </a>
      );
    })}
  </div>

  {page.lastPage > 1 && (
    <nav>
      {page.url.prev ? (
        <a
          href={page.url.prev}
        >
          ← Prev
        </a>
      ) : (
        <span>← Prev</span>
      )}

      {Array.from({ length: page.lastPage }, (_, i) => i + 1).map((num) => (
        <a
          href={num === 1 ? "/blog" : `/blog/${num}`}
        >
          {num}
        </a>
      ))}

      {page.url.next ? (
        <a
          href={page.url.next}
        >
          Next →
        </a>
      ) : (
        <span>Next →</span>
      )}
    </nav>
  )}
</Layout>

ブログページ

src/pages/blog/[slug].astro
---
import Layout from "../../layouts/Layout.astro";
import { getCollection, render } from "astro:content";

export async function getStaticPaths() {
  const localPosts = await getCollection("blog", ({ data }) => !data.draft);
  const cmsPosts = await getCollection("blogCms");

  return [
    ...localPosts.map((post) => ({
      params: { slug: post.id },
      props: { post, source: "local" as const },
    })),
    ...cmsPosts.map((post) => ({
      params: { slug: post.id },
      props: { post, source: "cms" as const },
    })),
  ];
}

const { post, source } = Astro.props;
const rendered = source === "local" ? await render(post) : null;
---

<Layout>
  <article">
    <header>
      <h1>
        {post.data.title}
      </h1>
      <div>
        {post.data.tags.length > 0 && (
          <div>
            {post.data.tags.map((tag) => (
              <span>
                {tag}
              </span>
            ))}
          </div>
        )}
      </div>
    </header>
    <div>
      {rendered ? <rendered.Content /> : <Fragment set:html={(post.data as { body: string }).body} />}
    </div>
  </article>
</Layout>

まとめ

  • ブログ記事に画像を載せる場合、mdファイルだと管理がしにくかったんですが、microCMSのデータをコレクションとして定義することで、画像の管理もmicroCMS側で完結できるのでシンプルな構成にすることができました。
    • テキスト中心の記事の場合は、mdファイルで作成
    • 画像を含めた記事を作成する場合は、microCMSで作成
  • Astro 6.0では、新たにLive Content Collectionsが導入され、リクエスト時にデータを取得することができるようになりました。更新されるデータに対して再ビルドなしで表示できるようになっているので、そちらの機能も試してみたいなと思っています
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?