ホーム>Laravel>【Laravel】ページネーション(paginate)の使い方:simplePaginate・cursorPaginateとの違いと、links()の表示・日本語化
Laravel

【Laravel】ページネーション(paginate)の使い方:simplePaginate・cursorPaginateとの違いと、links()の表示・日本語化

いつもご利用ありがとうございます。
この記事にはアフィリエイト広告を含む広告が掲載されており、その広告費によって運営しています。
Amazonのアソシエイトとして、ララテックは適格販売により収入を得ています。

Laravelのpaginate・simplePaginate・cursorPaginateの使い方と違いを、Laravel 13で実際に発行されるSQL・JSON・links()の画面で確かめました。2ページ目で検索条件が消えるときのwithQueryString、ページネーションの日本語化、矢印が巨大になるときの直し方も載せています。

一覧ページで「5件ずつ表示して、下にページ番号を出したい」というときに使うのが、Laravel のページネーションです。

Laravel では paginate() を1つ書くだけで、件数の計算・LIMIT と OFFSET・ページ番号のリンクまで作ってくれます。

ただ、似たメソッドが paginate()・simplePaginate()・cursorPaginate() の3つあって、どれを使えばいいのか迷いますよね?

この記事では、3つのメソッドで実際にどんな SQL が発行されるのか、JSON や画面がどう変わるのかを、Laravel 13 で確かめました!

確かめた環境は、Docker の php:8.4-cli(PHP 8.4.27)・Composer 2.11.2・Laravel 13.42.0(SQLite)、CSS のビルドは node:22(Node 22.23.5)、画面の確認は Chromium 156 です(2026年11月16日)。

composer create-project laravel/laravel で作ったままのプロジェクトに、posts テーブルを1つ足して試しています。

試すために用意した posts テーブルと23件のデータ

posts テーブルは、タイトルとカテゴリーだけの簡単なものです。

Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->string('category');
    $table->timestamps();
});

シーダーで23件入れました。

偶数の記事が laravel、奇数の記事が react というカテゴリーです。

foreach (range(1, 23) as $i) {
    Post::create([
        'title' => "記事{$i}",
        'category' => $i % 2 === 0 ? 'laravel' : 'react',
    ]);
}

23件を5件ずつに分けるので、最後のページは5ページ目(3件)になります。

paginate() で一覧とページ番号を出す

まずは一番よく使う paginate() です。

routes/web.php に、5件ずつ取って一覧のビューに渡すルートを書きました。

use App\Models\Post;
use Illuminate\Support\Facades\Route;

Route::get('/posts', function () {
    $posts = Post::orderBy('id')->paginate(5);

    return view('posts', ['posts' => $posts]);
});

paginate() の引数は、1ページに表示する件数です。

ビューでは、いつもの @foreach で回したあとに {{ $posts->links() }} と書くだけです。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>記事一覧</title>
    @vite(['resources/css/app.css'])
</head>
<body class="p-8">
    <h1 class="text-xl font-bold mb-4">記事一覧</h1>
    <ul class="mb-6">
        @foreach ($posts as $post)
            <li>{{ $post->id }}: {{ $post->title }}({{ $post->category }})</li>
        @endforeach
    </ul>

    {{ $posts->links() }}
</body>
</html>

npm run build で CSS をビルドしてから /posts?page=2 を開くと、6〜10件目と、ページ番号のリンクが出ました。

2ページ目を開いた画面。記事6〜記事10が並び、下に「Showing 6 to 10 of 23 results」と、前へ・1〜5・次へのページ番号のボタンが表示されている

左の「Showing 6 to 10 of 23 results」も、右のボタンも、links() が出したものです。

画面の幅を 400px にすると、ページ番号と件数が消えて「Previous」「Next」の2つのボタンだけになりました。

幅400pxで開いた画面。記事6〜記事10の下に「« Previous」と「Next »」のボタンだけが表示されている

スマホ向けの表示も最初から入っているので、自分で作らなくて大丈夫です。

何ページ目かは ?page= で決まる

どのページを出すかは、URL の ?page=2 の数字で決まります。

コントローラーで page を受け取る処理は書いていませんが、paginate() がリクエストから自動で読んでいます。

ありえない値を入れたときの動きも見てみました。

URL結果
/posts?page=26〜10件目
/posts?page=99エラーにならず、データが空(current_page は 99)
/posts?page=abc1ページ目
/posts?page=-11ページ目

