Site cover image

ふつうのITエンジニアの独り言

本業はAndroidとiPhoneのアプリ開発のエンジニアです。将来はフリーランスで海の近くで妻とのんびり暮らすことを夢見て、幅広くIT技術に触れていきたいと思います。このブログはその備忘録と私のポートフォリオとして活動記録を記すものです。

astroを基本から学び、astro-notion-blogを理解したい(その4) ~astro-notion-blogの基本構成の理解する~

目次


はじめに


 前回まではAstroのフレームワークについて公式ドキュメントを参考に学習を進めてきた。Astroの仕組みを理解したいま、あらためてastro-notion-blogのソースコードを見返して、意味不明だった構造をひもといていきたい。

Pagesの構成


src/pages/index.astro

 ブログのトップページとなるファイル。

src/pages/[slug].astro

 ブログの記事毎のページを作成するためのファイル。動的ルーティングを利用して、slugをURLにしている。slugはgetStaticPathsで取得するようになっている。

export async function getStaticPaths() {
  const posts = await getAllPosts()
  return posts.map((post: interfaces.Post) => ({ params: { slug: post.Slug } }))
}
[slug].astroのgetStaticPathsでslugをparamsに格納している。

src/pages/posts/page/[page].astro

 トップページに表示されるブログ記事の一覧が1ページに収まらない場合に、複数ページに分けて一覧を作成するためのファイル。[page]には1〜Nの数字が入り、数字が小さいほど最新の記事一覧になる。

export async function getStaticPaths() {
  const numberOfPages = await getNumberOfPages()

  let params = []
  for (let i = 2; i <= numberOfPages; i++) {
    params.push({ params: { page: i.toString() } })
  }
  return params
}
[page].astroのgetStaticPathsで[page]をparamsに格納している。

 ちなみに、1ページに表示できる記事の最大数は、NUMBER_OF_POSTS_PER_PAGEで定義されている。

export const NUMBER_OF_POSTS_PER_PAGE = 10
server-constants.tsで上限が10に設定されている。

src/pages/posts/tag/[tag].astro

 特定タグをもつ記事一覧の1ページ目を作成するためのファイル。

src/pages/posts/tag/[tag]/page/[page].astro

 特定タグをもつ記事一覧が1ページに収まらない場合に、複数ページに分けて一覧を作成するためのファイル。ここでフォルダ名にも動的ルーティングが使えることを知りました。

 [page]には1〜Nの数字が入り、数字が小さいほど最新の記事一覧になる。[tag].astroとのコードに大差ないが、タイトルにページ番号がなどの違いが見られる。

export async function getStaticPaths() {
  const allTags = await getAllTags()

  let params = []

  await Promise.all(
    allTags.map((tag: SelectProperty) => {
      return getNumberOfPagesByTag(tag.name).then((numberOfPages: number) => {
        for (let i = 2; i <= numberOfPages; i++) {
          params.push({ params: { tag: tag.name, page: i.toString() } })
        }
      })
    })
  )

  return params
}
動的ルーティングのために[tag]と[page]をparamsに格納していることが分かる。

レイアウトの利用


Layout.astro

 全てのページからimportされて使われているレイアウトファイル。

graph TD
  Layout("Layout.astro")
  Layout --> |import|Index("index.astro")
  Layout --> |import|Page("posts/page/[page].astro")
  Layout --> |import|Slug("posts/[slug].astro")
  Layout --> |import|Tag("posts/tag/[tag].astro")
  Layout --> |import|TagPage("posts/tag/[tag]/page[tag].astro")

 Layout内にはslotが2つあり、<slot>にname属性を埋め込むことで任意のslotに差し込むことが出来るということみたい。例えば、次のように3つのslotを埋め込んだ場合、nameがないslotがデフォルトとなり、name指定した場合に、一致するnameのslotにタグが埋め込まれることになる。


<div>
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot> <!-- 名前なしのデフォルトスロット -->
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>
slotを3つ記述したレイアウトの例。
---
import Layout from '../Layout.astro';
---
<Layout>
  <h1 slot="header">headerのslotに埋め込むタグ</h1>
  <p>デフォルトのslotに埋め込むタグ</p>
  <p slot="footer">footerのslotに埋め込むタグ</p>
</Layout>
レイアウトの呼び出し側でタグにslot属性にnameを指定する。

検索ボタンの仕組み


SearchButton.astro

 左側のメニューに表示される検索ボタンはどのような仕組みになっているのか確認してみた。検索はSearchButton.astroをインポートしており、ボタンを押したときの動作もここで定義されている。また、ボタンだけでなくキーボードでも検索画面が表示されるようにkeydownのイベントが登録されていることが分かる。

  document.addEventListener('DOMContentLoaded', () => {
    function handleKeydown(event) {
      if (event.keyCode === 75 && event.metaKey) {
        // Ctrl+K or Cmd+K
        document.querySelector('.open-search-modal').click()
      }
    }

    document.addEventListener('keydown', handleKeydown)
  })

SerchModal.astro

 SearchButtonを押したとき非表示となっているSerchModalが表示される仕組みとなっているが、SearchButton.astroには検索ボタンが押されたときのイベントハンドルが見当たらない。探したところ、SerchModal.astroに記述されているのを発見した。

    Array.from(document.getElementsByClassName('open-search-modal')).forEach(
      (element) => {
        element.addEventListener('click', openModal)
      }
    )
SearchModal.astroでイベントリスナーを登録している。open-search-modalはSearchButtonのクラス名に割り当てられている。

 検索処理は、事前に読み込んでおいたRSSフィードから検索文字列の比較を行い、部分一致する記事を検索結果の窓に表示する仕組みとなっている。

    async function fetchRSS() {
      const url = new URL(location.href)
      const port = url.port ? `:${url.port}` : ''

      const res = await fetch(
        `${url.protocol}//${url.hostname}${port}/feed`
      )
      if (res.status !== 200) {
        console.log(res.status)
        throw new Error('Failed to fetch RSS feed')
      }

      //以下省略
    }
RSSフィードを読み込む処理
    function search(query) {
      const results = []
      feedItems.forEach((item) => {
        if (item.title.includes(query) || item?.description?.includes(query)) {
          results.push(item)
        }
      })
      return results
    }
RSSフィードのアイテムを順番に判定する。タイトルとディスクリプションが検索の対象になっている。

RSSフィード

 検索に利用しているRSSフィードは、”src/pages/feed.ts”で記述されている。astroのRSSフィード生成に関しては公式にも記載されている。ちなみに、生成されたSSフィードは、<ルートURL>/feedで確認することが出来る。

まとめ


 今回は、astro-notion-blogのページ構造や検索の仕組みを確認した。その中で、以下のことを新たに学ぶことができた。

  • フォルダ名に動的ルーティングが使えること
  • レイアウトにslotを複数記述できること
  • RSSフィードの生成方法

次回はレイアウトを実際に修正して、ブログの見た目を変更することに挑戦していきたい。