このサイトはこうやって作った

Last updated on
このブログではGoogle Adsenseを使ったアフェリエイト広告を導入しています

この記事ではこのサイトをどのようにして作ったかについて記述していく。

サイトリニューアルまでの経緯

もともと、以前までのサイトGoogleサイトで作ったものだった。とにかく独自ドメイン代金以外は無料で使え、容量制限がゆるいサイト作成サービスを検討した結果行き着いた結論だった。

しかしながら、このサイトは決して使いやすいものではなかった。以下のような欠点があったのだ。

  • GUIでのページ作成・レイアウトデザインができてコーディングの素人でも作れるが、その分すべての操作をマウスを使って行わなければならず、コーディング経験者にとっては作りにくい
  • レイアウトのテンプレートが少なく、自分でいじれる部分も少ない
  • サイトの構造上、新しく作ったページへのリンクをいちいち自分で貼らなければいけない
    • これはそういうレイアウトにした僕が悪い部分もあるが……
  • サイトのレスポンスが遅い
    • PageSpeed Insightsでチェックすると山のように改善点が出てくる。Googleのサービスなのに……

ポートフォリオなどのランディングページの作成には向いているのだろうが、ブログのように使うのには問題があったのである。

この問題からサイトの構築・更新は遅々として進まず、2022年の時点ではもはや死に体の状態にあった。

せっかく作った個人サイトなので閉鎖という道は選びたくなかったが、かといってこのままGoogleサイトで続ける気にもなれず、移転先を探した。しかしながら、無料サイト・ブログサービスは以下のような問題があり、なかなかいい移転先が見つからなかった。

  • 運営会社の都合で、サービスが突然終了するかもしれない
  • 大半の無料サービスでは、Google Adsenseなどを使った収益化ができない
  • 無料サービスでは、コストがかからない代償として(サイト運営者が望まない)広告が勝手に載る可能性がある
  • HTTPS化ができないものが大半
  • 独自ドメインが使えないものが多い

WordpressなどのCMSをレンタルサーバーに入れて運用する、という方式も考えたがそれではドメイン代の他に年間約3000〜5000円のサーバーレンタル料がかかる。また、Wordpressは脆弱性の問題もあり、あまり使いたくなかった。

無料のレンタルサーバーもあるにはあるが、

  • 容量が少ない
  • HTTPS化できない
  • 独自ドメインが使えない

といった問題があって使う気にはなれなかった。

そんな中、見つけたのがJamstack・静的サイトジェネレーター(SSG)・ヘッドレスCMSという仕組みだった。

Jamstackについて

Jamstackというのは、もともとはJavaScript・API・Markup(HTMLのこと)という3つの要素を使ったシステム構成(Stack)のことだった。

(現在ではJavaScriptとAPIは必須ではなくなった)

どういうシステムかというと、要するに「サイトのHTMLをあらかじめ作っておいて、インターネット上に表示するためだけのサーバー(ホスティングサーバー)に置いておく」方式と言える。

詳しい説明は下記のサイトに譲るが、

これまで主流だったWebサイトでは閲覧者がサイトにアクセスするたびにWebサーバーがデータベースにアクセスし、コンテンツのデータを取ってきてそれをもとにHTMLを組むという方式だったためレスポンスが遅く、セキュリティ的にも脆弱性があった。それに対し、Jamstackではサイトを作ったとき・コンテンツを作ったときにHTMLを作っておく。データベースからコンテンツのデータを取ってくるのはそのHTMLを作るときだ。この方式だとアクセスがあるたびにHTMLを生成するのではないのでレスポンスが速く、生成する際に変なスクリプトを埋め込まれるといったセキュリティ上の脆弱性も少ない、というわけだ。

そして、作るサイトが個人サイトの範疇であればもう一つ利点がある。それは、費用が安く抑えられるということだ。Jamstackでサイトを作る際に使用するホスティングサービスやヘッドレスCMSといったサービスは個人利用であれば無料で使えるものが多いのである。実際、このサイトにかかっている費用は独自ドメインの更新料(年間1500円程度)だけだ。

