LOADING

saco blog logo

Webコーダー日記

Nuxt勉強【はじめてつくるNuxtサイト(三好アキさん著書) – 第5章 ブラッシュアップ編】その2

webコーダーのsaco @sacocco_sacoya です。
前回に引き続きはじめてつくるNuxtサイト(三好アキさん著書)で学んだことを記事にします。
※ もし記述内容として誤りやアドバイスなどがありましたら、Xお問合せフォームにてお知らせいただけるととても喜びます!

教材内の目次では第5章の「ブラッシュアップ編」のページネーション作成から最終章のAPIルートを分割する、までの記録です!

生じた疑問点やエラー、教材と結果が違う箇所など、私が触ってみた結果も交えて記述していきたいと思います。

↓前回の記事はこちら

記事一覧ページを分割する(ページネーション)

ブログ一覧ページにページネーションを作成していきます。
記事の表示数は5件とします。

pages/blog/index.vueのscript内に、以下のコードを追記しました。

<script setup>
//追記 1ページに表示したい記事数
const blogsPerPage = 5

const { data } = await useAsyncData("blogQuery", () => 
    queryContent("/blog")
    .sort({ id: -1 })
    //追記
    .limit(blogsPerPage)
    .find()
)
</script>

blogPerPagesという定数に数値の5を入れて、limitメソッドで表示する記事数を制限しています。

現在の状態で記事一覧ページを表示すると、記事が6件あるうち、5件までの表示となりました。

次に、記事一覧ページが全部で何ページあるかを計算するコードを記述します。

<script setup>
const blogsPerPage = 5

const { data } = await useAsyncData("blogQuery", () => 
    queryContent("/blog")
    .sort({ id: -1 })
    .limit(blogsPerPage)
    .find()
)

//追記 ブログの総数を取得
const allBlogs = await queryContent("/blog").find()

//追記 一覧ページが何ページ必要か計算
const numberPages = Math.ceil(allBlogs.length / blogsPerPage)
</script>

queryContentを使用して、content/blogフォルダの中にあるファイル数を取得します。

Math.ceilという数値を切り上げて整数にするJavascriptの組み込み関数を使用し、「ブログの総数(allBlogs) / 一覧ページでのブログ表示数(blogsPerPage)」として、記事一覧ページが何ページ必要かを計算します。
ここでは6/5=1.2で、数値切り上げのため「2」という数値が numberPagesに入ります。

これがページネーションのリンクの数になります。

2ページ目以降の記事一覧ページを作成する方法

次に、2ページ目以降の記事一覧ページを作成します。
blogフォルダ内にpageフォルダを作成し、この中に[pagination].vueというファイルを作成しました。
このファイルの表示内容はindex.vue(記事一覧ページ)とほぼ同じものなので、index.vueの内容を丸ごとコピーして貼り付けます。

1点違う点として、該当の記事を表示するための表示制限のコードを記述します。

<script setup>
const blogsPerPage = 5

const currentPage = useRoute().params.pagination//追記

const { data } = await useAsyncData("blogQuery", () =>
    queryContent("/blog")
        .sort({ id: -1 })
        .limit(blogsPerPage)
        //追記 該当の記事以外、表示をスキップ
        .skip(blogsPerPage * (currentPage - 1))
        .find()
)

const allBlogs = await queryContent("/blog").find()

const numberPages = Math.ceil(allBlogs.length / blogsPerPage)

</script>

ここで追記した内容として、まず初めにcurrentPageuseRoute()という関数の中にある、現在のルートのパラメータを含むオブジェクトを.paramsで取得しています。
そしてそのparamsの中にあるpaginationの値を取得しています。

useRoute()メソッドをコンソールで出力すると以下のようになっており、中にparamsというオブジェクト、その中にpaginationの値が入っていることが確認できます。

そして.skip()メソッドの中でblogPerPage(5) × (currentPage(2) – 1)として、表示させたい記事より前の記事のデータをスキップしています。

この状態で 〜blog/page/2 にアクセスすると、1つ目の記事が表示されました。

ページネーションリンクを作成する方法

