Пагинация без пропуска материалов
Каталог содержит пять материалов. Разделим его на страницы по две записи, используя paginate в getStaticPaths. Урок посвящён полноте списка и связям между его частями. Пагинация не должна скрывать последнюю запись, повторять один и тот же материал на всех страницах или выводить кнопку за пределами существующего диапазона.
Маршрут становится src/pages/catalog/[...page].astro. Rest-параметр позволяет первой странице занимать корневую позицию каталога. Прежний catalog/index.astro удаляется, чтобы не создавать конкурирующую страницу по тому же адресу. Смена формы маршрута здесь является конкретным изменением исходников, а не добавлением второй версии списка рядом с первой.
Страницы списка как часть данных
paginate получает уже отфильтрованный и отсортированный массив. Статус черновика нельзя применять только после разбиения: тогда на странице могли бы остаться пустые позиции, а число страниц описывало бы другой набор. Порядок тоже фиксируется заранее. Подготовка исходного множества и разбиение должны идти в согласованной последовательности.
Для пяти записей и размера два ожидаются три страницы: две полные и одна с последним материалом. Это полезное граничное условие, потому что ошибка округления проявляется именно на неполной странице. Размер два выбран для наблюдения в маленьком примере; настоящая библиотека может использовать другой предел в зависимости от удобства обзора и содержания карточек.
Страница получает объект page. Данные берутся из page.data, а текущий номер и границы — из свойств того же объекта. Не вычисляем предыдущий URL простым вычитанием числа и конкатенацией строк. page.url.prev и page.url.next учитывают договор маршрута. Их конкретная форма зависит от конфигурации вывода, поэтому ссылка должна использовать предоставленное значение.
Кнопка предыдущей страницы появляется только при наличии prev, следующей — при наличии next. Это обычные ссылки, а не элементы, требующие JavaScript. Посетитель может открыть третью страницу напрямую. История и новая вкладка сохраняют штатное поведение. В маленькой библиотеке такой путь важнее бесконечной прокрутки, которая без дополнительного договора затруднила бы адресацию позиции.
Название вкладки включает номер страницы. H1 продолжает обозначать каталог, а текст навигации сообщает текущую и последнюю страницу. Полезно различать название области и положение в последовательности. Если позднее каждая тема получит собственную пагинацию, параметр темы должен войти в params вызова paginate; простой общий список уже не будет достаточен.
Изменение числа материалов может изменить последнюю страницу. Удаление записи или переключение её в черновик требует новой сборки всех затронутых списков. Если загрузить на хостинг только одну новую HTML-страницу, остальные могут продолжить показывать старые границы. Статический выпуск следует рассматривать как согласованный набор, а не случайную пачку независимых файлов.
Ожидаемый результат — каждый из пяти материалов встречается в объединении страниц ровно один раз. Это ещё не выполненная проверка. При будущем просмотре выпишите ID с каждой страницы, соедините их и сравните с исходной публичной выборкой. Затем рассмотрите первую и последнюю страницы отдельно: отсутствие одной из соседних ссылок там является правильным состоянием, а не недостатком дизайна.
Полный файл src/pages/catalog/[...page].astro текущего этапа:
---
import Base from '../../layouts/Base.astro';
import { publishedLessons, articleUrl } from '../../lib/lessons';
export async function getStaticPaths({ paginate }) { return paginate(await publishedLessons(), { pageSize: 2 }); }
const { page } = Astro.props;
---
<Base title={`Каталог — страница ${page.currentPage}`}><h1>Каталог уроков</h1><nav aria-label="Темы"><a href="/topics/content.html">Контент</a> <a href="/topics/interface.html">Интерфейс</a></nav><ul>{page.data.map(entry => <li><a href={articleUrl(entry)}>{entry.data.title}</a><p>{entry.data.description}</p></li>)}</ul><nav aria-label="Страницы каталога">{page.url.prev && <a href={page.url.prev}>Предыдущая страница</a>} <span>Страница {page.currentPage} из {page.lastPage}</span> {page.url.next && <a href={page.url.next}>Следующая страница</a>}</nav></Base>
Разбор состояния библиотеки
Разбиение списка не создаёт новые статьи, а создаёт дополнительные представления того же набора. Если в карте проекта считаются материалы, страницы пагинации нужно учитывать отдельно. Число HTML-файлов может расти быстрее числа уроков. Такое различие важно при оценке масштаба библиотеки: двадцать тысяч страниц каталога не означают двадцать тысяч самостоятельных обучающих текстов.
Первая страница rest-маршрута отличается от последующих по адресной форме. Поэтому не заменяем page.url.current ручной строкой /catalog/1/. Конкретная конфигурация может использовать файловые пути у последующих страниц. В будущем просмотре выпишите действительные предоставленные URL. Инструкция должна соответствовать полученному выпуску, а не привычной адресной схеме другого генератора.
Если редактор добавит материал в начало, записи сдвинутся между страницами. Адрес конкретной статьи остаётся прежним, но её положение в списке изменится. Это нормальное поведение пагинации и ещё одна причина разделять ID, permalink и номер списковой страницы. Ссылаться на урок лучше его собственным URL, а не только адресом каталога с надеждой, что запись всегда будет на второй позиции.
Для самостоятельного варианта увеличьте pageSize до трёх. Ожидаемый диапазон станет другим: две страницы, из которых последняя неполная. Затем верните размер два и добавьте шестую публичную запись. Теперь все три страницы полные. Эти два условия помогают увидеть различие между размером страницы и размером общего набора, не меняя одновременно алгоритм сортировки.
Отдельно рассмотрите нулевой набор после временного переключения всех записей в черновики. Поведение paginate и корневого адреса для пустых данных следует определить в продукте. Не обещайте существование красивой пустой первой страницы без соответствующей реализации. Настоящему каталогу может понадобиться статическое корневое состояние, отдельное от динамического разбиения. Наш контрольный набор остаётся непустым, поэтому этот случай отмечен как граница развития.
Ссылки пагинации должны быть различимы по назначению. Навигация имеет собственный aria-label, чтобы не путаться с темами и соседними уроками. Текст страницы сообщает номер и предел. При добавлении числовых ссылок можно отмечать текущую через aria-current. Не превращайте каждую ссылку в кнопку без href: новый внешний вид не должен уничтожать обычные действия браузера и возможность прямого адреса.
При обновлении библиотеки браузер может сохранить в истории адрес поздней страницы, которая больше не создаётся после сокращения списка. Для настоящего сайта следует определить политику таких устаревших списковых URL. Это отдельная задача от сохранения адресов самих статей. Не отправляйте неизвестную страницу каталога на произвольный урок. Понятная ошибка или согласованный редирект должны описывать изменение списка, а не маскировать потерянный маршрут.
Условия для самостоятельного наблюдения
Это план будущего разбора, а не журнал выполненных проверок. Рассмотрите одинаковые исходные данные и сопоставьте действие с ожидаемым состоянием.
| Условие | Ожидаемое состояние и его граница |
|---|---|
| Пять записей по две | Последняя страница содержит один материал, который нельзя потерять при округлении. |
| Первая страница | Ссылка предыдущего положения отсутствует, ссылка следующего присутствует при наличии данных. |
| Последняя страница | Следующий адрес отсутствует, поскольку он не соответствует созданной странице. |
| Черновик исключён | Фильтрация происходит до вычисления диапазона пагинации. |
| Повторяющиеся данные | Нужно сравнить объединение страниц с исходными ID, а не только общее число карточек. |
| Старая index страница | Конкурирующий файл следует удалить при переходе к rest-маршруту. |
Оглавление серии, курсы ProfessorWeb и полный архив исходников. Для этого урока выберите lesson-06.
Первичные источники: Routing, Configuration.
Редакционный статус: локальный черновик. Установка, учебные программы, браузерные сценарии, тесты и публикация не выполнялись. Описанные эффекты ожидаемые.