静的サイトジェネレーター(SSG)とは?

ただ、HTMLを生成して置いておくといっても、そのHTMLをいちいち手書きで作成するのは難しい。そこで、テンプレートに基づいて自動的にHTMLを生成してくれるプログラムが静的サイトジェネレーター(Static Site Generator, SSG)である。

SSGはJavaScript(Node.js)やGoなどのプログラミング言語で作られており、ユーザーが自分の作りたいサイトのイメージに基づいてテンプレートをプログラミングすることにより、ビルドを行うことでコンテンツのデータを組み込んだHTMLを組んでくれる。

ヘッドレスCMSについて

最近のSSGにはMarkdown形式(.md)のファイルにコンテンツを書いて指定のフォルダに置いておくことでコンテンツをビルドの際にHTMLに組み込んでくれるものもある。そのため、データベースを使わなくてもサイトを作ることはできる。ただ、いちいちファイルを作ってフォルダに置いてビルドする、というのは面倒だ。そのため、データベースにコンテンツのデータを登録し管理するためにヘッドレスCMSというものを使う。

ヘッドレスCMSとは、Wordpressなどの既存のCMSから閲覧者が実際に見る部分(フロントエンド)を取り除いたもの(ヘッドレス)と考えていい。ヘッドレスCMSで提供するのはデータベースに記事を登録・管理する部分だけで、サイトのフロントエンドの部分は自分で好きなように用意してくれ、というわけである。

ヘッドレスCMSからデータを取ってくるのにはAPIという仕組みを使う。これは、インターネットを介してアプリケーションの機能を利用するための仕組みで、ヘッドレスCMSの場合APIのURLにパラメーターでIDやトークン(APIを利用するためのパスワードのようなもの)、取ってきたいデータを指定するパラメーターをつけてアクセスすることで、データがJSONという形式の文字列で返ってくる。

(この動作をよく「APIを叩く」という。ピアノの鍵盤を叩くと音が出るようにAPIを叩くとデータが返ってくるというわけだ)

SSGの中でこのAPIを叩くことで記事のデータを取ってきて、HTMLの中に組み込んで生成してくれるわけである。

ヘッドレスCMSというサービスについて知ったのは2022年6月に転職した新しい会社での業務でのことだった。僕はWebエンジニア見習いという立場なのだが、その中でヘッドレスCMSを使った業務があり、奇しくもその経験がこのサイトを作る上で役立つことになった。

ホスティングサービスについて

本記事でいうホスティングサービスとは静的コンテンツ(HTML)をWeb上に公開することに特化したWebサービスで、いわゆるレンタルサーバー(さくらレンタルサーバーやX-Serverなど)のようにデータベースを持っていないものを指す。データベースを置かず、CMSのようにアクセスのたびにHTMLを組むのではなく、あらかじめ作ったHTMLを公開するだけなので運用上のコストが安く抑えられ、堅牢性も高い。

最近はSSGのコードをGitHubなどのサービスにアップロードし、ホスティングサービスと連携することで、自動的にビルドの作業をやってくれるものが多くなった。代表的なものとしてはNetlifyVercelCloudflare Pagesなどがある。

どういうサービスを使ったか

ヘッドレスCMSやホスティングサービスを使ったJamstack構成ならコストを抑えた上で高速かつ堅牢なサイトを作れるらしい、とわかったため、早速新たなサイトの制作に向けて動き出した。2022年11月末頃のことである。

最初に問題になるのはどのようなサービスやフレームワークを使ってサイトを作っていくかということだった。

SSG:Astro

サイトのHTMLを生成するSSGにはNext.js・Nuxt.js・Gatsby・Hugoなどがあるが、今回使用したのはAstro(Astro.js)というものである。Astroは2022年にVer1.0がリリースされたばかりのニューフェイスのSSGで、2024年1月現在ではVer4.1が最新版となっている。

