Astroでレストパラメーターを使った動的ルーティングで日記を表示する

2023-01-19
2023-01-19

日記とはつまり以下のようなファイル構造を持つコンテンツのことで

- 2023
  - 01
    - 01.md
    - 02.md
    - ...
- 2022
  - ...

AstroではMarkdownファイルを単にsrc/pages以下に配置すれば次のようなURLでアクセスできる。
これはファイルベースのルーティング(静的ルーティング)による。

  • http://localhost:3000/2023/01/01
  • http://localhost:3000/2023/01/02

このままだと単純にパースされたMarkdownの内容が表示されるだけだが、以下のようにメタデータでlayoutを指定してやれば、きちんとデザインを適用できる。

---
layout: ../../../layouts/Layout.astro
---

ただ今回の趣旨としては、すべてのファイルのメタデータでlayoutを指定するのはダルいのでそれを回避したいという話で。
メタデータでlayoutを指定しない。したくない。

と言っても新しい日付のファイルを作るたびにコピペするだけなので、別にやればいいじゃんと言われればそうなのだが、実は今回は他のツールからの移行という事情があって、もともとのMarkdownにはlayoutの記述はない。

スクリプト組んですべてのファイルにlayoutを追加するアプローチも難しくはないのだが、せっかくなので日々の手間が少しでも減る方向でということで、layout指定なしで行ける方法を取ることにした。

そこで動的ルーティングである。

レストパラメーターを使った動的ルーティング

公式ドキュメントのルーティングにあるように、src/pages以下にブラケットで囲んだ動的パラメーターを含むファイルを作成することで、動的ルーティングを行うことができる。

個々のファイルやディレクトリを動的パラメーターにしてsrc/pages/[year]/[month]/[date].astroのようなディレクトリとファイルを作るやり方もできるのだが、レストパラメーターを使うと柔軟性なディレクトリ構造を持ったパスを簡単に定義できるので、今回はそれを使い、src/pages/[...diary_date].astroというファイルを作った。

そのastroファイル内でgetStaticPathsを定義して、ルーティングするすべてのパスを返す。つまり日記のすべてのパスを返す。

なおこれに先立ち日記のファイルはsrc/pagesからsrc/contentに移動している。
src/pages以下に置いておくと静的ルーティングの方が優先されてしまうので動的ルーティングで同じパスを定義しても意味がないからだ。

src/pages/[…diary_date].astro

---
import Layout from '../layouts/Layout.astro';

export async function getStaticPaths() {
  const allPosts = await Astro.glob('../content/*/*/*.md')
  const posts = allPosts.filter((post) => post.file.match(/\d{4}\/\d{2}\/\d{2}.md/))

  return posts.map((post) => {
    const path = post.file.replace(".md", "").replace("index", "").split("/").slice(-3).join("/")
    return {
      params: { diary_date: path },
      props: { post },
    };
  });
}

const { diary_date } = Astro.params
const { Content } = Astro.props.post
---
<Layout title={diary_date}>
  <div class="diary">
    <div>{diary_date}</div>
    <div><Content /></div>
  </div>
</Layout>

以下少し解説。

レイアウトの指定

Astroページコンポーネントのコンポーネントスクリプトでレイアウトをimport。

import Layout from '../layouts/Layout.astro';

これによりすべてのMarkdownファイルでlayoutを指定する必要はなくなる。

動的ルーティングの対象となるファイルの絞り込み

const allPosts = await Astro.glob('../content/*/*/*.md')
const posts = allPosts.filter((post) => post.file.match(/\d{4}\/\d{2}\/\d{2}.md/))

AstroのクエリーはAstro.globで全部取得してからfilterで絞り込むというやり方が標準的なようだ。
globの引数には文字列リテラルしか指定できないので、ここで変数を使って絞るということは残念ながらできない。

記事が増えてくるとパフォーマンスが心配になって来るが、4000件くらい記事があっても数秒でビルド(npm run build)でき、開発サーバー(npm run dev)で見る際は初回の表示以外は瞬時にレンダリングされるので個人の日記程度であれば問題はないであろう。

動的ルーティングのパス

ここではdiary_dateが動的パラメーターで、パスとして2023/01/01のような値を想定し、それをMarkdownファイルパスから取得している。

ファイルパスはAstro.globで取ってきたファイルのプロパティから得る。

return posts.map((post) => {
  const path = post.file.replace(".md", "").split("/").slice(-3).join("/")
  return {
    params: { diary_date: path },
    props: { post },
  };
});

ファイル名とソース内のパラメーターの名前(diary_date)は合わせる必要がある。
変更するときは両方変更しないといけないので注意。

年別アーカイブと月別アーカイブ

以上で目的は達成した。やったね。
毎日layout書かなくていいのはちりも積もって大きい。

同じようにして年別アーカイブや月別アーカイブも作れる。

例えば現状だと以下のようなURLにアクセスするとNot Foundになる。

  • http://localhost:3000/2023/
  • http://localhost:3000/2023/01/

これらのURLで年や月ごとにアーカイブを表示するには、index.mdを追加してsrc/pages/[...diary_month].astrosrc/pages/[...diary_year].astroを作ってやればよい。

- 2023
  - index.md
  - 01
    - index.md
    - 01.md
    - 02.md
    - ...
- 2022
  - ...

Profile

フルスタック気味のフリーランスプログラマー。

どちらかと言うと得意はインフラ構築とサーバーサイドプログラミングですが、フロントエンドもぼちぼちやっています。

最近の興味範囲はWordPress、AWS、サーバーレス、UIデザイン。

愛車はセロー。カメラはペンタックス。旅好きです。横浜在住。