2ページ目以降の記事一覧ページの作成が完了したので、各ページへリンクするためのページネーションを作成します。

componentsフォルダに pagination.vue ファイルを作成し、以下のコードを入力しました。

<template>
    <h2 class="paginationWrapper">
    //v-forディレクティブで、ページネーションリンクを1つずつ出力する
        <NuxtLink v-for="(paginationLink, index) in paginationLinks" :to="paginationLink" :key="index">
        {{ index + 1 }}//リンクとして表示されるページ数
        </NuxtLink>
    </h2>
</template>

<script setup>
//definePropsで親コンポーネントから定数numberPagesを取得
//(numberPagesは、ページネーションの合計ページ数のこと)
const props = defineProps({
    numberPages: Number,
})


const paginationLinks = Array
        .from({ length: props.numberPages },
        (_, i)=> i === 0 ? `/blog`:`/blog/page/${i + 1}`)
</script>

まず<script>の中身から解説します。

definePropsメソッドを使用して、親コンポーネント[pagination].vueで取得した定数のnumberPagesを取得します。

続いてArray.fromメソッドでページネーションのリンクを配列として取得します。

Array.fromメソッドとはjavascriptのメソッドで、配列 or 配列のようなオブジェクトからさらに新しい配列を作成してくれるメソッドです。
Array.fromメソッドの第一引数に { length: props.numberPages } と指定することで、親コンポーネントから受け取ったページネーションの合計数(numberPage)を最大値として、新しく配列を作成しています。

第二引数には、アロー関数の中に三項演算子でページネーションのリンクを生成しています。

(_, i)=>はアロー関数の開始宣言です。
このアロー関数はページネーションリンクを作成するための関数になります。
アロー関数開始宣言内に、2つの引数を指定していますが、これは<template>ファイルの中でページネーションリンクを出力するためのv-forディレクトリと紐づいています。

第一引数に「_」と指定されているのは、アロー関数内において「未使用の引数」という意味になります。(この「_」は慣習的な記述方法です。)

この例において、v-forディレクティブで(item, index)のように引数が2つ取られています。
v-forディレクティブで使用するための引数の数と、アロー関数での引数の数を合わせる必要があるのですが、アロー関数内ではv-forディレクティブでは1つしか引数を使用しないため、開発者にその意図(使用しない関数であること)が伝わりやすくするために「_」を指定しています。
 (引数の記述順序は決まりがないため、(i,_)=>でも同じ結果が得られます。 )

第二引数に記述された「i」には、Array.fromメソッドが生成する配列の各要素のインデックスが入ります。

「i」が0の場合はpagenationLinkが「blog」となり、それ以外の場合は「i + 1」として、[pagination].vueで作成された各ページへのリンクを取得する形になります。

以上でpaginationのリンクコンポーネントが作成できたので、[pagination].vueで呼び出します。

<template>
    <div class="wrapper">
        <div class="container">
            <h1>Blog</h1>
            <p>エンジニアの日常生活をお届けします</p>
            <div v-for="singleData in data" :key="singleData.id" class="blogCard">
                <div class="textsContainer">
                    <h3>{{ singleData.title }}</h3>
                    <p>{{ singleData.excerpt }}</p>
                    <p>{{ singleData.date }}</p>
                    <NuxtLink :to="singleData._path" class="linkButton">Read More</NuxtLink>
                </div>
                <div class="blogImg">
                    <nuxt-img :src="singleData.image" alt="blog-image" format="webp" />
                </div>
            </div>
        </div>
        //↓追加
        <Pagination :numberPages="numberPages"/>
    </div>
</template>

<script setup>
const blogsPerPage = 5

const currentPage = useRoute().params.pagination

const { data } = await useAsyncData("blogQuery", () =>
    queryContent("/blog")
        .sort({ id: -1 })
        .limit(blogsPerPage)
        .skip(blogsPerPage * (currentPage - 1))
        .find()
)

const allBlogs = await queryContent("/blog").find()

const numberPages = Math.ceil(allBlogs.length / blogsPerPage)

</script>

:numberPages の「:」は、v-bindディレクティブの短縮形で、プロパティのバインディング(主に親コンポーネントから子コンポーネントへデータを渡すこと)を示しています。

