Mar 17, 2024

Astroブログ作成:記事の表示まで

全体像と目標

今回の記事では、Astroでブログを作成する全体像と、記事が表示できるまでの手順を説明します。NuxtやGatsbyの経験があれば、比較的スムーズに移行できる内容になっています。

目標は、以下の機能を備えたブログの構築です。

  • ダークモード対応

  • 目次機能

  • 記事一覧

  • 関連記事

今回は、基本的な記事の表示ができるところまでを説明します。

フォルダの構成

Astroブログのフォルダ構成は以下の通りです。ヒーロー画像がpublicに位置しているのは、OGPにも利用していたので、外部からも簡単に読めるようにしています。また画像サイズも1200x630に統一しています。記事内で利用している画像は、content/imagesの中に保存しています。

Astro フォルダ構成
/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に定義します。

content/blog/about.mdx
---
title: '「58歳のおじさんが自宅にサーバを作ってみました。」'
publishedDate: 2021-01-31
updatedDate: 2024-03-16
heroImage: about.jpg
description: 'クラウドに自分のサーバを構築することは簡単ですが、自分好みのサーバを自宅に構築したいと思います。その備忘録を残していきたいと思います。'
name: akibo.I
tags: []
---
content/config.ts
import { defineCollection, z } from 'astro:content'
 
const blog = defineCollection({
  type: 'content',
  // Type-check frontmatter using a schema
  schema: z.object({
    title: z.string(),
    publishedDate: 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になります。全ての記事のファイル名に合わせてページが作成されます。なお以下のコード例は、関連する記事の前後の記事データも取得しています。

src/pages/blog/[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’)関数を使って記事を取得します。所得した記事の一覧は、日付でソートしています。

src/lib/utility.ts
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コンポーネントは全体のヘッダー、フッターやサイドバーなどの全体に関わるレイアウトとなります。

src/layouts/BlogPost.astro
---
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>
src/layouts/BaseLayout.astro
---
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>
 

これで一通りの記事の表示ができしまた。次回は記事の一覧や今回説明しなかった他のコンポーネントについて記載したいと思います。