|
| 1 | +--- |
| 2 | +type: tutorial |
| 3 | +title: '任意: コンテンツコレクションを作る' |
| 4 | +description: |- |
| 5 | + 「初めてのAstroブログ」チュートリアル - |
| 6 | + ファイルベースのルーティングからコンテンツコレクションへブログを移行する |
| 7 | +i18nReady: true |
| 8 | +head: |
| 9 | + - tag: title |
| 10 | + content: 'ブログ作成チュートリアル: コンテンツコレクションを作る | Docs' |
| 11 | +--- |
| 12 | +import PackageManagerTabs from '~/components/tabs/PackageManagerTabs.astro'; |
| 13 | +import Box from '~/components/tutorial/Box.astro'; |
| 14 | +import Checklist from '~/components/Checklist.astro'; |
| 15 | +import MultipleChoice from '~/components/tutorial/MultipleChoice.astro'; |
| 16 | +import PreCheck from '~/components/tutorial/PreCheck.astro'; |
| 17 | +import Option from '~/components/tutorial/Option.astro'; |
| 18 | +import { Steps } from '@astrojs/starlight/components'; |
| 19 | + |
| 20 | +以上でAstroの[組み込みのファイルベースルーティング](/ja/guides/routing/#static-routes)を使ったブログができましたが、これを[コンテンツコレクション](/ja/guides/content-collections/)を使うように更新していきましょう。ブログ記事のような、似たようなコンテンツのグループを管理するうえで、コンテンツコレクションは非常に強力です。 |
| 21 | + |
| 22 | +<PreCheck> |
| 23 | + - ブログ記事のフォルダーを`src/blog/`に移動する |
| 24 | + - ブログ記事のフロントマターを定義するスキーマを作成する |
| 25 | + - `getCollection()`でブログ記事のコンテンツとメタデータを取得する |
| 26 | +</PreCheck> |
| 27 | + |
| 28 | +## 学ぶ: ページとコレクション |
| 29 | + |
| 30 | +コンテンツコレクションを使う場合でも、`src/pages/`フォルダーは「About Me」ページのような、個別のページ用には引き続き使います。ただし、ブログ記事をこの特別なフォルダーの外に移すことで、ブログ一覧の生成や各記事の表示に、より強力でパフォーマンスの高いAPIが使えるようになります。 |
| 31 | + |
| 32 | +同時に、各記事に共通する構造を定義する **[スキーマ](/ja/guides/content-collections/#defining-the-collection-schema)** を用意でき、Astroが[Zod](https://zod.dev/)(TypeScript向けのスキーマ宣言・検証ライブラリ)を通じてその構造どおりかどうかを検証してくれることで、コードエディター上でもより的確なガイドや補完を受けられるようになります。スキーマでは、説明や著者などのフロントマターのプロパティを必須にするかどうか、文字列や配列などの各プロパティの型を何にするかを指定できます。結果として、多くの間違いを早い段階で発見でき、問題箇所をはっきり示すエラーメッセージが得られます。 |
| 33 | + |
| 34 | +詳しくはガイドの[Astroのコンテンツコレクション](/ja/guides/content-collections/)を読むか、以下の手順に沿って、基本的なブログを`src/pages/posts/`から`src/blog/`へ移行してみてください。 |
| 35 | + |
| 36 | +<Box icon="question-mark"> |
| 37 | +### 確認テスト |
| 38 | + |
| 39 | +1. `src/pages/`に残すべきなのは、どの種類のページですか? |
| 40 | + |
| 41 | + <MultipleChoice> |
| 42 | + <Option> |
| 43 | + 同じ基本的な構造とメタデータを持つブログ記事 |
| 44 | + </Option> |
| 45 | + <Option> |
| 46 | + ECサイトの商品ページ |
| 47 | + </Option> |
| 48 | + <Option isCorrect> |
| 49 | + 連絡先ページのように、似たページが複数ないもの |
| 50 | + </Option> |
| 51 | + </MultipleChoice> |
| 52 | + |
| 53 | +2. ブログ記事をコンテンツコレクションに移す**利点にならない**ものはどれですか? |
| 54 | + |
| 55 | + <MultipleChoice> |
| 56 | + <Option isCorrect> |
| 57 | + 各ファイルに対してページが自動的に生成される |
| 58 | + </Option> |
| 59 | + <Option> |
| 60 | + Astroが各ファイルについてより多くを把握するため、エラーメッセージが改善される |
| 61 | + </Option> |
| 62 | + <Option> |
| 63 | + よりパフォーマンスの高い関数で、データ取得が改善される |
| 64 | + </Option> |
| 65 | + </MultipleChoice> |
| 66 | + |
| 67 | +3. コンテンツコレクションでTypeScriptを使うと. . . |
| 68 | + <MultipleChoice> |
| 69 | + <Option> |
| 70 | + 不快な気持ちになる |
| 71 | + </Option> |
| 72 | + <Option isCorrect> |
| 73 | + コレクションを理解・検証し、エディター支援を提供してくれる |
| 74 | + </Option> |
| 75 | + <Option> |
| 76 | + `tsconfig.json`で`strictest`を設定する必要がある |
| 77 | + </Option> |
| 78 | + </MultipleChoice> |
| 79 | + |
| 80 | +</Box> |
| 81 | + |
| 82 | +以下の手順では、「初めてのAstroブログ」チュートリアルの最終成果を、ブログ記事用のコンテンツコレクションを追加して拡張する方法を示します。 |
| 83 | + |
| 84 | +## 依存関係をアップグレードする |
| 85 | + |
| 86 | +ターミナルで次のコマンドを実行し、Astroとすべてのインテグレーションをそれぞれ最新にアップグレードします。 |
| 87 | + |
| 88 | + <PackageManagerTabs> |
| 89 | + <Fragment slot="npm"> |
| 90 | + ```shell |
| 91 | + # Astroと公式インテグレーションをまとめてアップグレード |
| 92 | + npx @astrojs/upgrade |
| 93 | + ``` |
| 94 | + </Fragment> |
| 95 | + <Fragment slot="pnpm"> |
| 96 | + ```shell |
| 97 | + # Astroと公式インテグレーションをまとめてアップグレード |
| 98 | + pnpm dlx @astrojs/upgrade |
| 99 | + ``` |
| 100 | + </Fragment> |
| 101 | + <Fragment slot="yarn"> |
| 102 | + ```shell |
| 103 | + # Astroと公式インテグレーションをまとめてアップグレード |
| 104 | + yarn dlx @astrojs/upgrade |
| 105 | + ``` |
| 106 | + </Fragment> |
| 107 | + </PackageManagerTabs> |
| 108 | + |
| 109 | +## 記事用のコレクションを作成する |
| 110 | + |
| 111 | +<Steps> |
| 112 | +1. `src/blog/`という名前の新しい**コレクション**(フォルダー)を作成します。 |
| 113 | + |
| 114 | +2. 既存のブログ記事(`.md`ファイル)をすべて`src/pages/posts/`から、この新しいコレクションへ移動します。 |
| 115 | + |
| 116 | +3. `postsCollection`用の[スキーマを定義する](/ja/guides/content-collections/#defining-the-collection-schema)ために`src/content.config.ts`ファイルを作成します。既存のブログチュートリアルのコードに合わせ、記事のフロントマターで使っているプロパティをすべて定義するために、次の内容をファイルに追加します。 |
| 117 | + |
| 118 | + ```ts title="src/content.config.ts" |
| 119 | + // glob ローダーをインポートする |
| 120 | + import { glob } from "astro/loaders"; |
| 121 | + // `astro:content` からユーティリティをインポートする |
| 122 | + import { defineCollection } from "astro:content"; |
| 123 | + // Zod をインポートする |
| 124 | + import { z } from "astro/zod"; |
| 125 | + // 各コレクションの loader と schema を定義する |
| 126 | + const blog = defineCollection({ |
| 127 | + loader: glob({ pattern: '**/[^_]*.md', base: "./src/blog" }), |
| 128 | + schema: z.object({ |
| 129 | + title: z.string(), |
| 130 | + pubDate: z.date(), |
| 131 | + description: z.string(), |
| 132 | + author: z.string(), |
| 133 | + image: z.object({ |
| 134 | + url: z.string(), |
| 135 | + alt: z.string() |
| 136 | + }), |
| 137 | + tags: z.array(z.string()) |
| 138 | + }) |
| 139 | + }); |
| 140 | + // コレクションを登録するため、collections オブジェクトをエクスポートする |
| 141 | + export const collections = { blog }; |
| 142 | + ``` |
| 143 | + |
| 144 | +4. Astroにスキーマを認識させるには、開発サーバを終了(`Ctrl + C`)して再起動し、チュートリアルを続けます。これで`astro:content`モジュールが定義されます。 |
| 145 | +</Steps> |
| 146 | + |
| 147 | +## コレクションからページを生成する |
| 148 | + |
| 149 | +<Steps> |
| 150 | +1. `src/pages/posts/[...slug].astro`という名前のページファイルを作成します。コレクション内に置いたMarkdownやMDXは、Astroのファイルベースルーティングでは自動的にはページになりません。そのため、各ブログ記事を生成するためのページを自分で用意する必要があります。 |
| 151 | + |
| 152 | +2. 次のコードを追加して[コレクションをクエリし](/ja/guides/content-collections/#querying-build-time-collections)、生成する各ページでスラッグと記事本文が使えるようにします。 |
| 153 | + |
| 154 | + ```astro title="src/pages/posts/[...slug].astro" |
| 155 | + --- |
| 156 | + import { getCollection, render } from 'astro:content'; |
| 157 | +
|
| 158 | + export async function getStaticPaths() { |
| 159 | + const posts = await getCollection('blog'); |
| 160 | + return posts.map(post => ({ |
| 161 | + params: { slug: post.id }, props: { post }, |
| 162 | + })); |
| 163 | + } |
| 164 | +
|
| 165 | + const { post } = Astro.props; |
| 166 | + const { Content } = await render(post); |
| 167 | + --- |
| 168 | + ``` |
| 169 | + |
| 170 | +3. Markdown用レイアウトのなかで記事の`<Content />`をレンダリングします。これですべての記事に共通のレイアウトを指定できます。 |
| 171 | + |
| 172 | + ```astro title="src/pages/posts/[...slug].astro" ins={3,15-17} |
| 173 | + --- |
| 174 | + import { getCollection, render } from 'astro:content'; |
| 175 | + import MarkdownPostLayout from '../../layouts/MarkdownPostLayout.astro'; |
| 176 | +
|
| 177 | + export async function getStaticPaths() { |
| 178 | + const posts = await getCollection('blog'); |
| 179 | + return posts.map(post => ({ |
| 180 | + params: { slug: post.id }, props: { post }, |
| 181 | + })); |
| 182 | + } |
| 183 | +
|
| 184 | + const { post } = Astro.props; |
| 185 | + const { Content } = await render(post); |
| 186 | + --- |
| 187 | + <MarkdownPostLayout frontmatter={post.data}> |
| 188 | + <Content /> |
| 189 | + </MarkdownPostLayout> |
| 190 | + ``` |
| 191 | + |
| 192 | +4. 各記事のフロントマターから`layout`の指定を削除します。レンダリング時にレイアウトでラップされるようになったため、このプロパティはもう不要です。 |
| 193 | + |
| 194 | + ```md title="src/content/posts/post-1.md" del={2} |
| 195 | + --- |
| 196 | + layout: ../../layouts/MarkdownPostLayout.astro |
| 197 | + title: '私の最初のブログ記事' |
| 198 | + pubDate: 2022-07-01 |
| 199 | + ... |
| 200 | + --- |
| 201 | + ``` |
| 202 | +</Steps> |
| 203 | + |
| 204 | +## `import.meta.glob()`を`getCollection()`に置き換える |
| 205 | + |
| 206 | +<Steps> |
| 207 | +5. チュートリアルのブログページ(`src/pages/blog.astro`)のようにブログ記事の一覧がある箇所では、Markdownファイルからコンテンツとメタデータを取得する方法として、`import.meta.glob()`を[`getCollection()`](/ja/reference/modules/astro-content/#getcollection)に置き換える必要があります。 |
| 208 | + |
| 209 | + ```astro title="src/pages/blog.astro" "post.data" "getCollection(\"blog\")" "/posts/${post.id}/" del={7} ins={2,8} |
| 210 | + --- |
| 211 | + import { getCollection } from "astro:content"; |
| 212 | + import BaseLayout from "../layouts/BaseLayout.astro"; |
| 213 | + import BlogPost from "../components/BlogPost.astro"; |
| 214 | +
|
| 215 | + const pageTitle = "私のAstro学習ブログ"; |
| 216 | + const allPosts = Object.values(import.meta.glob("../pages/posts/*.md", { eager: true })); |
| 217 | + const allPosts = await getCollection("blog"); |
| 218 | + --- |
| 219 | + ``` |
| 220 | + |
| 221 | +6. 各`post`に対して返されるデータの参照も更新します。フロントマターの値は、各オブジェクトの`data`プロパティにあります。また、コレクションを使う場合、各`post`オブジェクトが持つのは完全なURLではなく、ページの`slug`です。 |
| 222 | + |
| 223 | + ```astro title="src/pages/blog.astro" "data" "/posts/$\{post.id\}/" del={14} ins={15} |
| 224 | + --- |
| 225 | + import { getCollection } from "astro:content"; |
| 226 | + import BaseLayout from "../layouts/BaseLayout.astro"; |
| 227 | + import BlogPost from "../components/BlogPost.astro"; |
| 228 | +
|
| 229 | + const pageTitle = "私のAstro学習ブログ"; |
| 230 | + const allPosts = await getCollection("blog"); |
| 231 | + --- |
| 232 | + <BaseLayout pageTitle={pageTitle}> |
| 233 | + <p>ここには、私がAstroを学んでいく旅の様子を投稿します。</p> |
| 234 | + <ul> |
| 235 | + { |
| 236 | + allPosts.map((post) => ( |
| 237 | + <BlogPost url={post.url} title={post.frontmatter.title} />)} |
| 238 | + <BlogPost url={`/posts/${post.id}/`} title={post.data.title} /> |
| 239 | + )) |
| 240 | + } |
| 241 | + </ul> |
| 242 | + </BaseLayout> |
| 243 | + ``` |
| 244 | + |
| 245 | +7. チュートリアルのブログプロジェクトでは、`src/pages/tags/[tag].astro`でタグごとのページを動的に生成し、`src/pages/tags/index.astro`でタグ一覧を表示しています。 |
| 246 | + |
| 247 | + 次の2つのファイルにも、上と同じ変更を適用します。 |
| 248 | + |
| 249 | + - `import.meta.glob()`の代わりに`getCollection("blog")`ですべてのブログ記事のデータを取得する |
| 250 | + - `frontmatter`の代わりに`data`ですべてのフロントマターの値にアクセスする |
| 251 | + - 記事の`slug`を`/posts/`パスに足してページのURLを作る |
| 252 | + |
| 253 | + 個別のタグページを生成するページは、次のようになります。 |
| 254 | + |
| 255 | + ```astro title="src/pages/tags/[tag].astro" "post.data.tags" "getCollection(\"blog\")" "post.data.title" ins={2} "/posts/${post.id}/" |
| 256 | + --- |
| 257 | + import { getCollection } from "astro:content"; |
| 258 | + import BaseLayout from "../../layouts/BaseLayout.astro"; |
| 259 | + import BlogPost from "../../components/BlogPost.astro"; |
| 260 | +
|
| 261 | + export async function getStaticPaths() { |
| 262 | + const allPosts = await getCollection("blog"); |
| 263 | + const uniqueTags = [...new Set(allPosts.map((post) => post.data.tags).flat())]; |
| 264 | +
|
| 265 | + return uniqueTags.map((tag) => { |
| 266 | + const filteredPosts = allPosts.filter((post) => |
| 267 | + post.data.tags.includes(tag) |
| 268 | + ); |
| 269 | + return { |
| 270 | + params: { tag }, |
| 271 | + props: { posts: filteredPosts }, |
| 272 | + }; |
| 273 | + }); |
| 274 | + } |
| 275 | + |
| 276 | + const { tag } = Astro.params; |
| 277 | + const { posts } = Astro.props; |
| 278 | + --- |
| 279 | +
|
| 280 | + <BaseLayout pageTitle={tag}> |
| 281 | + <p>{tag}のタグが付いた記事</p> |
| 282 | + <ul> |
| 283 | + { posts.map((post) => <BlogPost url={`/posts/${post.id}/`} title={post.data.title} />) } |
| 284 | + </ul> |
| 285 | + </BaseLayout> |
| 286 | + ``` |
| 287 | + |
| 288 | + <Box icon="puzzle-piece"> |
| 289 | + ### やってみよう - タグインデックスページのクエリを更新する |
| 290 | + |
| 291 | + `src/pages/tags/index.astro`で、ブログ記事に使われているタグを取得するために`getCollection`をインポートして使います。[上記と同じ手順](#importmetaglobをgetcollectionに置き換える)に従ってください。 |
| 292 | + |
| 293 | + <details> |
| 294 | + <summary>コードを表示</summary> |
| 295 | + ```astro title="src/pages/tags/index.astro" "post.data" "getCollection(\"blog\")" ins={2} |
| 296 | + --- |
| 297 | + import { getCollection } from "astro:content"; |
| 298 | + import BaseLayout from "../../layouts/BaseLayout.astro"; |
| 299 | + const allPosts = await getCollection("blog"); |
| 300 | + const tags = [...new Set(allPosts.map((post) => post.data.tags).flat())]; |
| 301 | + const pageTitle = "タグインデックス"; |
| 302 | + --- |
| 303 | + <!-- ... --> |
| 304 | + ``` |
| 305 | + </details> |
| 306 | + </Box> |
| 307 | +</Steps> |
| 308 | + |
| 309 | +## スキーマに合わせてフロントマターを更新する |
| 310 | + |
| 311 | +必要に応じて、レイアウトなどプロジェクト全体で使用しているフロントマターの値のうち、コレクションのスキーマと一致していないものを更新します。 |
| 312 | + |
| 313 | +ブログチュートリアルの例では、`pubDate`は文字列でした。一方、記事のフロントマターの型を定義するスキーマに従うと、`pubDate`は`Date`オブジェクトになります。これを利用して、`Date`オブジェクトで使えるメソッドで日付を整形できます。 |
| 314 | + |
| 315 | +ブログ記事のレイアウトで日付を表示するには、`toLocaleDateString()`メソッドで文字列に変換します。 |
| 316 | + |
| 317 | +```astro title="src/layouts/MarkdownPostLayout.astro" ins="toString()" |
| 318 | +<!-- ... --> |
| 319 | +<BaseLayout pageTitle={frontmatter.title}> |
| 320 | + <p>{frontmatter.pubDate.toLocaleDateString()}</p> |
| 321 | + <p><em>{frontmatter.description}</em></p> |
| 322 | + <p>著者: {frontmatter.author}</p> |
| 323 | + <img src={frontmatter.image.url} width="300" alt={frontmatter.image.alt} /> |
| 324 | +<!-- ... --> |
| 325 | +``` |
| 326 | + |
| 327 | +## RSSの関数を更新する |
| 328 | + |
| 329 | +チュートリアルのブログプロジェクトにはRSSフィードが含まれています。この関数でも`getCollection()`を使ってブログ記事の情報を返す必要があります。返された`data`オブジェクトからRSSの各項目を生成します。 |
| 330 | + |
| 331 | + ```js title="src/pages/rss.xml.js" del={2,11} ins={3,6,12-17} |
| 332 | + import rss from '@astrojs/rss'; |
| 333 | + import { pagesGlobToRssItems } from '@astrojs/rss'; |
| 334 | + import { getCollection } from 'astro:content'; |
| 335 | + |
| 336 | + export async function GET(context) { |
| 337 | + const posts = await getCollection("blog"); |
| 338 | + return rss({ |
| 339 | + title: 'Astro学習者 | ブログ', |
| 340 | + description: 'Astroを学ぶ旅', |
| 341 | + site: context.site, |
| 342 | + items: await pagesGlobToRssItems(import.meta.glob('./**/*.md')), |
| 343 | + items: posts.map((post) => ({ |
| 344 | + title: post.data.title, |
| 345 | + pubDate: post.data.pubDate, |
| 346 | + description: post.data.description, |
| 347 | + link: `/posts/${post.id}/`, |
| 348 | + })), |
| 349 | + customData: `<language>ja-jp</language>`, |
| 350 | + }) |
| 351 | + } |
| 352 | + ``` |
| 353 | + |
| 354 | +コンテンツコレクションを使ったブログチュートリアルの完全な例は、チュートリアルリポジトリの[コンテンツコレクションブランチ](https://github.com/withastro/blog-tutorial-demo/tree/content-collections)を参照してください。 |
| 355 | + |
| 356 | +<Box icon="check-list"> |
| 357 | + |
| 358 | +## チェックリスト |
| 359 | +<Checklist> |
| 360 | +- [ ] コンテンツコレクションを使って、似たコンテンツのグループを管理し、パフォーマンスや整理のしやすさを向上させられる。 |
| 361 | +</Checklist> |
| 362 | +</Box> |
0 commit comments