Site pagination: offset, cursor, SEO, and prefetch

Site pagination: offset, cursor, SEO, and prefetch

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1421
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1287
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    984
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1248
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    986
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Website development for FIXPER company
    999

Site pagination: offset, cursor, SEO, and prefetch

Note: when a catalog exceeds 10,000 items or a log table contains 500,000 rows, pagination is essential. In our practice, there was a project with 1.5 million records in PostgreSQL — COUNT executed in 4 seconds even after indexing. Users lost their position when hitting 'Back', and search bots did not index all pages. We implemented pagination for 50+ projects, from e-commerce stores to analytical dashboards. This is especially critical for projects on Laravel and Next.js, where incorrect pagination leads to Core Web Vitals degradation and lower conversion. In this article, we'll break down pagination implementation: from algorithm selection to SEO tags and prefetch.

Choosing a pagination type for an e-commerce store

The choice between offset and cursor depends on the task. Offset pagination is simple: LIMIT 20 OFFSET 40. But on large datasets, COUNT is a bottleneck. We use approximate counting via pg_class.reltuples in PostgreSQL, speeding up the query by 100x. Cursor pagination is faster but does not provide a URL for each page — a problem for SEO.

Criteria Offset Cursor
Speed on 1M rows 2-4 s (COUNT) <10 ms
Stability under inserts Shifting No
SEO compatibility Yes No
Implementation complexity Low Medium

Why prefetch matters for mobile devices?

Prefetch allows loading the next page before a click. On mobile devices with slow connections, this reduces INP by 40%. We use react-query with staleTime: 30_000 and preload data in useEffect. Example for Next.js:

useEffect(() => { if (page < lastPage) { queryClient.prefetchQuery({ queryKey: ['products', page + 1], queryFn: () => fetchProducts(page + 1), staleTime: 30_000, }); } }, [page]); 

Performance comparison: prefetch vs no prefetch

Scenario LCP INP Transition time
No prefetch 2.8 s 200 ms 600 ms
With prefetch 2.1 s 120 ms 100 ms

When to use cursor pagination?

Cursor pagination is suitable for real-time feeds (Twitter, comment feed) or APIs with large data volumes where order without shifts is important. We use it in projects with more than 100,000 records with frequent inserts. Cursor pagination requires a unique sort key (e.g., ID or created_at) and does not support arbitrary page jumps, but ensures stability when new records are added.

Step-by-step guide to implementing pagination on Next.js

  1. Define the API contract: choose offset or cursor. For offset, return current_page, last_page, total, per_page.
  2. Implement server logic: use paginate() in Laravel or find().skip().limit() in MongoDB. Cache COUNT via Redis for 5 minutes.
  3. Develop the UI component: with ellipsis, aria-label, responsiveness.
  4. Synchronize with URL: via useSearchParams in Next.js or history.pushState in vanilla JS.
  5. Add prefetch: with React Query or useEffect with fetch.
  6. Implement SEO tags: <link rel="prev"/> and <link rel="next"/> for Yandex. Google does not use rel prev/next for ranking (Google Search Central), but they are important for Yandex.
  7. Test: check LCP < 2.5 s, INP < 200 ms.

Server side: offset vs cursor pagination

To speed up COUNT, we use approximate counting via pg_class.reltuples in PostgreSQL or caching in Redis for 5 minutes. This reduces query time from seconds to milliseconds.

// Laravel — standard paginate() public function index(Request $request): JsonResponse { $perPage = min($request->integer('per_page', 20), 100); $products = Product::where('is_active', true) ->orderByDesc('created_at') ->paginate($perPage); return response()->json([ 'data' => ProductResource::collection($products->items()), 'current_page' => $products->currentPage(), 'last_page' => $products->lastPage(), 'per_page' => $products->perPage(), 'total' => $products->total(), 'from' => $products->firstItem(), 'to' => $products->lastItem(), ]); } 

UI pagination component

The component displays page numbers with an ellipsis for many pages. Buttons have aria-label for accessibility. Active page is highlighted. Styled with Tailwind.

// components/Pagination.tsx interface PaginationProps { currentPage: number lastPage: number onPageChange: (page: number) => void } export function Pagination({ currentPage, lastPage, onPageChange }: PaginationProps) { const pages = buildPageRange(currentPage, lastPage) return ( <nav aria-label="Page navigation"> <ul className="flex items-center gap-1"> <li> <button onClick={() => onPageChange(currentPage - 1)} disabled={currentPage === 1} aria-label="Previous page" > ← </button> </li> {pages.map((page, i) => page === '...' ? ( <li key={`ellipsis-${i}`} aria-hidden="true">…</li> ) : ( <li key={page}> <button onClick={() => onPageChange(page as number)} aria-current={page === currentPage ? 'page' : undefined} className={page === currentPage ? 'font-bold' : ''} > {page} </button> </li> ) )} <li> <button onClick={() => onPageChange(currentPage + 1)} disabled={currentPage === lastPage} aria-label="Next page" > → </button> </li> </ul> </nav> ) } function buildPageRange(current: number, last: number): (number | '...')[] { if (last <= 7) return Array.from({ length: last }, (_, i) => i + 1) const delta = 2 const range: (number | '...')[] = [] const left = current - delta const right = current + delta let prev: number | null = null for (let i = 1; i <= last; i++) { if (i === 1 || i === last || (i >= left && i <= right)) { if (prev !== null && i - prev > 1) range.push('...') range.push(i) prev = i } } return range } 

URL synchronization

The page number must live in the URL — otherwise the user loses position on refresh and cannot share the link. We use useSearchParams in Next.js App Router.

// Next.js App Router 'use client' import { useRouter, useSearchParams, usePathname } from 'next/navigation' function usePageParam() { const searchParams = useSearchParams() const router = useRouter() const pathname = usePathname() const page = Number(searchParams.get('page') ?? '1') const setPage = (newPage: number) => { const params = new URLSearchParams(searchParams.toString()) params.set('page', String(newPage)) router.push(`${pathname}?${params.toString()}`, { scroll: true }) } return [page, setPage] as const } 

What's included in our pagination work

  • Audit of the current pagination scheme (SQL queries, response time)
  • API contract design (offset/cursor, response format)
  • Server logic implementation with COUNT caching
  • UI component development with responsiveness and accessibility
  • URL synchronization (Next.js, React Router, Vanilla)
  • Prefetch setup for instant transitions
  • SEO tag integration (rel prev/next, canonical, sitemap for pagination)
  • Documentation and training for the client's team

Deadlines and cost

A basic component with API integration — from 1 day. A full package with URL synchronization, prefetch, SEO tags, responsive design, and training — from 2 to 4 days. The cost is calculated individually after analyzing your project. Development time savings on ready-made components are up to 30% compared to writing from scratch.

Contact us for a free evaluation of your project. Our engineers with years of experience will help implement pagination considering SEO and performance. We guarantee LCP < 2.5 s and correct indexing in Yandex. Order pagination implementation with a Core Web Vitals guarantee. Get an engineer consultation and a prototype in 1 day.