Маршруты уроков и общая компоновка
Карточка «Маршрута» уже строит ссылку из идентификатора урока. Создадим адреса, которые можно открыть напрямую, отправить другому человеку и сохранить в закладках браузера. В SvelteKit маршрут задаётся деревом файлов, поэтому путь /lessons/html-start будет связан с каталогом lessons/[id]. Общая компоновка обеспечит навигацию между каталогом и кабинетом.
В этом уроке ещё нет личного серверного хранения. Страница конкретного материала будет получать только публичные данные. Отметки остаются локальной демонстрацией из предыдущих глав. Такое ограничение позволяет разобрать маршрутизацию, не смешивая её с проверкой сессии. Все указанные результаты остаются ожидаемыми: приложение здесь не запускалось.
Адрес как часть модели
Динамический сегмент [id] означает, что часть адреса станет параметром запроса. Само имя параметра не проверяет, существует ли урок. Пользователь способен вручную набрать /lessons/unknown, поэтому работа страницы начинается с поиска в каталоге, а не с уверенности, что ссылка пришла только из нашей карточки.
Добавьте полный файл src/routes/lessons/[id]/+page.ts. Это универсальная загрузка публичных данных; в следующем уроке подробнее рассмотрим её среду выполнения.
import { error } from '@sveltejs/kit';
import { lessons } from '$lib/catalog';
import type { PageLoad } from './$types';
export const load: PageLoad = ({ params }) => {
const lesson = lessons.find(item => item.id === params.id);
if (!lesson) error(404, 'Урок не найден');
return { lesson };
};
error в SvelteKit 2 прекращает обычный путь выполнения. После отсутствующего урока не возвращается объект с неопределённым значением, который затем развалится при обращении к названию. Число 404 относится к отсутствию ресурса, а не к невозможности прочитать существующий закрытый кабинет. Эти случаи должны оставаться разными для браузера и для человека.
Теперь создайте полный компонент src/routes/lessons/[id]/+page.svelte. Тип страницы генерируется из соглашений маршрута; он связывает форму результата load с используемыми данными.
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
<svelte:head><title>{data.lesson.title} — Маршрут</title></svelte:head>
<h1>{data.lesson.title}</h1>
<p>Плановая длительность: {data.lesson.minutes} минут.</p>
<p>Это учебная карточка материала. Полный текст ProfessorWeb здесь не копируется.</p>
<a href="/">Все уроки</a>
Ожидаем, что адрес с css-layout покажет «Раскладка страницы» и двадцать пять минут. Это не результат чтения внешнего API; значение взято из прежней фикстуры. Подмена источника позже не должна изменить назначение параметра id.
Шапка, которая переживает переход
Общая компоновка содержит то, что повторяется у дочерних страниц: название приложения, навигацию, основную область. В Svelte 5 дочерняя разметка layout поступает через snippet children. Добавьте полный src/routes/+layout.svelte.
<script lang="ts">
let { children } = $props();
</script>
<a class="skip" href="#main">К содержимому</a>
<header>
<a href="/">Маршрут</a>
<nav aria-label="Основная навигация">
<a href="/">Уроки</a>
<a href="/account">Кабинет</a>
</nav>
</header>
<main id="main" tabindex="-1">{@render children()}</main>
Папка маршрута не должна содержать копию этой шапки в каждой странице. Тогда изменение ссылки в одном layout применится ко всем соответствующим страницам. Но layout не является подходящим местом для любой бизнес-операции: сохранение закладки должно выполняться там, где явно определяется действие, параметры и пользователь.
Пока создайте src/routes/account/+page.svelte с объяснением, что личный кабинет будет подключён в серверных главах. Неработающую ссылку лучше заменить существующей учебной страницей с честным состоянием. Ожидаемый экран сообщает, что постоянное сохранение ещё не доступно, а не показывает выдуманные личные записи.
Прямой запрос и переход внутри приложения
Обычный a сохраняет назначение ссылки: браузер способен открыть её в новой вкладке или скопировать адрес. SvelteKit улучшает навигацию, когда приложение уже работает в браузере, но чтение URL не должно зависеть от предварительного посещения главной. Прямой запрос к странице урока тоже обязан получить подходящий ответ.
Нельзя заменять ссылку на кнопку только потому, что переход выглядит интерактивным. Кнопка меняет состояние или отправляет действие, ссылка выбирает ресурс. Это различие влияет на клавиатуру, контекстное меню и ожидания человека. В предыдущих главах закладка была кнопкой, а заголовок урока — ссылкой; теперь смысл этого разделения становится видимым.
Выбранная структура соответствует официальным соглашениям маршрутов SvelteKit. Дальнейшие разъяснения относятся к модели нашего кабинета: название курса в каталоге, сохранение прогресса и расположение кнопок не диктуются самим фреймворком.
Ошибка идентификатора и устойчивость страниц
Попробуйте рассмотреть несколько адресов до запуска. Известный идентификатор даёт страницу урока. Неизвестный должен вернуть 404. Путь /lessons/ без сегмента не соответствует [id], если для него не создана отдельная страница списка. Адрес /account существует независимо от идентификатора материала. Эти случаи помогают проверить, что маршруты выражают реальную структуру приложения.
Если редактор удалит урок из каталога, сохранённая внешняя ссылка тоже перестанет находить запись. Для опубликованного сайта может понадобиться перенаправление или сообщение о заменившем материале. Учебная фикстура этого не реализует; устойчивые идентификаторы уменьшают риск, но не решают весь жизненный цикл содержания.
Название страницы и структура адреса
Ссылка из карточки использует идентификатор, но заголовок браузерной вкладки должен показывать понятное название. Эти значения решают разные задачи. Если оставить во всех маршрутах только «Маршрут», человек с несколькими открытыми материалами не сможет различить вкладки. Если использовать название как часть постоянного URL без политики переадресации, редакционная правка изменит внешний адрес.
В примере svelte:head задаёт название из уже выбранного урока. Для ошибки не нужно придумывать название неизвестного материала. Экран 404 получает собственную подпись. Такая логика делает прямое открытие ссылки и навигацию внутри приложения одинаково объяснимыми.
Структура папок также влияет на вложенные layout. Будущая область /account/settings могла бы унаследовать шапку кабинета, а публичные уроки — только общую шапку сайта. Однако папка account не доказывает наличие авторизации у каждого endpoint. При добавлении нового серверного обработчика проверка личности остаётся обязательной даже внутри знакомой области маршрутов.
Ещё один вариант — язык в адресе. Если появится /ru/lessons/[id], потребуется определить, сохраняется ли идентификатор материала между переводами и как строится ссылка из карточки. Простое дописывание сегмента в одном href не меняет контракт всего каталога. В нашей серии язык один, поэтому маршрут остаётся коротким; будущую международную структуру следует проектировать до массового изменения постоянных ссылок.
Параметры адреса также нужно считать недоверенным вводом. В текущем поиске они сравниваются с готовыми строками каталога. При будущем запросе базы используйте параметры драйвера, сохраняя проверку существования урока и понятный ответ для отсутствующего ресурса.
Персональные сведения пока нельзя добавить в catalog.ts, потому что этот файл используется публичной страницей и доступен браузерной части. Общая компоновка также не делает данные приватными по названию папки. В следующем уроке разделим универсальную и серверную загрузку и покажем, как передать странице только разрешённый результат.