Astroブログ作成:記事の表示まで
全体像と目標
今回の記事では、Astroでブログを作成する全体像と、記事が表示できるまでの手順を説明します。NuxtやGatsbyの経験があれば、比較的スムーズに移行できる内容になっています。
目標は、以下の機能を備えたブログの構築です。
-
ダークモード対応
-
目次機能
-
記事一覧
-
関連記事
今回は、基本的な記事の表示ができるところまでを説明します。
フォルダの構成
Astroブログのフォルダ構成は以下の通りです。ヒーロー画像がpublicに位置しているのは、OGPにも利用していたので、外部からも簡単に読めるようにしています。また画像サイズも1200x630に統一しています。記事内で利用している画像は、content/imagesの中に保存しています。
/public
/images
/about.jpg (ヒーロー画像)
/src
/content
/blog
/about.mdx (記事)
/images
/xxx.png
config.ts (frontmatterの定義)
/components
/BaseHead.astro
/BaseLayout.astro
/DateTime.astro
/Footer.astro
/Header.astro
/PrevNextNavi.astro
/Tagnavi.astro
/Toc.astro
/lib
/utility.ts
/layouts
/BlogPost.astro
/Baselayout.astro
/pages
index.astro
/blog
/[slug].astro
記事の登録
Gatsbyで利用していたMarkdownファイルを流用し、拡張子をmdからmdxに変更して登録します。また記事のメタデータは、フロントマターに記述します。このフロントマターに合わせてconfig.tsに定義します。
---
title: '「58歳のおじさんが自宅にサーバを作ってみました。」'
pubDate: 2021-01-31
updatedDate: 2024-03-16
heroImage: about.jpg
description: 'クラウドに自分のサーバを構築することは簡単ですが、自分好みのサーバを自宅に構築したいと思います。その備忘録を残していきたいと思います。'
name: akibo.I
tags: []
---
import { defineCollection, z } from 'astro:content'
const blog = defineCollection({
type: 'content',
// Type-check frontmatter using a schema
schema: z.object({
title: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string(),
description: z.string(),
name: z.string(),
tags: z.array(z.string()),
}),
})
export const collections = { blog }
今後説明予定のNotion側でPublishedDateとしていたため、rss.xml.jsのpubDateをpubDate=post.data.publishedDateと変更しました。
記事の表示
記事の表示は、以下のファイルで実装されています。なお共通する関数はutility.tsに集約しています。
-
pages/blog/[slug].astro:記事の読み込みと記事表示
-
lib/utility.ts:記事の取得
-
layout/BlogPost.astro:記事ページレイアウト
-
layout/BaseLayout.astro:すべてのページのベースレイアウト
記事の読み込みと記事の表示
こちらが今回メインとなる[slug].astroになります。全ての記事のファイル名に合わせてページが作成されます。なお以下のコード例は、関連する記事の前後の記事データも取得しています。
---
import BlogPost from '../../layouts/BlogPost.astro'
import { type blog, getBlogs } from '../../lib/utility'
export async function getStaticPaths() {
const posts = await getBlogs(true)
return posts.map((post, index) => {
const prev = index === 0 ? undefined : posts[index - 1]
const next = index < posts.length + 1 ? posts[index + 1] : undefined
return {
params: { slug: post.slug },
props: { post, prev, next },
}
})
}
interface Props {
post: blog
prev?: blog
next?: blog
}
const { post, prev, next } = Astro.props
const { Content, headings } = await post.render()
---
<BlogPost headings={headings} post={post} prev={prev} next={next}>
<Content />
</BlogPost>
記事の取得
こちらが実際にファィルからgetCollection(‘blog’)関数を使って記事を取得します。所得した記事の一覧は、日付でソートしています。
import { getCollection, type CollectionEntry } from 'astro:content'
import { DEFAULT_IMAGE, MAIN_TAGS } from '../consts'
export type blog = CollectionEntry<'blog'>
export async function getBlogs(tag: boolean) {
return sorted(tag, await getCollection('blog'))
}
function sorted(tag: boolean, posts: blog[]): blog[] {
return posts.sort((a: blog, b: blog) => {
return b.data.publishedDate.valueOf() - a.data.publishedDate.valueOf()
})
}
画面レイアウト
BlogPostコンポーネントは、記事ページのレイアウト、BaseLayoutコンポーネントは全体のヘッダー、フッターやサイドバーなどの全体に関わるレイアウトとなります。
---
import type { MarkdownHeading } from 'astro'
import BaseLayout from '../layouts/BaseLayout.astro'
import DateTime from '../components/DateTime.astro'
import PrevNextNavi from '../components/PrevNextNavi.astro'
import Toc from '../components/Toc.astro'
import Tagnavi from '../components/Tagnavi.astro'
import type { blog } from '../lib/utility'
interface Props {
headings: MarkdownHeading[]
post: blog
prev?: blog
next?: blog
}
const { headings, post, prev, next } = Astro.props
---
<BaseLayout
title={post.data.title}
description={post.data.description}
heroImage={post.data.heroImage}
>
<div class="flex justify-center">
<div class="prose min-w-[calc(70%)] px-12 pt-4">
<article>
<div class="text-right">
<DateTime publishedDate={post.data.publishedDate} updatedDate={post.data.updatedDate} />
</div>
<h1>{post.data.title}</h1>
<slot />
<hr class="m-8" /><PrevNextNavi prev={prev} next={next} />
</article>
</div>
<div class="invisible w-0 xl:visible xl:w-full">
<div class="sticky top-16 z-0 min-w-[calc(30%)] pb-0 pl-0 pr-4 pt-2">
<Toc {headings} />
<div class="mt-6">
<Tagnavi />
</div>
</div>
</div>
</div>
</BaseLayout>
---
import BaseHead from '../components/BaseHead.astro'
import Header from '../components/Header.astro'
import Footer from '../components/Footer.astro'
import { Image } from 'astro:assets'
import { getHeroImage } from '../lib/utility'
type Props = {
title: string
description: string
heroImage: string
}
const { title, description, heroImage } = Astro.props
const img = getHeroImage(heroImage)
---
<html lang="ja">
<head>
<BaseHead title={title} description={description} image={'/images/' + heroImage} />
</head>
<body>
<div class="fixed m-0 w-full">
<Header />
</div>
<main>
<div class="hero-image">
{heroImage && <Image class="h-96 w-full object-cover" src={img} alt="" />}
</div>
<slot />
<Footer />
</main>
</body>
</html>
これで一通りの記事の表示ができしまた。次回は記事の一覧や今回説明しなかった他のコンポーネントについて記載したいと思います。