Nuxt Content × SSG で作る静的ブログ。3:記事一覧ページとページング実装
技術スタック JavascriptNuxt.js

Nuxt Content × SSG で作る静的ブログ。3:記事一覧ページとページング実装

2021.05.09 2021.05.06

こんにちはjunです。前回の記事は詳細ページの実装と静的書き出しを行いました。今回の記事では

  • 記事一覧ページの作成
  • ページング機能

について解説していきます。それでは早速いきましょう。

articlesの一覧ページを作る

pagesディレクトリを設定する

詳細ページのルーティングを作るために以前は以下のようなディレクトリの設定をしました。

├── pages
│   ├── articles
│       ├── _slug.vue

この場合、/articles/{sulg}というURLが有効になります。一覧ページは/articles/というルートでcontent/articles/配下の原稿が一覧となって見れるページです。しかし上記の設定では表示されないので以下のようにします。

├── pages
│   ├── articles
│       ├── index.vue #追加
│       ├── _slug.vue

index.vueというものを足しました。このファイルは/articles/というルートに対応しています。ディレクトリの設定は以上となります。

一覧のデータを取得してレンダーする

それではindex.vueにソースを書いていきましょう。詳細ページでは特定のパスに対応するコンテンツを取得していましたが、一覧ページではarticlesのコンテンツを15件ほど取得するようにしましょう。とりあえずソースを載せます。

page/articles/index.vue
<template>
    <div class="">
        <h1>記事一覧</h1>
        <ul>
        <li v-for="(c,index) in content" :key="index">
            <nuxt-link :to="c.path">{{c.title}}</nuxt-link>
        </li>
        </ul>
    </div>
</template>

<script>
export default {
  async asyncData({ store,$content, params }) {
    const content = await $content('articles')
    .only(['title','path'])
    .sortBy('createdAt', 'desc')
    .skip(0).limit(15)
    .fetch();

    return {
      content,
    }
  }
}
</script>

まずは$content('articles')articles配下のコンテンツを読む指定をします。そして.only(['title','path'])を使用することでtitleとpathのみのデータを取得することができます。このonly()を指定しない場合、bodyプロパティという原稿内容も取得してしまいます。原稿がボリューミーなほど取得コストが大きくなり、静的書き出しなどにも影響されます。そのため一覧などではonly()を使用して必要最低限のプロパティを使用した方がいいです。

sortBy()にて特定プロパティでソートし、後のページングで使いますが.skip(0).limit(15)にて15件の記事を取得します。asyncData()内で記事を取得して、それをリストで出力します。

$content()に対して加えることが可能なメソッドはこちらで確認できます。

Nuxt.jsで内部リンクを作成する時は<nuxt-link>を使用します。tocontent.pathを指定することで詳細ページに移動できるようになります。

一覧ページはこれぐらいで実装できます。今は最初から15件しか取得しないので、大量にある時はページングができるようにしましょう。

ページングを実装する

私のサイトでは/articles/page/2の様なルートで対応しています。この様なルートを設定する場合は以下のようにpages/を設定します。

├── pages
│   ├── articles
│       ├── index.vue 
│       ├── _slug.vue
│       ├── pages          #追加
│            ├──_id.vue    #追加

_id.vueを作成することで/articles/page/{n}という動的ルートが作成されます。そこでは以下のように設定します。

page/articles/page/_id.vue
<template>
    <div class="">
        <h1>記事一覧</h1>
        <ul>
            <li v-for="(c,index) in content" :key="index">
                <nuxt-link :to="c.path">{{c.title}}</nuxt-link>
            </li>
        </ul>
        <ul class="p-pagenation-container">
            <li class="c-pagenation-unit" v-for="(pg) in num" :key="pg.num">
                <nuxt-link v-if="pg.pg" :to="'/articles/page/'+pg.num" :class="(current == pg.num)?'is-current':''">
                    {{pg.num}}
                </nuxt-link>
                <span v-else>
                    {{pg.num}}
                </span>
            </li>
        </ul>
    </div>
