Кабинет читателя и границы SvelteKit
«Маршрут» помогает читателю ProfessorWeb продолжать обучение: найти урок, добавить закладку и отметить завершение. В этой серии вы постепенно соберёте кабинет, сохраняя одни и те же уроки и идентификаторы. Первый результат — понятная модель приложения: какие данные общедоступны, какие принадлежат читателю и в какой среде выполняется код.
Нужны основы HTML, JavaScript, TypeScript и HTTP. Используем синтаксис Svelte 5 и соглашения SvelteKit 2 не ниже 2.16 для типов PageProps, проверенные по официальной документации 11 октября 2026 года. Это выбранные ветки API, а не заявление о самых новых версиях. Установка, сборка и запуск учебного проекта здесь не выполнялись; описанные экраны и ответы являются ожидаемыми.
Урок, читатель и отметка
Представим три материала: html-start, css-layout, js-events. Название можно исправить, однако идентификатор останется прежним. Он соединяет маршрут, запись закладки и отметку прогресса. Если хранить закладки по заголовку, редакционная правка создаст впечатление, что сохранённый урок исчез. Если хранить по порядковому номеру массива, вставка новой статьи переместит закладку на другой материал.
Полный исходный файл src/lib/catalog.ts задаёт общедоступную фикстуру. Слово «фикстура» означает заранее подготовленные учебные данные; их происхождение не следует путать с запросом к работающему ProfessorWeb.
export type Lesson = {
id: string;
title: string;
minutes: number;
course: string;
};
export const lessons: Lesson[] = [
{ id: 'html-start', title: 'Структура страницы', minutes: 18, course: 'web' },
{ id: 'css-layout', title: 'Раскладка страницы', minutes: 25, course: 'web' },
{ id: 'js-events', title: 'События браузера', minutes: 22, course: 'web' }
];
Для читателя позже понадобятся две независимые коллекции идентификаторов: сохранённые и завершённые уроки. Закладка выражает намерение вернуться, завершение — действие самого читателя. Одно не предполагает другое. Тот, кто закончил материал, может оставить закладку; тот, кто сохранил страницу, может ещё не начинать чтение. Такое различие важно уже при проектировании кнопок, потому что интерфейс должен объяснять смысл изменения.
В начальных уроках отметки будут существовать только внутри страницы. Затем перенесём их в серверный учебный репозиторий и привяжем к доверенной сессии. Это смена владельца данных, а не просто добавление строки сохранения. До перехода перезагрузка сбрасывает отметки; после перехода источник истины находится на сервере. Обе границы будут названы прямо в соответствующих главах.
Компонент и приложение
Svelte описывает отдельный интерфейс: разметку, значения, обработчики и стили. SvelteKit организует приложение вокруг этих компонентов: выбирает страницу по URL, загружает данные, обрабатывает отправку формы и формирует серверный ответ. Поэтому файлу с карточкой урока не требуется самостоятельно разбирать адрес или управлять входом пользователя.
Удобное начальное дерево показывает разделение обязанностей. Каталог не содержит секретов. Компоненты не решают, кому разрешено менять прогресс. Серверные модули позже разместим в отдельной области, чтобы их нельзя было случайно импортировать в браузерный код.
src/
lib/
catalog.ts
components/
LessonCard.svelte
server/
repository.ts
routes/
+layout.svelte
+page.svelte
lessons/[id]/+page.svelte
account/+page.svelte
Это схема будущего состояния, а не требование сразу создавать все файлы. Первую страницу можно написать целиком в +page.svelte. Вынос карточки станет полезным, когда у неё появится повторяющаяся структура и ясный контракт. Разделение ради количества папок только затруднило бы чтение небольшого примера.
Обратите внимание на routes: адрес задаётся структурой каталога, а не таблицей маршрутов внутри компонента. При прямом открытии ссылки запрос приходит серверу. При последующей навигации SvelteKit может обновить страницу в браузере. Один компонент поэтому способен участвовать в двух разных способах получения экрана. Эту модель нельзя сводить к утверждению, что весь Svelte работает только на клиенте. Соглашения маршрутизации описаны в документации SvelteKit.
Первый экран без личных данных
Добавьте полный src/routes/+page.svelte. Он пока выводит только каталог; кнопок записи здесь намеренно нет, потому что их поведение мы ещё не определили.
<script lang="ts">
import { lessons } from '$lib/catalog';
</script>
<svelte:head><title>Маршрут — уроки</title></svelte:head>
<h1>Продолжите обучение</h1>
<ul>
{#each lessons as lesson (lesson.id)}
<li><a href={`/lessons/${lesson.id}`}>{lesson.title}</a>
<span>{lesson.minutes} минут</span></li>
{/each}
</ul>
Ожидаемый экран содержит три ссылки и длительности. Ссылки пока ведут к будущему маршруту; не следует принимать наличие адреса в разметке за готовую страницу. В третьем уроке создадим адресный слой, а в текущей статье достаточно увидеть связь идентификатора и href.
Ключ lesson.id связывает повторяющийся элемент с конкретным уроком. Когда массив изменится, это поможет Svelte сопоставить элементы. Текст внутри фигурных скобок выводится как значение, а не как произвольная HTML-разметка. В нашем каталоге нет необходимости использовать {@html}: обычное название статьи не должно превращаться в исполняемый шаблон.
Что должен сохранить дальнейший проект
У «Маршрута» есть несколько постоянных правил. Публичный каталог одинаков для всех. Личный прогресс никогда не попадает в статически созданную публичную страницу. Запись требует проверки пользователя на сервере, даже если кнопка доступна только после входа. Ответ об успешном сохранении появляется после серверного подтверждения. Эти правила помогут оценивать решения следующих уроков без привязки к конкретному визуальному оформлению.
Представьте, что позже появится второй курс. Не потребуется новый тип пользователя или другой механизм закладок: достаточно устойчивых идентификаторов и поля курса. Но если нужен прогресс внутри длинного материала, одной булевой отметки уже мало. Понадобятся позиции, версии содержания и соглашение о завершении. В этой серии ограничимся завершением целого урока; такое ограничение делает результат проверяемым.
Каталог как договор редактора и интерфейса
Рассмотрим изменение фикстуры: длительность css-layout становится тридцать минут, а заголовок уточняется. Ссылка, закладка и завершение продолжают относиться к css-layout. Это показывает, какие свойства являются содержанием, а какие идентичностью. Не используйте одно поле одновременно для удобного названия и долговечного ключа.
Для будущей загрузки из CMS нужно сохранить тот же договор. Ответ может содержать больше технических полей, но интерфейсу требуется ограниченная модель Lesson. Разбор внешнего JSON должен подтвердить, что идентификатор существует, длительность допустима и название является строкой. Объявление типа в TypeScript не проверяет полученный сетевой ответ само по себе.
Если материал временно недоступен, полезно отдельно решить, остаётся ли он в личной истории. Полное удаление записи и скрытие из публичного каталога имеют разные последствия для читателя. В учебном проекте каталог неизменен; будущий владелец должен определить поведение архивных уроков до подключения постоянно меняющегося источника. Такое решение относится к жизненному циклу содержания, а не к реактивности Svelte.
Сохраните начальный каталог как исходное состояние. Следующий шаг добавит локальные отметки и вычисляемые значения, поэтому вы сможете различить исходные данные, действие читателя и отображаемое следствие. Именно это различие лежит в основе реактивного интерфейса, который далее перенесём в полноценный маршрут приложения.