最後のページを超えた数字を入れても 404 にはならず、空の一覧が出ます。

「存在しないページは 404 にしたい」ときは、$posts->isEmpty() などで自分で判定する必要がありそうです。

paginate() は SQL を2回発行する

DB::enableQueryLog() で、paginate(5) が発行した SQL を見てみました。

select count(*) as "aggregate" from "posts"
select * from "posts" order by "id" asc limit 5 offset 0

1回目で全体の件数を数え、2回目で5件だけ取っています。

3ページ目(?page=3)では、2回目が limit 5 offset 10 になりました。

全体の件数が分かるので、「全23件」「最後は5ページ目」のような表示ができるわけです。

simplePaginate() は「前へ」「次へ」だけ

paginate() を simplePaginate() に変えると、ページ番号が消えて「Previous」「Next」だけになりました。

$posts = Post::orderBy('id')->simplePaginate(5);

simplePaginateで2ページ目を開いた画面。記事6〜記事10の下に「« Previous」と「Next »」のボタンだけが左右に表示されている

発行された SQL は1回だけでした。

select * from "posts" order by "id" asc limit 6 offset 0

5件ずつなのに limit 6 になっているのがポイントです。

1件多く取ってみて、6件目があれば「次のページがある」と判断しているようです。

件数を数える count(*) がないので、全体の件数と最後のページ番号は分かりません。

データが多くて count(*) が重いときや、「もっと見る」のような画面なら、こちらで十分です。

cursorPaginate() はページ番号の代わりに cursor を使う

3つ目の cursorPaginate() は、URL に page ではなく cursor という文字列が付きます。

$posts = Post::orderBy('id')->cursorPaginate(5);

1ページ目の JSON(後で書く「API で返す」と同じ方法)の、data 以外の部分はこうなりました。

{
    "path": "http://localhost:8000/api/posts-cursor",
    "per_page": 5,
    "next_cursor": "eyJpZCI6NSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ",
    "next_page_url": "http://localhost:8000/api/posts-cursor?cursor=eyJpZCI6NSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ",
    "prev_cursor": null,
    "prev_page_url": null
}

この cursor を base64 でデコードすると、中身はただの JSON でした。

{"id":5,"_pointsToNextItems":true}

「id が 5 の次から」という意味です。

この cursor を付けて2ページ目を取ったときの SQL がこちらです。

select * from "posts" where ("id" > ?) order by "id" asc limit 6

バインドされた値は 5 でした。

OFFSET を使わず where id > 5 で続きを取っているので、何ページ進んでも、前のページの行を読み飛ばす必要がありません。

その代わり、ページ番号は出せず、画面の表示は simplePaginate と同じ「Previous」「Next」だけになります。

ちなみに、orderBy() を書かずに Post::cursorPaginate(5) としてみたところ、エラーにはならず、SQL に order by "posts"."id" asc が自動で付いていました。

orderByDesc('id') にすると order by "id" desc になります。

3つのメソッドの違い

確かめた結果をまとめるとこうなります。

paginatesimplePaginatecursorPaginate
SQL の回数2回(count と本体)1回1回
続きの取り方OFFSETOFFSETWHERE id > 前の最後の値
URL?page=2?page=2?cursor=eyJ...
ページ番号のリンク出る出ない出ない
全件数(total)・最後のページ分かる分からない分からない

迷ったら、普通の一覧ページは paginate() で大丈夫です。

件数がとても多いテーブルや、無限スクロールのような画面では simplePaginate() や cursorPaginate() を考える、という使い分けになります。

API なら paginator をそのまま return する

API で JSON を返したいときは、paginator をそのまま return するだけで JSON になりました。

Route::get('/api/posts', fn () => Post::orderBy('id')->paginate(5));

/api/posts?page=2 の結果です(data は1件目だけ残し、links は途中を省略しています)。

