Порядок уроков и оглавление статьи
Каталог уже сортируется по учебному порядку. Используем тот же набор для кнопок предыдущего и следующего урока, а из результата Markdown сформируем оглавление текущей статьи. Это две разные последовательности: одна связывает документы курса, другая помогает перемещаться внутри документа. Урок согласует их без отдельного списка случайно написанных ссылок.
После render(entry) доступны Content и headings. Из заголовков выбираем второй уровень и строим ссылки на их slug. Идентификатор формирует Markdown-процессор, поэтому не повторяем собственное правило транслитерации текста. Если заголовок изменился, его фрагмент может измениться тоже. Ссылка оглавления должна следовать актуальному результату обработки, а не старому предположению редактора.
Две разные последовательности чтения
Для соседних уроков получаем publishedLessons. Находим текущий ID и берём элементы до и после него. На первом уроке предыдущего элемента нет, на последнем отсутствует следующий. Это правильные границы серии. Не нужно рисовать отключённую ссылку на выдуманный адрес или отправлять последний урок снова на первый без явного редакционного решения.
Адрес каждой соседней записи строится через articleUrl. Благодаря этому материал со старым publicPath остаётся доступным в той же учебной последовательности. Если кнопки использовали бы конкатенацию ID отдельно, они могли бы ссылаться на уже удалённый временный маршрут. Внешне навигация выглядела бы готовой, но прямое действие завершалось бы ошибкой.
Текущий пример считает все пять публичных материалов одной учебной серией. Для библиотеки с несколькими курсами этого недостаточно. Следует ввести явное членство и порядок в серии, затем получать соседей только из её набора. Тематическая принадлежность сама по себе не означает последовательность уроков: два материала одной темы могут быть самостоятельными справочниками.
Поиск позиции использует ID, а не название статьи. Название можно менять ради ясности; ID связывает текущую запись с данными. Если текущая статья почему-то отсутствует в выбранном наборе, индекс станет отрицательным и наивный доступ к соседям даст неверное состояние. В данном маршруте обе стороны используют фильтр опубликованных записей, но производственный компонент должен явно обработать нарушение договора.
В названии ссылки показываем и действие, и название соседнего урока. Это объясняет, куда произойдёт переход, без необходимости открывать документ. Оглавление серии ведёт к каталогу. В более крупном проекте оно должно вести к конкретному курсу, а не к общему списку тысяч материалов. Подмена курса глобальным каталогом допустима только как явно маленький учебный пример.
Ожидаемое состояние — у средней статьи два соседа, у крайних один, а оглавление тела ведёт к двум существующим разделам. Эти наблюдения не выполнены браузером при написании серии. Для будущего просмотра начните с первого и последнего уроков, затем измените порядок двух записей и рассмотрите все затронутые связи после сборки.
Не используем JavaScript для самой навигации. Обычные ссылки работают при выключенных интерактивных советах и поиске. Это сохраняет основную форму чтения библиотеки и помогает поисковым роботам обнаружить связные материалы через последовательность документов.
Полный файл src/pages/articles/[id].astro текущего этапа:
---
import { getCollection, render } from 'astro:content';
import { publishedLessons, articleUrl } from '../../lib/lessons';
import ReadingHint from '../../components/ReadingHint.astro';
import Base from '../../layouts/Base.astro';
export async function getStaticPaths() {
return (await getCollection('lessons', ({ data }) => !data.draft)).map(entry => ({ params: { id: entry.id }, props: { entry } }));
}
const { entry } = Astro.props;
const { Content, headings } = await render(entry);
const sequence = await publishedLessons();
const position = sequence.findIndex(item => item.id === entry.id);
const prev = sequence[position - 1];
const next = sequence[position + 1];
---
<Base title={entry.data.title} description={entry.data.description} canonicalPath={articleUrl(entry)}><h1>{entry.data.title}</h1><nav aria-label="Оглавление статьи"><ul>{headings.filter(h => h.depth === 2).map(h => <li><a href={`#${h.slug}`}>{h.text}</a></li>)}</ul></nav><Content /><ReadingHint /><nav aria-label="Уроки серии">{prev && <a href={articleUrl(prev)}>Предыдущий урок: {prev.data.title}</a>} <a href="/catalog/">Оглавление</a> {next && <a href={articleUrl(next)}>Следующий урок: {next.data.title}</a>}</nav></Base>
Разбор состояния библиотеки
Оглавление тела использует заголовки, которые действительно получены при render. Если автор вставил ручной HTML-заголовок или плагин изменил правила идентификаторов, потребуется рассмотреть результат обработки заново. Не стоит считать любую строку, похожую на заголовок, частью машинного оглавления. Контракт должен соответствовать выбранному Markdown-процессору и фактическим элементам документа.
При повторяющихся названиях разделов процессор может сформировать разные slug. Поэтому алгоритм оглавления должен брать готовые идентификаторы, а не вызывать собственную функцию по тексту. Такое разделение исключает одну распространённую причину неработающих фрагментов: две независимые системы по-разному обработали пробелы, знаки или повтор. Связь с результатом render делает источник адреса однозначным.
Соседние уроки вычисляются из текущего публичного набора. Если черновик скрыт, последовательность проходит мимо него. Это соответствует нашему простому договору, но в настоящем курсе может быть нежелательным, если скрытый урок содержит обязательную предпосылку. Редактор должен решать полноту серии, а не надеяться, что арифметика индексов создаст педагогически правильное продолжение. Порядок данных является только технической формой этого решения.
Для самостоятельного варианта поменяйте order навигации и адресов. После будущей сборки у затронутых страниц должны измениться соседи. Затем верните исходную последовательность. Рассмотрите, какие документы изменяются, хотя их собственный Markdown остался прежним. Это объясняет, почему статический выпуск зависит не только от тела конкретной статьи, но и от данных общего маршрута обучения.
Текст предыдущей ссылки не должен описывать тот урок, который открыт сейчас. При копировании шаблонного абзаца такой дефект выглядит мелким, но нарушает ориентирование. Мы получаем название из соседней записи, поэтому кнопка и URL согласованы. Если нужен короткий вариант названия, лучше добавить отдельное поле данных, а не вручную обрезать строку по неизвестной границе слова.
Навигацию статьи стоит разместить так, чтобы она была заметна после чтения, но не спорила с основным текстом. В примере блок стоит после содержимого и совета. Верхнее оглавление помогает выбрать раздел, нижние ссылки — продолжить серию. При узком экране оба блока должны переноситься и сохранять смысл. Новый визуальный дизайн не должен превращать названия соседей в нечитаемые обрезанные подписи.
Ссылка оглавления в этом небольшом проекте ведёт в общий каталог, поскольку пять записей образуют одну серию. В ProfessorWeb курс имеет самостоятельную страницу и последовательность, поэтому реальная интеграция использует соответствующее оглавление. Не копируйте учебную ссылку как универсальный ответ для всей библиотеки. Архитектурный приём состоит в явной последовательности данных, а конкретный адрес оглавления определяется владельцем этой последовательности.
Условия для самостоятельного наблюдения
Это план будущего разбора, а не журнал выполненных проверок. Рассмотрите одинаковые исходные данные и сопоставьте действие с ожидаемым состоянием.
| Условие | Ожидаемое состояние и его граница |
|---|---|
| Первый урок | Предыдущая ссылка отсутствует, следующая строится по выбранной последовательности. |
| Последний урок | Ссылка следующего отсутствует, переход к началу не создаётся молча. |
| Средний урок | Названия обоих соседей соответствуют текущей сортировке данных. |
| Старый адрес соседа | ArticleUrl сохраняет publicPath вместо удалённого временного маршрута. |
| Заголовок изменён | Оглавление использует slug результата render, а не собственную копию преобразования. |
| Появились разные курсы | Нужно ввести явное членство; тема и общий каталог не заменяют учебную серию. |
Оглавление серии, курсы ProfessorWeb и полный архив исходников. Для этого урока выберите lesson-11.
Первичные источники: Collections, Routing.
Редакционный статус: локальный черновик. Установка, учебные программы, браузерные сценарии, тесты и публикация не выполнялись. Описанные эффекты ожидаемые.