Zenn
🚀

Astro の Content Layer を使って Zenn や Qiita に投稿した記事をブログ一覧に表示する

2025/02/13に公開

はじめに

Astro を使って作成している自身のブログとは別に Qiita や Zenn などに技術記事を投稿されている方も多いと思います。私もそうです。

Astro 5 から追加された Content Layer の機能を使えば、それらの外部のコンテンツデータを取り込み、自サイトのコンテンツも一緒混ぜて簡単に表示することができます。

実際に取り込んだ外部データと自ブログのコンテンツを並べて表示している図
https://blog-no-name.com/

Content Layer とはなんぞや、的な話はすでに詳しく書いてくださっている方がいたのでそちらをご覧ください。

今回は RSS フィードから取得したデータを使って Qiita や Zenn の記事を表示する方法について記載しています。おまけで Specker Deck と dev.to からの取得についても載せています。

各サービスの RSS フィードの URL

※ Qiita の RSS フィードの仕様で、取得できるデータが新着 4 件になっています。
https://github.com/increments/qiita-discussions/discussions/204

実装

ライブラリの導入

Astro で RSS フィードのデータを読み込み Content Layer に組み込んで使用するにあたり、コミュニティによって公開されている@ascorbic/feed-loader の Content Loader を利用します。

npm install @ascorbic/feed-loader

content.config.ts の修正

astro の設定ファイルを修正して Loader を有効化し、各種フィードを読み込む設定を追加します。

content.config.ts
import { defineCollection } from "astro:content";
import { feedLoader } from "@ascorbic/feed-loader";

const qiitas = defineCollection({
  loader: feedLoader({
    url: 'http://qiita.com/ユーザー名/feed',
  }),
});

const zenns = defineCollection({
  loader: feedLoader({
    url: 'https://zenn.dev/ユーザー名/feed',
  }),
});

const slides = defineCollection({
  loader: feedLoader({
    url: 'https://speakerdeck.com/ユーザー名.rss',
  }),
});

const devTos = defineCollection({
  loader: feedLoader({
    url: 'https://dev.to/feed/ユーザー名',
  }),
});

export const collections = {
  slides,
  qiitas,
  zenns,
  devTos,
};

Astro の任意のコンポーネントで利用する

Loader の設定ができたので、Astro の任意のコンポーネントで外部コンテンツを利用することが可能です。

index.astro
---
import { getCollection } from 'astro:content';

type Post = {
  collection: string;
  id: string;
  data: {
    title?: string;
    pubdate?: Date;
    link?: string;
    description?: string;
  };
};


const buildPosts = async (): Promise<Post[]> => {
  // Loader を使用してコンテンツを取得する
  const getAllPosts = async (): Promise<Post[]> => {
    const slides = (await getCollection('slides')) as Post[];
    const qiitas = (await getCollection('qiitas')) as Post[];
    const zenns = (await getCollection('zenns')) as Post[];
    const devTos = (await getCollection('devTos')) as Post[];
    return [...slides, ...qiitas, ...zenns, ...devTos];
  };

  // 取得したコンテンツを日付で新しい順にソートする
  const sortPostsByDate = (posts: Post[]): Post[] => {
    return posts.sort((a, b) => {
      return (b.data.pubdate?.getTime() ?? 0) - (a.data.pubdate?.getTime() ?? 0);
    });
  };

  return sortPostsByDate(await getAllPosts());
};

const posts = await buildPosts();

const getPostHref = (post: Post): string => {
  return post.data.link ?? '';
};
---
<main>
  <h1>Super Cool Blog</h1>
  <ul>
    {
      posts.map((post) => (
        <li>
          <a href={getPostHref(post)}>
            <span>
              type:
              {post.collection === 'slides'
                ? 'Speacker Deck'
                : post.collection === 'qiitas'
                  ? 'Qiita'
                  : post.collection === 'zenns'
                    ? 'Zenn'
                    : post.collection === 'devTos'
                      ? 'dev.to'
                      : 'local content'}
            </span>
            <h2>title: {post.data.title ?? ''}</h2>
            <p>pubdate: {post.data.pubdate?.toISOString().slice(0, 10) ?? ''}</p>
          </a>
        </li>
      ))
    }
  </ul>
</main>

自サイトのブログコンテンツを外部コンテンツに混ぜて扱う

Content Layer の機能で、コンテンツが抽象化されているので外部コンテンツと自サイトのコンテンツを混ぜて扱うことも容易です。

まずは content.config.ts を修正して、自サイトのコンテンツを読み込む設定を追加します。
以下の例では、./src/content/blog というディレクトリ配下に .md ファイル作成しているコンテンツを読み込むようにしています。

content.config.ts
import { defineCollection } from "astro:content";
import { feedLoader } from "@ascorbic/feed-loader";

+ const blogs = defineCollection({
+   loader: glob({ pattern: '**/[^_]*.md', base: './src/content/blog' }),
+   schema: z.object({
+     title: z.string(),
+     pubdate: z.coerce.date(),
+   }),
+ });

// 省略

export const collections = {
+ blogs,
  slides,
  // 省略
};

自サイトコンテンツのサンプルです。

src/content/blog/20250213.md
---
title: '2025 年最初のブログだよ'
pubdate: '2025-02-13'
---

ブログ書いたよ。みんな見てね。

先ほど作成した astro ファイル内で、↑のブログコンテンツも外部コンテンツに混ぜて扱うようにしてみます。

index.astro
---
export type Post = {
  // 省略
  data: {
    title?: string;
    pubdate?: Date;
    link?: string;
    description?: string;
  };
+  body?: string; // 自サイトの md ファイルの場合は body に本文が入る
};


const buildPosts = async (): Promise<Post[]> => {
  // Loader を使用してコンテンツを取得する
  const getAllPosts = async (): Promise<Post[]> => {
+   const blogs = (await getCollection('blogs')) as Post[];
    const slides = (await getCollection('slides')) as Post[];
    // 省略
+     return [...blogs, ...slides, ...qiitas, ...zenns, ...devTos];
-     return [...slides, ...qiitas, ...zenns, ...devTos];
  };

  // 省略

  return sortPostsByDate(await getAllPosts());
};

const posts = await buildPosts();

const getPostHref = (post: Post): string => {
+  return post.collection === 'blogs' ? `blogs/${post.id}` : (post.data.link ?? '');
-  return post.data.link ?? '';
};
---

// 省略

このように、すごく簡単に自サイトのコンテンツと外部に公開したコンテンツを一緒に扱えるようになります。
これでどこに記事を書こうが問題なし!

参考情報

Discussion

ログインするとコメントできます