[pagination].vueで作成したnumberPagesの値を pagination.vueへバインディングし、バインディングされた値を利用してpagination.vueで各ページへのリンクを作成する、という流れです。

ブラウザを確認すると、無事ページネーションが作成されました!

ソースコードを確認してみると、アクティブなリンクには.router-link-activeと.router-link-exact-activeというクラスが自動で付与されていました。

このリンクはNuxtLink特有のクラスで、リンク先と該当ページのパスが同じ場合、自動で付与されるクラスのようです。

.router-link-activeはルータリンクが部分的に一致する場合、.router-link-exact-activeはルータリンクが完全に一致する場合に付与されます。

以上で記事一覧ページのページネーションが完成しました!

メタデータの設定

Nuxt3ではメタデータを設定する方法がいくつかあるようです。
教材内では 一番簡単とされる useHead というメソッドを使用する方法が紹介されていました。

メタデータとは

meta要素は、「meta情報(メタ情報、メタデータ、metaタグ)」とも呼ばれており、「検索エンジン」(Google)や「ブラウザ」(GoogleChrome等)にWebページの情報を伝えるHTMLタグのことです。

記述箇所はページの<head></head>に囲われている領域のタグの中。

参照:メタ情報(meta要素)って何のこと?要素の意味とSEOでの役割

index.vueを開き、<script>内に以下のコードを追加しました。

<script setup>

useHead({
    title:"Abe Hiroki Official site",
    meta: [
        {name: "description", content:"Abe Hirokiのポートフォリオサイトです。",}
    ],
})

//以下省略
</script>

これでサイトタイトルとサイト説明文を追加することができました。

以下のように、同じ要領で他のページにも追加していきます。
contact.vueの他、error.vueblog/index.vueにも適宜追加しました。

//contact.vue

<script setup>
useHead({
    title: "コンタクト",
    meta: [
        { name: "description", content: "コンタクトページです" }
    ],
})
</script>

復習:Nuxt Contentとは

[id].vueにはNuxt Contentが自動的にメタデータを挿入してくれるので、設定は必要ないとのことです。
ここでなぜNuxtContentが自動的にメタデータを挿入してくれるのか疑問が生じたので、NuxtContetを復習しました。

Nuxt Contentとは、contentディレクトリ内のマークダウンやJSONなどのファイルを自動的に読み込み、ページとして生成するための仕組みでした。

contentフォルダ内の各マークダウンファイルには、titleやdescriptionとして各データを記述していました。
このデータがそのまま自動的にメタ情報として読み込まれる仕組みをNuxtContentは持っているとのことです。

↓マークダウンファイルの中の記述

↓ブラウザで1つ目のブログ記事のソースを開くと、各meta情報が追加されていました!


以上で、NuxtContentがcontentフォルダ内にある各マークダウンファイルに記述したデータを元に、自動でメタデータを生成してくれるということがわかりました。

サイト全体に適用させたいメタ情報の設定(ファビコンの設定など)

ファビコンやcharsetなどのサイト全体に適用する必要があるメタデータについては、app.vueファイルに以下のように記述しました。

<template>
    <NuxtLayout>
        <NuxtPage />
    </NuxtLayout>
</template>

<script setup>
//↓追記
useHead({
    viewport: "width=device-width,initial-scale=1,maximum-scale=1",
    charset: "utf-8",
    link:[{rel:"icon",type:"image/x-icon",href:"/images/favicon.ico"}]
})
//↑追記
</script>

生成されたメタ情報は以下の通りです。

他のページにも同様にapp.vueに記入した情報が読み込まれました!
これで無事メタデータが全ページに適用されたことがわかりました。

※ファビコン画像はキャッシュの関係でブラウザに反映されるまで時間がかかる場合があるようです。

APIルート

Nuxtはポートフォリオサイトのようなフロントエンド開発以外にも、サーバー機能を持ったバックエンド開発にも使用できます。

APIルートとは

Web API(Application Programming Interface)のエンドポイント(Web APIで特定の機能やサービスにアクセスするためのURIまたはURL)や、エンドポイントの集合のこと。
APIルートは、外部の開発者がアプリケーションやサービスと対話するための特定のURLやパスを示す。

