Astro でも gatsby-plugin-mdx の excerpt を再現

2024/11/22

はじめに

Gatsby から Astro に乗り換えたのですが、 Gatsby では簡単に利用できていた記事本文の抜粋機能が Astro ではちょっと工夫しないと利用できなかったので、 対応内容をまとめてみました。

Gatsby で利用していた gatsby-plugin-mdxexcerpt 情報の再現方法についてです。

事前調査

gatsby-plugin-mdx における excerpt について

コードまでは追っていませんが、ドキュメントを読む限り、 内部的には rehype-infer-description-meta を利用しているとのことでした。

rehype-infer-description-meta 単体では、抽出した descriptionfile.data.meta.description に格納するだけの rehype プラグインなので、 gatsby-plugin-mdx はおそらく、格納された結果を excerpt として参照できるようにしているのかと推測します。

Astro における frontmatter 加工について

Astro でも Gatsby と同様、frontmatter を利用できるわけですが、remark プラグイン、 もしくは rehype プラグインを通して加工することができます。 ドキュメントにサンプルも記載されていますが、 プラグイン内で file.data.astro.frontmatter に対して変更を加えることで、frontmatter を加工できるようです。

ただし、加工結果を取得する場合は、必ず render() メソッドを呼ぶ必要があります(ドキュメント)。

方針

rehype-infer-description-meta が生成した file.data.meta.descriptionfile.data.astro.frontmatter.excerpt に格納するだけのプラグインを自作する。

実装内容

rehype-infer-description-meta-astro-adapter.ts

import type { Root } from "hast";
import type { Plugin } from "unified";

const rehypeInferDescriptionMetaAstroAdapter: Plugin<[], Root> = () => {
  return (_, file) => {
    (
      file.data.astro as {
        // biome-ignore lint/suspicious/noExplicitAny:
        frontmatter: Record<string, any>;
      }
    ).frontmatter.excerpt = file.data.meta?.description ?? "";
  };
};

export default rehypeInferDescriptionMetaAstroAdapter;

astro.config.mjs

// @ts-check
import { defineConfig } from "astro/config";

import rehypeInferDescriptionMeta from "rehype-infer-description-meta";
import rehypeInferDescriptionMetaAstroAdapter from "./src/plugins/rehype-infer-description-meta-astro-adapter";

export default defineConfig({
  markdown: {
    rehypePlugins: [
      [rehypeInferDescriptionMeta, { truncateSize: 140 }],
      rehypeInferDescriptionMetaAstroAdapter,
    ],
  },
});

呼び出し例

export interface PostMeta {
  slug: CollectionEntry<"posts">["slug"];
  data: InferEntrySchema<"posts"> & {
    excerpt: string;
  };
}

export const getPostMetasAsync = async (options?: {
  filter?: (entry: CollectionEntry<"posts">) => boolean;
}): Promise<PostMeta[]> => {
  const filter = options?.filter;
  const posts = await getCollection("posts", filter);
  posts.sort((a, b) => {
    return b.data.date.valueOf() - a.data.date.valueOf();
  });
  return await Promise.all(
    posts.map(async (post) => {
      const { remarkPluginFrontmatter } = await post.render();
      const data: PostMeta["data"] = {
        title: remarkPluginFrontmatter.title,
        date: remarkPluginFrontmatter.date,
        tags: remarkPluginFrontmatter.tags,
        excerpt: remarkPluginFrontmatter.excerpt,
      };
      return {
        slug: post.slug,
        data,
      };
    }),
  );
};

おわりに

今回は gatsby-plugin-mdx に倣って rehype-infer-description-meta を利用したので、データ構造を合わせるためだけの rehype プラグインを自作しましたが、 やりたいこと的には remark プラグインの方があっていそうな気もしますし、色々な解決策がありそうですね。