この記事の概要
AstroのContent Collectionに対して公式ドキュメントに記載されていない、ハック的なアプローチで、Markdownコンテンツをページ内で分割表示するためのやり方の記事です。
この記事で紹介する方法は、個人的に見つけたものであり、後述するように「たまたま動いている」可能性があるため、プロダクション環境での使用は推奨されません。
今回の方法は個人サイトを作る中で見つけたものなので、私一人で考えたものに過ぎません(= 他者のレビューを通っていません)。
環境
この記事を執筆している時点でのAstroの最新、4.15.9を使っています。
動作検証や説明のために書いたコードは以下のリポジトリにあります。
実施したいこと
Astroプロジェクトにおいて、以下のようなフォルダ構成でcontent collectionを利用しているとします。
src
├── content
│ └── blog
│ ├── alfa
│ │ └── index.md
│ ├── bravo
│ │ └── index.md
│ └── charlie
│ └── index.md
└── pages
├── blog
│ └── [...slug].astro
└── index.astro
このとき、src/pages/blog/[slug].astro
は大まかには以下のようになります。
---
import { getCollection } from "astro:content";
export async function getStaticPaths() {
const entries = await getCollection("blog");
return entries.map((entry) => ({
params: { slug: entry.slug },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content } = await entry.render();
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>{entry.data.title}</title>
</head>
<body>
<h1>{entry.data.title}</h1>
<Content />
</body>
</html>
このとき、Content
を分割してページを構成したいとします。
例えばブログの最初に要約を載せ、要約とメインコンテンツとの間に特定のコンポーネントを挿入したいとします。
以下のコードは動きません。
+ import { SomeComponent } from "path/to/component"
const { entry } = Astro.props;
- const { Content } = await entry.render();
+ const { SummaryContent, MainContent } = await entry.render();
---
// 中略
<body>
<h1>{entry.data.title}</h1>
- <Content />
+ <SummaryContent />
+ <SomeComponent />
+ <MainContent />
</body>
この通りでは動かないのですが、どうにか似たような挙動ができるように頑張ります。
問題点
まずContent
にはMarkdownファイルを完全にレンダリングした内容が入っています。
そのため途中で分割することができません。
またentry.render()
にはContent
, headings
, remarkPluginFrontmatter
のみが入っており、やはり分割はできなさそうです。
解決策
かなりゴリ押しの解決ではありますが、解決できました。
ファイル構成の変更
まずは以下のようにファイル構成を変更します。
src
├── content
│ └── blog
│ ├── alfa
│ │ ├── index.md
+ │ │ └── _summary.md
│ ├── bravo
│ │ ├── index.md
+ │ │ └── _summary.md
│ └── charlie
│ ├── index.md
+ │ └── _summary.md
└── pages
├── blog
│ └── [...slug].astro
└── index.astro
ファイル名にアンダースコアをつけているのは、ビルド対象から外すためです。
逆に、つけないとexample.com/blog/alfa/summary
でアクセスできてしまいます。
ルーティング処理の変更
ひとまず完成形のコードを載せます。
---
- import { getCollection } from "astro:content";
+ import { getCollection, type CollectionEntry } from "astro:content";
+ export async function getSummary(slug: CollectionEntry<"blog">["slug"]) {
+ try {
+ const summary = await import(`../../content/blog/${slug}/_summary.md`);
+ return summary;
+ } catch (error) {
+ return null;
+ }
+ }
export async function getStaticPaths() {
const entries = await getCollection("blog");
- return entries.map((entry) => ({
- params: { slug: entry.slug },
- props: { entry },
- }));
+ const paths = await Promise.all(
+ entries.map(async (entry) => {
+ const SummaryContent = await getSummary(entry.slug);
+ return {
+ params: { slug: entry.slug },
+ props: { entry, SummaryContent },
+ };
+ }),
+ );
+ return paths;
}
- const { entry } = Astro.props;
+ const { entry, SummaryContent } = Astro.props;
const { Content } = await entry.render();
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>{entry.data.title}</title>
</head>
<body>
<h1>{entry.data.title}</h1>
+ {SummaryContent ? <SummaryContent.default /> : null}
<Content />
</body>
</html>
全体像をざっと箇条書きにすると以下のようになります。
- await importを利用した
getSummary()
を定義する -
getCollection()
の中でgetSummary()
を使い、propsにSummaryContent
を渡す - マークアップ内で
<SummaryContent.default>
を呼ぶ
怪しい箇所
SummaryContent
は<SummaryContent />
だと動かず<SummaryContent.default />
とする必要があります。
最初はAstro.glob()の説明にあるようにAstroInstance
型だからと思いましたが、実際にはany
型でした。
SummaryContent
の中身を見ると次のようになっていました。
{
frontmatter: [Getter],
file: [Getter],
url: [Getter],
rawContent: [Getter],
compiledContent: [Getter],
getHeadings: [Getter],
Content: [Getter],
default: [Function (anonymous)] {
isAstroComponentFactory: true,
moduleId: undefined,
propagation: undefined
},
[Symbol(Symbol.toStringTag)]: 'Module'
}
ここでContent
を見ると次のようになっていました。
[Function (anonymous)] {
isAstroComponentFactory: true,
moduleId: undefined,
propagation: 'self'
}
「似てるな……」と思って試しに<SummaryContent.default>
を指定したら、やりたかった挙動が叶いました。
こんなコードの書き方は仕事では絶対にしない方が良いと思うのですが、それはそれとして似たような挙動を実装したいときもあるような気がして、備忘録のため記事にしました。