{
    "current_page": 2,
    "data": [
        {
            "id": 6,
            "title": "記事6",
            "category": "laravel",
            "created_at": "2026-11-15T19:08:14.000000Z",
            "updated_at": "2026-11-15T19:08:14.000000Z"
        }
    ],
    "first_page_url": "http://localhost:8000/api/posts?page=1",
    "from": 6,
    "last_page": 5,
    "last_page_url": "http://localhost:8000/api/posts?page=5",
    "links": [
        {
            "url": "http://localhost:8000/api/posts?page=1",
            "label": "&laquo; Previous",
            "page": 1,
            "active": false
        },
        {
            "url": "http://localhost:8000/api/posts?page=2",
            "label": "2",
            "page": 2,
            "active": true
        },
        {
            "url": "http://localhost:8000/api/posts?page=3",
            "label": "Next &raquo;",
            "page": 3,
            "active": false
        }
    ],
    "next_page_url": "http://localhost:8000/api/posts?page=3",
    "path": "http://localhost:8000/api/posts",
    "per_page": 5,
    "prev_page_url": "http://localhost:8000/api/posts?page=1",
    "to": 10,
    "total": 23
}

links には、ページ番号のボタンを作るための情報が全部入っています。

Vue や React で自分でページ番号を並べるときは、この links を回せば作れます。

simplePaginate() の JSON には total・last_page・links がなく、代わりに current_page_url が入っていました。

2ページ目で検索条件が消えるときは withQueryString()

カテゴリーで絞り込んだ一覧にページネーションを付けると、ハマりやすいところがあります。

Route::get('/posts', function (Request $request) {
    $query = Post::orderBy('id');
    if ($request->filled('category')) {
        $query->where('category', $request->category);
    }
    $posts = $query->paginate(5);

    return view('posts', ['posts' => $posts]);
});

/posts?category=laravel を開くと、1ページ目は laravel の記事だけが並びます。

ところが、links() が作ったリンクはこうなっていました。

http://localhost:8000/posts?page=2
http://localhost:8000/posts?page=3

category=laravel が付いていません。

このリンクで2ページ目に進むと、絞り込みが外れて react の記事も混ざった6〜10件目が出ます。

withQueryString() を付けると、今の URL のクエリ文字列がそのままリンクに引き継がれました。

$posts = Post::where('category', $request->category)
    ->orderBy('id')
    ->paginate(5)
    ->withQueryString();

/posts-qs?category=laravel&sort=new で開いたときのリンクです。

http://localhost:8000/posts-qs?category=laravel&amp;sort=new&amp;page=2
http://localhost:8000/posts-qs?category=laravel&amp;sort=new&amp;page=3

引き継ぐものを選びたいときは、appends() で指定します。

->paginate(5)
->appends(['category' => $request->category]);

同じ URL で開くと、sort=new は付かず category だけが付きました。

http://localhost:8000/posts-appends?category=laravel&amp;page=2
http://localhost:8000/posts-appends?category=laravel&amp;page=3

取るカラムやページを引数で指定する

paginate() には、件数のほかにページも渡せます。

Post::select('id', 'title')->orderBy('id')->paginate(perPage: 3, page: 2);

この書き方で ?page=5 を付けて開いたところ、URL の5ではなく、引数の2ページ目(4〜6件目)が返ってきました。

引数で page を渡すと、URL の page より優先されます。

select() で絞ったカラムだけが data に入り、total は23のままでした。

ページ番号の数を onEachSide() で減らす

ページ数が多いと、links() は途中を「...」で省略します。

2件ずつ(12ページ)にして、onEachSide(1) を付けて6ページ目を開いてみました。

$posts = Post::orderBy('id')->paginate(2)->onEachSide(1);

onEachSide(1)で6ページ目を開いた画面。ページ番号が「1 2 ... 5 6 7 ... 11 12」と表示され、6が選択されている

今のページの左右に1つずつ(5と7)、両端に2つずつが出て、間は「...」になりました。

ページ番号のボタンの数を抑えたいときは、この数を小さくします(^^)

「Showing 6 to 10 of 23 results」を日本語にする

links() の文字は英語なので、日本語にしたいですよね?

「Previous」「Next」と「Showing」などの文字は、置き場所が別でした。

まず、言語ファイルを出します。

php artisan lang:publish

lang/en/pagination.php ができて、中に「Previous」「Next」が入っていました。

'previous' => '&laquo; Previous',
'next' => 'Next &raquo;',

これをコピーして lang/ja/pagination.php を作ります。

<?php

return [
    'previous' => '&laquo; 前へ',
    'next' => '次へ &raquo;',
];

「Showing」「to」「of」「results」は、lang/ja.json に書きます。

ビューの中で {!! __('Showing') !!} のように1語ずつ訳されていて、並び順は「Showing 6 to 10 of 23 results」のまま変えられません。