Astroが優れている点は主に次に挙げる点である。

ページの表示が速い

Astroはとにかくページの表示が他のSSGと比べて速い。以下はAstro公式サイトのトップページにあるグラフだが、これによればGatsbyやNext.jsなどと比べて圧倒的に速いと謳う。

Astro公式サイトトップページの表示速度グラフ

これは、生成されるページの中のJavaScriptを極力減らしているからであるという。曰く、「ページ内の未使用のJavaScriptをすべて削除することで、すべてのサイトをデフォルトで高速に保つことができます」というらしい。確かに、PageSpeed Insightsでページ速度を計測すると、一番に改善点として挙げられるのは「不要なJavaScriptを削除しろ」ということだ。その意味では、Astroの方針は理にかなっている。

ページのパーツ(ヘッダー・本文・フッター等)ごとにコンポーネントに分けて作成できる

Astroでは、WebページをAstro Islandsというコンポーネントごとに分けて設計・作成できる。

Astro Islandsについての説明図

こうすることで、ページごとに共通するコンポーネントを流用できるだけでなく、ヘッダーは動的コンテンツ、それ以外は静的コンテンツといったふうにコンポーネントごとの動作を分けることができる。

ReactやVueなどのライブラリも使うことができる

さらに、Astro IslandsではReact・Preact・Solid・Svelte・Vue・LitなどのUIフレームワークライブラリを使用することができる。動的コンテンツが必要な部分はこれらのライブラリを使うことで補うことができる。実際、このサイトでも検索結果ページなどでPreactを使用している。

この他にもAstroには特徴・利点があるが、詳しくは検索してみてほしい。

ヘッドレスCMS:microCMS

サイトの中身である記事や画像コンテンツを管理するヘッドレスCMSにはmicroCMSを使用している。

microCMSは国産のヘッドレスCMSサービスであり、日本語でのサポートが充実しているのが特徴である。また、無料プランでできることが多く、特に次に挙げる点のメリットが大きい。

  • APIリクエスト数:無制限
    • JamstackにおいてはAPIを叩くことで記事のデータを取ってくるが、この回数(リクエスト回数)に制限があると頻繁な更新ができないし、1回のビルドあたりのリクエスト回数が少なくなるようにアプリを設計しなければいけない。このような制限がかかるとサイトの制作や運用がやりにくくなるので、制限がないというのは非常にありがたい。
  • コンテンツ数:10,000件
    • コンテンツとはAPIを使って取ってくるデータ(本サイトの場合記事とカテゴリ、外部リンクがコンテンツにあたる)のことだが、本サービスでは無料プランでもコンテンツ数が潤沢だ。外部リンクやカテゴリだけで10,000件も使うとは思えないし、記事を1週間に1記事作成するとしたら10,000件使い切るまでに191年かかり、僕が生きているうちは事欠かない。毎日1記事作成するとしても27年かかる。
      • (そもそもそれまでサービスが存続しているかわからないし、途中でサービスが拡充あるいは縮小する可能性もあるが……)
  • メディアのストレージ容量:無制限
    • 本サイトはイラスト掲載を目的とするサイトであるため、メディア(無料プランの場合、静止画像ファイルのみ)の容量が無制限というのは大変ありがたい。レンタルサーバーではこうはいかない。1メディアのサイズ上限は40MBに制限されているが、イラストの画像ファイル(png、jpg)はせいぜい数MB程度しかないので、ほとんど問題ないだろう。また、Bandwidth(帯域幅。この場合、1ヶ月間の最大データ転送量)が20GB以上になってしまうとAPIが停止してしまうということだが、うちのサイトの過去3ヶ月間のBandwidthを見ても1ヶ月に1GBを超えていることすら稀であるから、おそらく問題ない。

