You switch paginate to simplePaginate for speed and the footer shows “Page 2 of undefined”. Each Laravel paginator sends a different payload. Here is what each one returns and a Vue footer that works with all of them.
TL;DR
paginatecounts (hastotal,last_page);simplePaginateandcursorPaginatedo not.cursorPaginateneeds a unique order column: order byid.- Render Previous/Next from
prev_page_url/next_page_url; they arenullat the ends. - Add
withQueryString()so filters survive the next page.
The CRUD guide loads posts with paginate(10). That is the right call when the list is small enough to count. This page is the other two paginators, and the Vue markup that breaks when you swap them without reading the payload.
If you are still choosing Inertia, a separate SPA, or Nuxt, start with the stack guide. Sorting the same list is the table guide. Do not call Post::all() and paginate in the browser.
What each method returns#
| Method | Count query | total / last_page | URL |
|---|---|---|---|
paginate(10) | Yes | Yes | ?page=2 |
simplePaginate(10) | No | No | ?page=2 |
cursorPaginate(10) | No | No | ?cursor=… |
paginate(10) runs a count query. The JSON includes total, last_page, current_page, prev_page_url, and next_page_url. The CRUD page can say “Page 2 of 6” because those keys exist.
simplePaginate(10) skips the count. You get current_page and the previous and next URLs. There is no total and no last_page. Use it when the table is large and the footer only needs Previous and Next.
Gotcha: cursors need a unique order
cursorPaginate(10) also skips the count. Instead of ?page=2 it uses a cursor string and returns next_page_url and prev_page_url. There is no last_page. It needs a unique order column, or a unique combination of columns. Two rows with the same published_at do not give the cursor a stable bound, so order by id. Skip and repeat when rows are inserted or deleted between pages is the offset case, paginate and simplePaginate.
$posts = Post::query()
->latest('id')
->cursorPaginate(10);Swap cursorPaginate for simplePaginate when you want page numbers without a total. Keep paginate from the CRUD guide when the count is cheap and the UI shows it.
The Vue footer#
Read posts.data for the rows, the same as the CRUD page. Stop reading last_page unless you called paginate().
<nav class="flex gap-4">
<Link v-if="posts.prev_page_url" :href="posts.prev_page_url">Previous</Link>
<Link v-if="posts.next_page_url" :href="posts.next_page_url">Next</Link>
</nav>prev_page_url and next_page_url are null on the ends. The v-if is the whole empty state. A cursor page and a simple page can share this footer. A counted page can add the current page and last_page beside it.
Tip: keep the filter on page 2
withQueryString() still matters once the list has a search box. Without it, Next drops ?search= and the second page is the unfiltered table.
Post::query()
->when($search !== '', fn ($query) => $query->where('title', 'like', "%{$search}%"))
->latest('id')
->simplePaginate(10)
->withQueryString();A test that locks the shape#
Fail fast if someone swaps the paginator
it('does not claim a last page it did not count', function () {
Post::factory()->count(3)->create();
$this->get('/posts')
->assertOk()
->assertInertia(fn ($page) => $page
->has('posts.data', 3)
->missing('posts.last_page')
->missing('posts.total')
);
});Point that test at whichever method you chose. missing('posts.total') fails if someone “helpfully” switches the controller back to paginate() while the Vue page was written for a simple paginator.
Where this stops#
Loading the next page by appending rows, instead of replacing them, is a partial reload. That is partial reloads. The form, the search box, and paginate(10) stay in the CRUD guide.
Recap#
- Pick the paginator by whether you can afford (and show) a count.
- One Previous/Next footer works for all three.
- Order cursor pages by a unique column.
withQueryString()whenever there is a filter.
FAQ#
Why is last_page undefined in my Vue pagination?
You are using simplePaginate or cursorPaginate, which skip the count. Use paginate or drop last_page from the footer.
When should I use cursorPaginate in Laravel?
For large, frequently changing lists where offset pages skip or repeat rows. Order by a unique column such as id.
Why does page 2 lose my search filter?
Add ->withQueryString() to the paginator so the links keep ?search=.



