覚書 (6)

記事の属性(主に日付)

現状はタイトルだけで、これでは不便なのでいろいろと足す。

/src/content/blog/post-1.md
---
title: ブログ記事1
created: 2023-10-01
modified: 2023-10-01
---

## 最初のブログ記事です

あいうえお

追加した属性をコレクションが取得できるようにする。現状だと /src/content/blog/ 以下に空のファイルを作っただけで Astro 君が激怒するため、すべて省略可能にする。

/src/content/config.ts
import { z, defineCollection } from 'astro:content';

const blogCollection = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string().optional(),
    created: z.date().optional(),
    modified: z.date().optional(),
    tags: z.array(z.string()).optional(),
    image: z.string().optional(),
  }),
});

export const collections = {
  blog: blogCollection,
};

日付を表示する

日付(created)をそのまま参照すると Date オブジェクトのクソ長い文字列が表示されるのでフォーマットしたい。また、日付フォーマットは記事本体でも使うだろうから、<time> をコンポーネントにしてしまおう。

なお <time> 要素は書式を守る必要がある。

/src/components/BlogTime.astro
---
interface Props { date: Date; class?: string }
const { date, ...attr } = Astro.props;

const dateTime = (date: Date) => date.toISOString();
const dateStr = (date: Date) => {
  return date.toLocaleDateString('ja-JP', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  });
};
---

<time datetime={dateTime(date)} {...attr}>
  <slot />
  {dateStr(date)}
</time>

さっそく目次で <BlogTime> をインポートする。

/src/pages/blog.astro
---
import { getCollection } from 'astro:content';
import BlogLayout from '../layouts/BlogLayout.astro';
import BlogTime from '../components/BlogTime.astro';

const posts = await getCollection('blog');
---

<BlogLayout title="Blog">
  <h2>記事一覧</h2>
  <ul>
    {
      posts.map((post) => (
        <li>
          {post.data.created && <BlogTime date={post.data.created} />}
          <a href={`/posts/${post.slug}/`}>{post.data.title}</a>
        </li>
      ))
    }
  </ul>
</BlogLayout>

記事の日付は <header> に入れたいので、記事 → レイアウト → <header> とリレーしていくことにする。

  1. 記事ファイルである [slug].astro はそのままでよい(すでにレイアウトに frontmatter を渡している)
  2. レイアウトの BlogLayoutfrontmatter.created を受け取って Header<BlogTime> を投げる。プロパティとして渡してもよいが、せっかくなので <slot> を使ってしまおう
  3. Header.astro もそのまま
/src/layouts/BlogLayout.astro
---
import BlogTime from '../components/BlogTime.astro';
import Head from './Head.astro';
import Header from './Header.astro';
import Footer from './Footer.astro';
import '../styles/style.css';

interface Props {
  title?: string;
  frontmatter?: {
    title?: string;
    created?: Date;
  };
}
const { title = 'ワイのAstro🚀Blog', frontmatter } = Astro.props;
const pageTitle = frontmatter?.title ?? title;


---

<html lang="ja">
  <Head title={pageTitle} />
  <body>
    <Header title={heading}>
      {frontmatter?.created && <BlogTime date={frontmatter?.created}>作成日: </BlogTime>}
    </Header>
    <main>
      <slot />
    </main>
    <Footer />
  </body>
</html>

必須の属性

すべての属性は省略できるけど、titlecreated は記事として表示するためには必須としたい。

まずは目次から。getCollection()filter コールバックで titlecreated両方が未設定ではない記事をリスト入りしている。

/src/pages/blog.astro
---
import { getCollection } from 'astro:content';
import BlogLayout from '../layouts/BlogLayout.astro';
import formatDate from '../components/formatDate';

const posts = await getCollection('blog', ({ data }) => {
  return data.title !== undefined && data.created !== undefined;
});
---

記事も同様に。

/src/posts/[slug].astro
---
import { getCollection } from 'astro:content';
import BlogLayout from '../../layouts/BlogLayout.astro';
import formatDate from '../../components/formatDate';

export async function getStaticPaths() {
  const posts = await getCollection('blog', ({ data }) => {
    return data.title !== undefined && data.created !== undefined;
  });

  return posts.map((entry) => ({
    params: { slug: entry.slug },
    props: { entry },
  }));
}

const { entry } = Astro.props;
const { Content } = await entry.render();
---

なんかガバガバに見えるが、たとえば title には型(string | undefined)が設定されており、違反すると Astro 君がお怒りになるので大丈夫だったりする。

新しい順に並べたい

目次で created の降順に並べ替える。テストも兼ねて新しい記事を作るが割愛する。

/src/pages/blog.astro
---
import { getCollection } from 'astro:content';
import BlogLayout from '../layouts/BlogLayout.astro';
import formatDate from '../components/formatDate';

const posts = await getCollection('blog', ({ data }) => {
  return data.title !== undefined && data.created !== undefined;
});

posts.sort((a, b) => {
  if (!a.data.created || !b.data.created) return 0;
  return b.data.created.valueOf() - a.data.created.valueOf();
});
---