また、microCMSはdiscordコミュニティを用意しており、microCMS社のエンジニアや利用者が交流・情報交換することができる。そこではmicroCMS自体のサービスについての質問だけでなく使用しているSSGフレームワークなどについても質問ができ、microCMS社のエンジニアや利用者の中で知見がある方などが答えてくれる。こういったサポートの良さもmicroCMSのメリットである。

ホスティングサービス:Cloudflare Pages

上記のように、最近主流のホスティングサービスとしてはNetlifyVercelCloudflare Pagesなどが挙げられる。

(この他にもGitHub PagesFirebase Hostingなどもあるが今回は検討しなかった)

これらの中からどれをサイトのデプロイに使うべきか? 今回、決め手となったのは以下のスクラップだった。

これによれば、Vercel及びNetlifyには以下の欠点がある。

  • Vercel
    • 個人プランでは収益化ができない。曰く、Google AdSenseのコードを貼ることすらだめだという。せっかくサイトを作る以上、たとえ利益が雀の涙でも収益化したいと考えていたため、これは痛かった。
  • Netlify
    • 日本国内からのアクセスだとVercel・Cloudflare Pagesに比べてロードが遅い。他2つのサービスだと日本国内にCDN(content delivery network、コンテンツ配信ネットワーク)があるが、Netlifyはないからだという。ローディングに時間がかかるのは大きな欠点だ。

Cloudflare Pagesだとこの2つの欠点はない。無料の個人プランでも収益化ができるし、CDNも日本国内対応で速くアクセスできる。さらに、Cloudflare PagesだとBandwidth(帯域幅。この場合、1ヶ月間の最大データ転送量)も無制限だという。ニッチな趣味の個人サイトであるため月に何万件もPVがあるとは考えられないが、制限がないというのは一応嬉しい。

というわけで、使用するホスティングサービスはCloudflare Pagesに決定となった。

どんなコードで作ったか

上記GitHubリポジトリが実際のこのサイトのコードである。2022年11月末より開発を始め、2022年12月31日より稼働を開始した。その後も必要に応じてアップデートを行っている。

基本的にはAstroの基本ブログテンプレートを元に必要な機能を付け加えていく形で開発を行った。microCMSのコンテンツをAstroのページとして表示する部分についてはほぼ下の記事を参考に実装した。

ここから先は特に工夫した部分について説明をしていく。

(といっても、多くは先人の知恵を頼ったものだが……)

microCMSからのデータフェッチ部分

microCMSからコンテンツのデータを取ってくるには、上記記事のようにsrc/libraryディレクトリ内のmicrocms.tsというファイルに書いてある関数を使ってデータフェッチ、つまりAPIを叩いてデータを取得する。本ブログの場合、以下の関数が該当する。