</template>

<script>
export default {
    validate({ redirect,params }) {
        if(/[0-9]+/.test(params.id)) return true;
        return redirect('/articles')
    },
    async asyncData({ store,$content, params }) {
        const count = await $content('articles').only('title').fetch();
        const current = params.id;
        if(current > Math.ceil( count.length / store.state.indexPerPage)) redirect('/articles');

        const from = store.state.indexPerPage * (params.id - 1);
        const to = store.state.indexPerPage * params.id;

        const content = await $content('articles')
        .only(['title','path'])
        .sortBy('createdAt', 'desc')
        .skip(from).limit(to)
        .fetch();

        return {
            content,
            current,
            count:count.length
        }
    },
    computed:{
        max(){
            return Math.ceil( this.$route.params.id / 15);
        },
        num(){
            let tmp = [];
            for(let n=1; n<=this.max;n++){
                if(n == 1 || n == this.max){
                    tmp.push({pg:true,num:n});
                    continue;
                }
                if((this.current - 2 <= n) && (n <= this.current + 2)){
                    tmp.push({pg:true,num:n})
                    continue;
                }
                if((this.current - 2 - 1 == n) || (n == this.current + 2 + 1) ){
                   tmp.push({pg:false,num:"..."})
                    continue;
                }
            }
            return tmp;
        }
    },
}
</script>

詳細を解説します。

{id}の値とページ数をチェック

/arciles/page/{id}において{id}が数値のみ許可するようにします。そこでNuxt SSRではvalidate()というものを使用できます。params.id{id}の値が取得できますので、そこで数値であることを確認します。もしそうでなければ、1ページ目にリダイレクトさせます。

数値であっても提供するページを超えた数を指定されては困ります。その時のために{id}のページ数が存在するかをチェックしておきます。もし存在しなければ1ページ目にリダイレクトさせます。

if(current > Math.ceil( count.length / store.state.indexPerPage)) redirect('/articles');
SSRであれば上記の設定は必須ですが、静的書き出しでは正直入りません。なぜなら静的書き出し時にはこのページング分だけのルートしか提供されないためです。静的HTMLで提供する時は存在しないルートにアクセスした時、404のページにリダイレクトさせておくといいです。

ページングに必要な値を取得する

これはページングを実装するために必要なロジックのな話になるので、一部省略しますが必要な値は

  • 何件目から(from)
  • 何件目まで(to)

が必要となります。ソースでは以下のように使用しています。

const from = store.state.indexPerPage * (params.id - 1);     //何件目から
const to = store.state.indexPerPage * params.id;             //何件目まで

const content = await $content('articles')
.only(['title','path'])
.sortBy('createdAt', 'desc')
.skip(from).limit(to)   // ページング取得
.fetch();

params.idが現在のページ数となっていますので、それを参考にしてページングによるコンテンツ取得をします。

最大ページと表示ページ範囲を設定してレンダーする

asyncData()での設定は以上でOKです。params.idから現在ページ数を用いてページングのレンダーを構築します。私のページングでは

  • 1ページ目と最後のページは常に表示
  • 現在ページから2ページ分だけ表示
  • 範囲外のページは「...」で表示する

という仕様で実装されています。詳細な仕組みは上記のソースをみてください。必要分のページのリンクを作成してページングは完成です。

静的書き出しの際には特に気にせず大丈夫。

ページングの設定して次は静的書き出しを行います。ただし前回のようにルートをnuxtに伝えるということは不要です。どうやら/articles/indexを書き出す時にページングのnuxt-linkを辿ってルートを解決してくれているそうです。実際の書き出しでも

✔ Generated route "/articles/page
✔ Generated route "/articles/page/1" 
✔ Generated route "/articles/page/2" 

以上のようなログが確認できました。

次回は

以上で一覧ページの作成とページングが実装終了しました。ページングとページリストはコンポーネント化しておいた方が後々の開発が楽になります。次回はカテゴリーとタグ機能の解説を行います。

Copyright © 2021 jun. All rights reserved.