そこで、この順番のまま日本語として読めるようにしました。

{
    "Showing": "表示中:",
    "to": "〜",
    "of": "件目(全",
    "results": "件)"
}

最後に、.env の APP_LOCALE を ja にします。

APP_LOCALE=ja

日本語化した画面。左に「表示中: 6 〜 10 件目(全 23 件)」、右にページ番号のボタンが表示されている

「表示中: 6 〜 10 件目(全 23 件)」になりました!

「Showing」を空文字 "" にして消せるかも試しましたが、空にすると訳されず「Showing」のまま出ました。

消したいときは、次の参考に書いたビューの書き出しで、ビューのほうを直すことになりそうです。

ページ送りの矢印が画面いっぱいに出るときは CSS が読まれていない

links() の HTML は Tailwind CSS 用です。

@vite(['resources/css/app.css']) の行を外して開くと、こうなりました。

CSSなしで開いた画面。「« 前へ 次へ »」がただのリンクで並び、その下に青い「<」の矢印が画面の幅いっぱいの大きさで表示されている

矢印の SVG に大きさを決める CSS が当たらず、画面の幅いっぱいに広がっています(;'∀')

「ページネーションの矢印が巨大になる」ときは、たいていこれです。

今の Laravel の resources/css/app.css には、ページネーションのビューを Tailwind の対象にする行が最初から入っていました。

@import 'tailwindcss';

@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';

なので、Tailwind を使っているなら、レイアウトに @vite(['resources/css/app.css']) を書いて npm run build(開発中は npm run dev)をすれば直ります。

Vite で「manifest が見つからない」と出るときは、Vite manifest not found の解決方法 を見てください。

Bootstrap を使っているなら useBootstrapFive()

Tailwind ではなく Bootstrap を使っているときは、Bootstrap 用のビューに切り替えます。

tinker で Paginator::useBootstrapFive() を呼んでから links() を出してみたところ、class="pagination"・page-item・page-link という Bootstrap のクラスの HTML に変わりました。

use Illuminate\Pagination\Paginator;

Paginator::useBootstrapFive();

参考: ビューを書き出して自分で直す・ページネーターを2つ置く

  • 公式ドキュメントでは、Paginator::useBootstrapFive() を App\Providers\AppServiceProvider の boot() の中で呼ぶ形が紹介されています。Bootstrap 4 用の useBootstrapFour() もあるそうです。
  • ビューの HTML を自分で直したいときは、php artisan vendor:publish --tag=laravel-pagination で resources/views/vendor/pagination に書き出せるとのことです。既定のビューは、その中の tailwind.blade.php だそうです。
  • 1つの画面にページネーションを2つ置くと、どちらも ?page= を使うのでぶつかります。paginate(15, ['*'], 'users') のように3つ目の引数で名前を変えられるとのことです。
  • cursorPaginate() は、並べ替えに使うカラムがユニーク(または組み合わせでユニーク)である必要があり、null を含むカラムは使えないとされています。
  • URL のパスを変える withPath('/admin/users') や、#users を付ける fragment('users') もあるようです。

出典: Database: Pagination(Laravel 13.x 公式ドキュメント)

まとめ

  • 普通の一覧は paginate()。count と本体の2回の SQL で、ページ番号まで出せます。
  • 件数が要らないなら simplePaginate()。1件多く取って次があるかを見るので、SQL は1回です。
  • 件数が多い・無限スクロールなら cursorPaginate()。OFFSET を使わず where id > ? で続きを取ります。
  • 検索条件を2ページ目に引き継ぐには withQueryString() か appends()。
  • 日本語化は lang/ja/pagination.php と lang/ja.json、矢印が巨大なら CSS のビルドを確かめます。

一覧を取る Eloquent の書き方は Laravel Eloquentの使い方、一覧を @foreach で表示するところは Laravel で foreach を使って一覧を作成する方法、絞り込みの条件は Laravel で where 複数で AND 検索をする方法 にも書いています。

union した結果に paginate() を付けたときの件数の数え方は、unionで違うテーブル同士を一緒に取得する方法 で確かめました。

誰かの参考になれば幸いです!

フィードバックのお願い
この記事のフィードバックがありましたらYoutubeの適当な動画にコメントしていただいたり、お問い合わせからご連絡ください。