src/library/microcms.ts
class CMSBlog {
  @Cache(userCache, { ttl: 300 })
  // export const getBlogs = async (queries?: MicroCMSQueries) => {
  public async getBlogs(queries?: MicroCMSQueries) {
    const data = await client.get<BlogResponse>({ endpoint: "blogs", queries });

    if (data.offset + data.limit < data.totalCount) {
      queries ? (queries.offset = data.offset + data.limit) : "";
      const result: BlogResponse = await this.getBlogs(queries);
      return {
        offset: result.offset,
        limit: result.limit,
        contents: [...data.contents, ...result.contents],
        totalCount: result.totalCount,
      };
    }
    return data;
  }
  public async getBlogDetail(contentId: string, queries?: MicroCMSQueries) {
    return await client.getListDetail<Blog>({
      endpoint: "blogs",
      contentId,
      queries,
    });
  }
  public async getCategories(queries?: MicroCMSQueries) {
    const data = await client.get<CategoryResponse>({
      endpoint: "categories",
      queries,
    });

    if (data.offset + data.limit < data.totalCount) {
      queries ? (queries.offset = data.offset + data.limit) : "";
      const result: CategoryResponse = await this.getCategories(queries);
      return {
        offset: result.offset,
        limit: result.limit,
        contents: [...data.contents, ...result.contents],
        totalCount: result.totalCount,
      };
    }
    return data;
  }
  public async getCategoryDetail(contentId: string, queries?: MicroCMSQueries) {
    return await client.getListDetail<Category>({
      endpoint: "categories",
      contentId,
      queries,
    });
  }
  public async getLinks(queries?: MicroCMSQueries) {
    const data = await client.get<LinkResponse>({ endpoint: "links", queries });

    if (data.offset + data.limit < data.totalCount) {
      queries ? (queries.offset = data.offset + data.limit) : "";
      const result: LinkResponse = await this.getLinks(queries);
      return {
        offset: result.offset,
        limit: result.limit,
        contents: [...data.contents, ...result.contents],
        totalCount: result.totalCount,
      };
    }
    return data;
  }
  public async getLinkDetail(contentId: string, queries?: MicroCMSQueries) {
    return await client.getListDetail<Link>({
      endpoint: "links",
      contentId,
      queries,
    });
  }
}
export const cmsBlog = new CMSBlog();

CMSBlogクラスのうち、getBlogsがブログ記事一覧、getBlogDetailが個別のブログ記事詳細、同様にgetCategories及びgetCategoryDetailがカテゴリ、getLinks及びgetLinkDetailが外部リンクについて一覧及び個別コンテンツを取得する。ここで改良を行った点は2つだ。

再帰関数によるコンテンツ全件取得

microCMSではGET /api/v1/{endpoint}というAPIを叩くことで複数のコンテンツをリスト形式で取ってくることができる。このコードの場合、node.js版のSDK・microcms-js-sdkを使っているため、await client.getである。

だが、ここで問題が発生する。microCMSでは1度の取得件数の制限値があるのだ。

limit

取得件数を指定します。
デフォルト値は10です。
上限値はありませんが、レスポンスサイズ(レスポンスヘッダのcontent-lengthの値)が約5MBを超えるとエラーが発生します。
そのため、大量のコンテンツの全件取得をしたい場合は下記のoffsetパラメータと組み合わせてページング処理を行ってください。

https://document.microcms.io/content-api/get-list-contents#h4cd61f9fa1

デフォルト値が10であるため、上記記事のコードをそのまま使ったのでは10件だけしか記事を取得できない。

解決策として単純なものではlimitパラメータを大きな値(例:10000など)に設定する事が考えられる。しかしながら、その数以上は取得できない。それに、limitパラメータをあまりに大きな値にすると、制限レスポンスサイズの5MBを超えてしまう可能性がある。

そこで、今回以下の記事を参考にして、再帰関数を作って処理を行うことにした。

  public async getBlogs(queries?: MicroCMSQueries) {
    const data = await client.get<BlogResponse>({ endpoint: "blogs", queries });

    if (data.offset + data.limit < data.totalCount) {
      queries ? (queries.offset = data.offset + data.limit) : "";
      const result: BlogResponse = await this.getBlogs(queries);
      return {
        offset: result.offset,
        limit: result.limit,
        contents: [...data.contents, ...result.contents],
        totalCount: result.totalCount,
      };
    }
    return data;
  }

2行目でawait client.get<BlogResponse>({ endpoint: "blogs", queries });としているのはチュートリアル通りだ。だが、4行目以降はif式になっている。ここで、データの総件数がオフセット値とリミット値の合計よりも大きい場合、つまり最初の取得では余っているコンテンツがある場合、オフセット値をオフセット値+リミット値とした上でgetBlogs関数自身を再呼び出ししている。これを全件取得するまで繰り返すことで、一回の呼び出しでのレスポンスサイズ制限を守ったうえで全件取得できるわけだ。この処理をgetBlogs,getCategories及びgetLinksに実装している。

