Реализация пагинации на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация пагинации на сайте
Простая
от 1 рабочего дня до 3 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация пагинации на сайте

Пагинация делит большой набор данных на страницы фиксированного размера. Это базовый элемент любого сайта с каталогом, таблицей или лентой публикаций. Реализация включает серверную часть (запросы с LIMIT/OFFSET), API-контракт и UI-компонент навигации.

Серверная часть

// Laravel — стандартный 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(),
    ]);
}

Запрос к API: GET /api/products?page=3&per_page=20

Под капотом Laravel формирует два SQL-запроса: COUNT(*) для total и SELECT ... LIMIT 20 OFFSET 40. На таблицах свыше 500 000 строк COUNT может тормозить — решается кешированием total или переходом на приблизительный подсчёт через pg_class.reltuples.

UI-компонент пагинации

// 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="Навигация по страницам">
      <ul className="flex items-center gap-1">
        <li>
          <button
            onClick={() => onPageChange(currentPage - 1)}
            disabled={currentPage === 1}
            aria-label="Предыдущая страница"
          >
            ←
          </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="Следующая страница"
          >
            →
          </button>
        </li>
      </ul>
    </nav>
  )
}

// Формирует массив вида [1, 2, '...', 7, 8, 9, '...', 20]
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

Номер страницы должен жить в URL — иначе пользователь теряет позицию при обновлении страницы и не может поделиться ссылкой.

// 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
}
// Vanilla React — history API
function setPageInUrl(page: number) {
  const url = new URL(window.location.href)
  url.searchParams.set('page', String(page))
  window.history.pushState({}, '', url)
}

Prefetch следующей страницы

Чтобы переходы ощущались мгновенными, подгружаем данные следующей страницы заранее:

import { useQueryClient } from '@tanstack/react-query'

function useProductsPrefetch(currentPage: number) {
  const queryClient = useQueryClient()

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

SEO

Для поисковых роботов страницы с пагинацией должны быть корректно связаны:

<!-- На странице /catalog?page=3 -->
<link rel="prev" href="/catalog?page=2" />
<link rel="next" href="/catalog?page=4" />
<link rel="canonical" href="/catalog?page=3" />

Google официально не использует rel=prev/next с 2019 года, но Яндекс — да. В Laravel Blade или Next.js Metadata API выводить эти теги стоит.

Сроки

Базовый компонент с API-интеграцией — 1 день. С URL-синхронизацией, prefetch, SEO-тегами и адаптивным отображением — 2 дня.