//例
https://api.example.com/users

このエンドポイントにリクエストを送信して、ユーザーに関する情報を取得したり、変更したりすることができるもの。
有名なAPIサービスとして、Google Maps APIやGitHub APIなどが挙げられる。

APIエンドポイントの作成

ルートフォルダにserver/api/presidents.jsという風に作成し、以下のコードを追加しました。

const data = [
    { name: "Joe Biden", period: "2021-" },
    { name: "Donald Trump", period: "2017-2021" },
    { name: "Barack Obama", period: "2009-2017" },
]
export default defineEventHandler((event) => {
    return {
        api: data
    }
})

この状態で http://localhost:3000/api/presidents へアクセスすると、以下のような画面が表示されます。

presidents.jsに記述した内容が、JSON形式で出力されています。

apiフォルダについて

NuxtのAPIエンドポイント内で配列を直接returnすると、Nuxt.jsがその配列を自動的にJSONとして処理してくれます。
ここで自動的に変換してくれるのは、Nuxtがapiディレクトリを特別なディレクトリとして扱う仕様のためです。
apiディレクトリ内にファイルを配置するだけで、それが自動的にAPIエンドポイントとして機能するようになります。

APIエンドポイントのデフォルトの出力がJSON形式であるのは、WebAPIでのデータ送受信はJSON形式であることが一般的であるためです。

また、APIエンドポイント内でretunされた値は、各ページやコンポーネントで自由に使用できるようになります。

なお、ここで作成したAPIエンドポイントは仮データ(モックアップデータ)として作られたものです。
実際の本番環境では、モックデータのかわりに、リアルなデータベースや外部のAPIと連携するコードに置き換えられるため、記述方法は変わってきます。

参照:server/ · Nuxt ディレクトリ構造 https://nuxt.com/docs/guide/directory-structure/server

まとめ

  • ブログ一覧ページの1ページ目はindex.vue、2ページ目以降はblog/page/[pagination].vueとしてindex.vueをベースにした汎用ファイルを作成する
  • ページネーションはコンポーネントとして作成
  • useHeadメソッドを利用するとメタデータを簡単に付与できる
  • 記事詳細ページにdescriptionなどの情報を記述していれば、NuxtContentがその情報を元に自動的にメタデータを付与してくれる
  • サイト全体に付与したいコードはapp.vueに記述
  • Nuxtはバックエンド開発としても利用できる
  • Nuxtにおいてapiディレクトリは特別なページとして扱われる

まとめは以上になります。

今回は私の知識として足りていなかった部分も合わせて掘り下げていきました。
特に理解が難しかった点が、ページネーション周りで使用されていたArray.fromメソッドやアロー関数などのJava Scriptに関する部分でした。
chatGPTに何度も尋ねたり、ドキュメントを見返したりしながら進めました。
(多分chatGPTに、「この人同じこと何回聞くねん…」と思われてました)
その時に感じたのが、自分の中に生じた「?」を逃さずに、一つずつ理解しようとすることが大事だなと思いました。

また、第一章で「実際のサイトやアプリ制作で必須の設定がすでになされているのがNuxt」という風に学びましたが、APIとのデータ送受信を簡単にする仕組みや、メタデータが簡単に設定できる点などがそれに当たるのかなと理解できました。

今回ではじめてつくるNuxtサイト(三好アキさん著書)の勉強ブログは最後の記事になります。

JSの知識不足という点で一部詰まった時はありましたが、書籍の内容としては全体的に非常にわかりやすく学習がスムーズにできたと感じています。
こちらの書籍でNuxtへの入口ができたことを嬉しく思うと共に、著書の三好アキさんに感謝を記したいと思います。

本日は以上です。

PROFILE

saco profile img

SACO

1990年12月生まれ / 岡山県在住

WEBコーダーです。
2019年から独学でweb製作の勉強を開始し、2020年にコーダーに転身しました。
製作の備忘録など記していきます。
ショートカットキー、notionが好きです。

ポートフォリオサイトです

saco portfolio logo