取得したコンテンツのキャッシュ化

Cloudflare Pagesでのデプロイでは通常のSSGモードを使用している。

(Cloudflare用のアダプターを使ったSSR(サーバーサイドレンダリング)もできるにはできるのだが、Cloudflare Pagesでは仕様の関係で環境変数に設定したmicroCMSのAPIドメインとAPIキーがそのままでは読めないという問題がある。詳しくは以下の記事を参照されたし)

ここで問題となってくるのは、デプロイの際に発生するビルドの時間だ。まだ記事が10個とか20個程度しかないのなら1〜2分程度で済むのだが、記事が増えるにつれビルドにかかる時間は増大していく。ひどいときは29記事で4分57秒もかかっていた。このままでは1000記事とかになったら1時間以上はかかってしまう。

そこで、下記記事を参考にしてビルド時間短縮のためのコードを実装した。

行った処理としては上記記事の通り

  1. 静的パス定義時のpropsに記事の中身も一緒に入れる
    src/pages/[...page].astro
    ---
    import BaseHead from "../components/BaseHead.astro";
    import Header from "../components/Header.astro";
    import Footer from "../components/Footer.astro";
    import Sidebar from "../components/Sidebar.astro";
    import Posts from "../components/Posts.astro";
    import PreloadFirstPostEyecatch from "../components/PreloadFirstPostEyecatch.astro";
    import GoogleResponsiveAd from "../components/GoogleResponsiveAd";
    import Pagination from "../components/Pagination.astro";
    import { SITE_TITLE, SITE_DESCRIPTION } from "../config";
    
    //microCMS呼び出し
    import { cmsBlog } from "../library/microcms";
    // const { contents: posts } = await getBlogs({ fields: ["id", "title", "publishedAt","eyecatch","category"] });
    
    export async function getStaticPaths({ paginate }: any) {
      const { contents: posts } = await cmsBlog.getBlogs({
        orders: "-publishedAt",
      });
      // 記事の配列から、1ページに10個づつ入るようにページを生成する
      return paginate(posts, { pageSize: 10 });
    }
    // ページ分割されたデータは、すべて "page" プロパティとして渡される
    const { page } = Astro.props;
    ---
    
    <!DOCTYPE html>
    <html lang="ja">
      <head>
        <BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
        <style>
          ul {
            list-style-type: none;
            padding: unset;
          }
          ul li {
            display: flex;
          }
          ul li time {
            flex: 0 0 130px;
            font-style: italic;
            color: #595959;
          }
          ul li a:visited {
            color: #8e32dc;
          }
    
          div a div {
            margin: 1em auto;
          }
        </style>
        <PreloadFirstPostEyecatch posts={page.data} />
      </head>
      <body>
        <Header />
        <main class="main">
          <Sidebar />
          <section class="contents">
            <Posts posts={page.data} />
            <Pagination page={page} adjacentPageNumber={1} />
            <GoogleResponsiveAd client:only="preact"/>
          </section>
        </main>
        <Footer />
      </body>
    </html>
  2. microCMSから取得した情報をキャッシュする
    src/library/microcms.ts
    import { Cache, CacheContainer } from "node-ts-cache";
    import { MemoryStorage } from "node-ts-cache-storage-memory";
    
    const userCache = new CacheContainer(new MemoryStorage());
    
    // 中略
    
    class CMSBlog {
      @Cache(userCache, { ttl: 300 })
      // export const getBlogs = async (queries?: MicroCMSQueries) => {
      public async getBlogs(queries?: MicroCMSQueries) {
        const data = await client.get<BlogResponse>({ endpoint: "blogs", queries });

この処理を行うことにより、29記事時点でのビルド時間を2分37秒まで圧縮することに成功した。

キャッシュ化処理前のビルド時間
キャッシュ化処理前のビルド時間
キャッシュ化処理後のビルド時間
キャッシュ化処理後のビルド時間

なお、src/library/microcms.tsにこのコードを実装すると、VScode上で以下の警告表示が出る。

VScodeでの警告表示
VScodeでの警告表示

先の記事の執筆者の方にmicroCMSのDiscordサーバーで聞いたところ、これはTypescriptのエラーで、"experimentalDecorators": truetsconfigcompilerOptionsに入れれば消えるという。実際、入れたら消えた。

tsconfig.json
{
  "extends": "astro/tsconfigs/strict",
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact",
    "experimentalDecorators": true
  }
}

ペジネーション

本サイトでは記事一覧ページ及びカテゴリ別ページにおいてペジネーション処理を行っている。

ペジネーション処理においては、専用のコンポーネントを作成し、それを必要な部分で読み込んでいる。

Pagination.astro
---
import type { Page } from "astro";
// PropsでpageとadjacentPageNumber(隣接するページを表示する数)を定義
export interface Props {
  page: Page;
  adjacentPageNumber?: number;
  categoryId?: string;
}
// propsからpageを取得(adjacentPageNumberの初期値は1)
const { page, adjacentPageNumber = 1, categoryId } = Astro.props;
// ページ番号の配列を作成
const pager = [...Array(page.lastPage).keys()].map((i) => ++i);
// リンク先のパスを生成する関数
const getPath = (page: number, cId?: string) => {
  const path = cId
    ? page === 1
      ? `/category/${cId}/`
      : `/category/${cId}/${page}`
    : page === 1
    ? `/`
    : `/${page}`;
  return path;
  //if (page) {
  //    return `./${page}`;
  //} else {
  //    return `./`;
  //}
};
---

<nav class="pagination">
  <ul>
    <!-- 前ページが存在する場合はPREVリンクを表示する -->
    {
      page.url.prev ? (
        <li>
          <a href={page.url.prev}>&#9665;</a>
        </li>
      ) : page.url.current === "/" ||
        page.url.current === `/category/${categoryId}` ? null : categoryId ? (
        <li>
          <a href={`/category/${categoryId}/`}>&#9665;</a>
        </li>
      ) : (
        <li>
          <a href="/">&#9665;</a>
        </li>
      )
    }
    <!-- 現在ページが「隣接ページ数 + 1」を超える場合は先頭ページと...を表示する -->
    {
      adjacentPageNumber + 1 < page.currentPage && (
        <>
          <li>
            <a href={getPath(1, categoryId)}>1</a>
          </li>
          <li>&#8230;</li>
        </>
      )
    }
    <!-- ページ番号の配列リストから「現在ページ +- 隣接ページ数」のページを表示する -->
    {
      pager.map(
        (p) =>
          page.currentPage - adjacentPageNumber - 1 < p &&
          p < page.currentPage + adjacentPageNumber + 1 &&
          (page.currentPage === p ? (
            <li>
              <span>{p}</span>
            </li>
          ) : (
            <li>
              <a href={getPath(p, categoryId)}>{p}</a>
            </li>
          ))
      )
    }
    <!-- 現在ページが「最終ページ - 隣接ページ数」の場合...と最終ページを表示する -->
    {
      page.currentPage < page.lastPage - adjacentPageNumber && (
        <>
          <li>&#8230;</li>
          <li>
            <a href={getPath(page.lastPage, categoryId)}>{page.lastPage}</a>
          </li>
        </>
      )
    }
    <!-- 次ページが存在する場合はNEXTリンクを表示する -->
    {
      page.url.next && (
        <li>
          <a href={page.url.next}>&#9655;</a>
        </li>
      )
    }
  </ul>
</nav>

他にも改良点はあるのだが、多すぎて書ききれない(この記事自体書き始めたのが2023/04であるため、1年以上かかっている)ため、今回の記事ではここまでとする。他の部分については今後おいおい書いていきたい。

もしこの投稿が「いいね!」と思ったら↓のWaveboxから拍手をください

wavebox logo